Merge branch 'recloudstream-master'
This commit is contained in:
commit
76ffc77781
|
@ -0,0 +1,47 @@
|
|||
import re
|
||||
import glob
|
||||
import requests
|
||||
|
||||
|
||||
SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt"
|
||||
START_MARKER = "/* begin language list */"
|
||||
END_MARKER = "/* end language list */"
|
||||
XML_NAME = "app/src/main/res/values-"
|
||||
ISO_MAP_URL = "https://gist.githubusercontent.com/Josantonius/b455e315bc7f790d14b136d61d9ae469/raw"
|
||||
INDENT = " "*4
|
||||
|
||||
iso_map = requests.get(ISO_MAP_URL, timeout=300).json()
|
||||
|
||||
# Load settings file
|
||||
src = open(SETTINGS_PATH, "r", encoding='utf-8').read()
|
||||
before_src, rest = src.split(START_MARKER)
|
||||
rest, after_src = rest.split(END_MARKER)
|
||||
|
||||
# Load already added langs
|
||||
languages = {}
|
||||
for lang in re.finditer(r'Triple\("(.*)", "(.*)", "(.*)"\)', rest):
|
||||
flag, name, iso = lang.groups()
|
||||
languages[iso] = (flag, name)
|
||||
|
||||
# Add not yet added langs
|
||||
for folder in glob.glob(f"{XML_NAME}*"):
|
||||
iso = folder[len(XML_NAME):]
|
||||
if iso not in languages.keys():
|
||||
languages[iso] = ("", iso_map.get(iso.lower(),iso))
|
||||
|
||||
# Create triples
|
||||
triples = []
|
||||
for iso in sorted(languages.keys()):
|
||||
flag, name = languages[iso]
|
||||
triples.append(f'{INDENT}Triple("{flag}", "{name}", "{iso}"),')
|
||||
|
||||
# Update settings file
|
||||
open(SETTINGS_PATH, "w+",encoding='utf-8').write(
|
||||
before_src +
|
||||
START_MARKER +
|
||||
"\n" +
|
||||
"\n".join(triples) +
|
||||
"\n" +
|
||||
END_MARKER +
|
||||
after_src
|
||||
)
|
|
@ -0,0 +1,39 @@
|
|||
name: Update locale lists
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- '**.xml'
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: "locale-list"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
create:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate access token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
repository: "recloudstream/cloudstream"
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Edit files
|
||||
run: |
|
||||
python3 .github/locales.py
|
||||
- name: Commit to the repo
|
||||
run: |
|
||||
git config --local user.email "111277985+recloudstream[bot]@users.noreply.github.com"
|
||||
git config --local user.name "recloudstream[bot]"
|
||||
git add .
|
||||
# "echo" returns true so the build succeeds, even if no changed files
|
||||
git commit -m 'update list of locales' || echo
|
||||
git push
|
|
@ -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,7 @@
|
|||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="11" />
|
||||
<option name="gradleJvm" value="Embedded JDK" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
|
|
31
README.md
31
README.md
|
@ -20,33 +20,4 @@
|
|||
### Supported languages:
|
||||
<a href="https://hosted.weblate.org/engage/cloudstream/">
|
||||
<img src="https://hosted.weblate.org/widgets/cloudstream/-/app/multi-auto.svg" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
<!--
|
||||
***The list of supported languages:***
|
||||
* 🇱🇧 Arabic
|
||||
* 🇧🇬 Bulgarian
|
||||
* 🇨🇳 Chinese Simplified
|
||||
* 🇹🇼 Chinese Traditional
|
||||
* 🇭🇷 Croatian
|
||||
* 🇨🇿 Czech
|
||||
* 🇳🇱 Dutch
|
||||
* 🇬🇧 English
|
||||
* 🇫🇷 French
|
||||
* 🇩🇪 German
|
||||
* 🇬🇷 Greek
|
||||
* 🇮🇳 Hindi
|
||||
* 🇮🇩 Indonesian
|
||||
* 🇮🇹 Italian
|
||||
* 🇲🇰 Macedonian
|
||||
* 🇮🇳 Malayalam
|
||||
* 🇳🇴 Norsk
|
||||
* 🇵🇱 Polish
|
||||
* 🇧🇷 Portuguese (Brazil)
|
||||
* 🇷🇴 Romanian
|
||||
* 🇪🇸 Spanish
|
||||
* 🇸🇪 Swedish
|
||||
* 🇵🇭 Tagalog
|
||||
* 🇹🇷 Turkish
|
||||
* 🇻🇳 Vietnamese
|
||||
-->
|
||||
</a>
|
|
@ -138,7 +138,7 @@ class ExampleInstrumentedTest {
|
|||
}
|
||||
break
|
||||
}
|
||||
if(!validResults) {
|
||||
if (!validResults) {
|
||||
System.err.println("Api ${api.name} did not load on any")
|
||||
}
|
||||
|
||||
|
@ -183,7 +183,9 @@ class ExampleInstrumentedTest {
|
|||
getAllProviders().amap { api ->
|
||||
if (api.hasMainPage) {
|
||||
try {
|
||||
val homepage = api.getMainPage()
|
||||
val f = api.mainPage.first()
|
||||
val homepage =
|
||||
api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages))
|
||||
when {
|
||||
homepage == null -> {
|
||||
System.err.println("Homepage provider ${api.name} did not correctly load homepage!")
|
||||
|
@ -192,7 +194,7 @@ class ExampleInstrumentedTest {
|
|||
System.err.println("Homepage provider ${api.name} does not contain any items!")
|
||||
}
|
||||
homepage.items.any { it.list.isEmpty() } -> {
|
||||
System.err.println ("Homepage provider ${api.name} does not have any items on result!")
|
||||
System.err.println("Homepage provider ${api.name} does not have any items on result!")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -231,7 +233,7 @@ class ExampleInstrumentedTest {
|
|||
invalidProvider.add(Pair(api, e))
|
||||
}
|
||||
}
|
||||
if(invalidProvider.isEmpty()) {
|
||||
if (invalidProvider.isEmpty()) {
|
||||
println("No Invalid providers! :D")
|
||||
} else {
|
||||
println("Invalid providers are: ")
|
||||
|
|
|
@ -43,9 +43,9 @@ class CustomReportSender : ReportSender {
|
|||
override fun send(context: Context, errorContent: CrashReportData) {
|
||||
println("Sending report")
|
||||
val url =
|
||||
"https://docs.google.com/forms/u/0/d/e/1FAIpQLSe9Vff8oHGMRXcjgCXZwkjvx3eBdNpn4DzjO0FkcWEU1gEQpA/formResponse"
|
||||
"https://docs.google.com/forms/d/e/1FAIpQLSdOlbgCx7NeaxjvEGyEQlqdh2nCvwjm2vwpP1VwW7REj9Ri3Q/formResponse"
|
||||
val data = mapOf(
|
||||
"entry.1586460852" to errorContent.toJSON()
|
||||
"entry.753293084" to errorContent.toJSON()
|
||||
)
|
||||
|
||||
thread { // to not run it on main thread
|
||||
|
|
|
@ -116,7 +116,7 @@ object CommonActivity {
|
|||
* when setting the app language.
|
||||
**/
|
||||
val appLanguageExceptions = hashMapOf(
|
||||
"zh_TW" to Locale.TRADITIONAL_CHINESE
|
||||
"zh-rTW" to Locale.TRADITIONAL_CHINESE
|
||||
)
|
||||
|
||||
fun setLocale(context: Context?, languageCode: String?) {
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package com.lagradost.cloudstream3
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.ui.HeaderViewDecoration
|
||||
|
||||
fun setHeaderDecoration(view: RecyclerView, @LayoutRes headerViewRes: Int) {
|
||||
val headerView = LayoutInflater.from(view.context).inflate(headerViewRes, null)
|
||||
view.addItemDecoration(HeaderViewDecoration(headerView))
|
||||
}
|
|
@ -13,8 +13,11 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
|
|||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.ui.result.UiText
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
|
@ -81,7 +84,7 @@ object APIHolder {
|
|||
synchronized(allProviders) {
|
||||
initMap()
|
||||
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
|
||||
// Leave the ?. null check, it can crash regardless
|
||||
// Leave the ?. null check, it can crash regardless
|
||||
?: allProviders.firstOrNull { it?.name == apiName }
|
||||
}
|
||||
}
|
||||
|
@ -237,7 +240,6 @@ object APIHolder {
|
|||
}
|
||||
|
||||
private fun Context.getHasTrailers(): Boolean {
|
||||
if (isTvSettings()) return false
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true)
|
||||
}
|
||||
|
@ -319,6 +321,57 @@ object APIHolder {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// THIS IS WORK IN PROGRESS API
|
||||
interface ITag {
|
||||
val name: UiText
|
||||
}
|
||||
|
||||
data class SimpleTag(override val name: UiText, val data: String) : ITag
|
||||
|
||||
enum class SelectType {
|
||||
SingleSelect,
|
||||
MultiSelect,
|
||||
MultiSelectAndExclude,
|
||||
}
|
||||
|
||||
enum class SelectValue {
|
||||
Selected,
|
||||
Excluded,
|
||||
}
|
||||
|
||||
interface GenreSelector {
|
||||
val title: UiText
|
||||
val id : Int
|
||||
}
|
||||
|
||||
data class TagSelector(
|
||||
override val title: UiText,
|
||||
override val id : Int,
|
||||
val tags: Set<ITag>,
|
||||
val defaultTags : Set<ITag> = setOf(),
|
||||
val selectType: SelectType = SelectType.SingleSelect,
|
||||
) : GenreSelector
|
||||
|
||||
data class BoolSelector(
|
||||
override val title: UiText,
|
||||
override val id : Int,
|
||||
|
||||
val defaultValue : Boolean = false,
|
||||
) : GenreSelector
|
||||
|
||||
data class InputField(
|
||||
override val title: UiText,
|
||||
override val id : Int,
|
||||
|
||||
val hint : UiText? = null,
|
||||
) : GenreSelector
|
||||
|
||||
// This response describes how a user might filter the homepage or search results
|
||||
data class GenreResponse(
|
||||
val searchSelectors : List<GenreSelector>,
|
||||
val filterSelectors: List<GenreSelector> = searchSelectors
|
||||
) */
|
||||
|
||||
/*
|
||||
0 = Site not good
|
||||
|
@ -460,6 +513,20 @@ abstract class MainAPI {
|
|||
open val hasMainPage = false
|
||||
open val hasQuickSearch = false
|
||||
|
||||
/**
|
||||
* A set of which ids the provider can open with getLoadUrl()
|
||||
* If the set contains SyncIdName.Imdb then getLoadUrl() can be started with
|
||||
* an Imdb class which inherits from SyncId.
|
||||
*
|
||||
* getLoadUrl() is then used to get page url based on that ID.
|
||||
*
|
||||
* Example:
|
||||
* "tt6723592" -> getLoadUrl(ImdbSyncId("tt6723592")) -> "mainUrl/imdb/tt6723592" -> load("mainUrl/imdb/tt6723592")
|
||||
*
|
||||
* This is used to launch pages from personal lists or recommendations using IDs.
|
||||
**/
|
||||
open val supportedSyncNames = setOf<SyncIdName>()
|
||||
|
||||
open val supportedTypes = setOf(
|
||||
TvType.Movie,
|
||||
TvType.TvSeries,
|
||||
|
@ -530,6 +597,14 @@ abstract class MainAPI {
|
|||
open fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the load() url based on a sync ID like IMDb or MAL.
|
||||
* Only contains SyncIds based on supportedSyncUrls.
|
||||
**/
|
||||
open suspend fun getLoadUrl(name: SyncIdName, id: String): String? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Might need a different implementation for desktop*/
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
package com.lagradost.cloudstream3
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.WindowManager
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
|
@ -28,6 +30,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature
|
|||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.google.android.gms.cast.framework.*
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.navigationrail.NavigationRailView
|
||||
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
|
||||
import com.lagradost.cloudstream3.APIHolder.allProviders
|
||||
|
@ -43,8 +46,7 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
|
|||
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.CommonActivity.updateLocale
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.*
|
||||
import com.lagradost.cloudstream3.network.initClient
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
|
||||
|
@ -58,42 +60,49 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStri
|
|||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
|
||||
import com.lagradost.cloudstream3.ui.APIRepository
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
|
||||
import com.lagradost.cloudstream3.ui.home.HomeViewModel
|
||||
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.search.SearchFragment
|
||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
|
||||
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
|
||||
import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.html
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.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.setUpBackup
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
|
||||
import com.lagradost.cloudstream3.utils.Event
|
||||
import com.lagradost.cloudstream3.utils.IOnBackPressed
|
||||
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
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.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.*
|
||||
import kotlinx.android.synthetic.main.bottom_resultview_preview.*
|
||||
import kotlinx.android.synthetic.main.fragment_result_swipe.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
@ -102,6 +111,7 @@ import java.net.URI
|
|||
import java.net.URLDecoder
|
||||
import java.nio.charset.Charset
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
|
||||
//https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
|
||||
|
@ -241,6 +251,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
Event<Boolean>() // homepage api, used to speed up time to load for homepage
|
||||
val afterRepositoryLoadedEvent = Event<Boolean>()
|
||||
|
||||
// kinda shitty solution, but cant com main->home otherwise for popups
|
||||
val bookmarksUpdatedEvent = Event<Boolean>()
|
||||
|
||||
|
||||
/**
|
||||
* @return true if the str has launched an app task (be it successful or not)
|
||||
* @param isWebview does not handle providers and opening download page if true. Can still add repos and login.
|
||||
|
@ -333,6 +347,16 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}
|
||||
}
|
||||
|
||||
var lastPopup : SearchResponse? = null
|
||||
fun loadPopup(result: SearchResponse) {
|
||||
lastPopup = result
|
||||
viewModel.load(
|
||||
this, result.url, result.apiName, false, if (getApiDubstatusSettings()
|
||||
.contains(DubStatus.Dubbed)
|
||||
) DubStatus.Dubbed else DubStatus.Subbed, null
|
||||
)
|
||||
}
|
||||
|
||||
override fun onColorSelected(dialogId: Int, color: Int) {
|
||||
onColorSelectedEvent.invoke(Pair(dialogId, color))
|
||||
}
|
||||
|
@ -364,6 +388,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
val isNavVisible = listOf(
|
||||
R.id.navigation_home,
|
||||
R.id.navigation_search,
|
||||
R.id.navigation_library,
|
||||
R.id.navigation_downloads,
|
||||
R.id.navigation_settings,
|
||||
R.id.navigation_download_child,
|
||||
|
@ -379,6 +404,27 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
R.id.navigation_settings_plugins,
|
||||
).contains(destination.id)
|
||||
|
||||
|
||||
val dontPush = listOf(
|
||||
R.id.navigation_home,
|
||||
R.id.navigation_search,
|
||||
R.id.navigation_results_phone,
|
||||
R.id.navigation_results_tv,
|
||||
R.id.navigation_player,
|
||||
).contains(destination.id)
|
||||
|
||||
nav_host_fragment?.apply {
|
||||
val params = layoutParams as ConstraintLayout.LayoutParams
|
||||
|
||||
params.setMargins(
|
||||
if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0,
|
||||
params.topMargin,
|
||||
params.rightMargin,
|
||||
params.bottomMargin
|
||||
)
|
||||
layoutParams = params
|
||||
}
|
||||
|
||||
val landscape = when (resources.configuration.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||
true
|
||||
|
@ -393,6 +439,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
|
||||
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
|
||||
}
|
||||
|
||||
//private var mCastSession: CastSession? = null
|
||||
|
@ -445,6 +496,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
// Start any delayed updates
|
||||
if (ApkInstaller.delayedInstaller?.startInstallation() == true) {
|
||||
Toast.makeText(this, R.string.update_started, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
try {
|
||||
if (isCastApiAvailable()) {
|
||||
mSessionManager.removeSessionManagerListener(mSessionManagerListener)
|
||||
|
@ -479,10 +535,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
|
||||
builder.setTitle(R.string.confirm_exit_dialog)
|
||||
builder.apply {
|
||||
setPositiveButton(R.string.yes) { _, _ -> super.onBackPressed() }
|
||||
// Forceful exit since back button can actually go back to setup
|
||||
setPositiveButton(R.string.yes) { _, _ -> exitProcess(0) }
|
||||
setNegativeButton(R.string.no) { _, _ -> }
|
||||
}
|
||||
builder.show()
|
||||
builder.show().setDefaultFocus()
|
||||
}
|
||||
|
||||
private fun backPressed() {
|
||||
|
@ -589,6 +646,37 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}
|
||||
}
|
||||
|
||||
lateinit var viewModel: ResultViewModel2
|
||||
|
||||
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
|
||||
viewModel =
|
||||
ViewModelProvider(this)[ResultViewModel2::class.java]
|
||||
|
||||
return super.onCreateView(name, context, attrs)
|
||||
}
|
||||
|
||||
private fun hidePreviewPopupDialog() {
|
||||
viewModel.clear()
|
||||
bottomPreviewPopup.dismissSafe(this)
|
||||
}
|
||||
|
||||
var bottomPreviewPopup: BottomSheetDialog? = null
|
||||
private fun showPreviewPopupDialog(): BottomSheetDialog {
|
||||
val ret = (bottomPreviewPopup ?: run {
|
||||
val builder =
|
||||
BottomSheetDialog(this)
|
||||
builder.setContentView(R.layout.bottom_resultview_preview)
|
||||
builder.setOnDismissListener {
|
||||
bottomPreviewPopup = null
|
||||
viewModel.clear()
|
||||
}
|
||||
builder.setCanceledOnTouchOutside(true)
|
||||
builder.show()
|
||||
builder
|
||||
})
|
||||
bottomPreviewPopup = ret
|
||||
return ret
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
app.initClient(this)
|
||||
|
@ -619,7 +707,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}
|
||||
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
|
||||
|
||||
updateTv()
|
||||
if (isTvSettings()) {
|
||||
setContentView(R.layout.activity_main_tv)
|
||||
} else {
|
||||
|
@ -675,9 +763,81 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
|
||||
setNegativeButton("Ok") { _, _ -> }
|
||||
}
|
||||
builder.show()
|
||||
builder.show().setDefaultFocus()
|
||||
}
|
||||
|
||||
observeNullable(viewModel.page) { resource ->
|
||||
if (resource == null) {
|
||||
bottomPreviewPopup.dismissSafe(this)
|
||||
return@observeNullable
|
||||
}
|
||||
when (resource) {
|
||||
is Resource.Failure -> {
|
||||
showToast(this, R.string.error)
|
||||
hidePreviewPopupDialog()
|
||||
}
|
||||
is Resource.Loading -> {
|
||||
showPreviewPopupDialog().apply {
|
||||
resultview_preview_loading?.isVisible = true
|
||||
resultview_preview_result?.isVisible = false
|
||||
resultview_preview_loading_shimmer?.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()
|
||||
|
||||
resultview_preview_title?.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)
|
||||
|
||||
resultview_preview_description?.setText(d.plotText)
|
||||
resultview_preview_poster?.setImage(
|
||||
d.posterImage ?: d.posterBackgroundImage
|
||||
)
|
||||
|
||||
resultview_preview_poster?.setOnClickListener {
|
||||
//viewModel.updateWatchStatus(WatchType.PLANTOWATCH)
|
||||
val value = viewModel.watchStatus.value ?: WatchType.NONE
|
||||
|
||||
this@MainActivity.showBottomDialog(
|
||||
WatchType.values().map { getString(it.stringRes) }.toList(),
|
||||
value.ordinal,
|
||||
this@MainActivity.getString(R.string.action_add_to_bookmarks),
|
||||
showApply = false,
|
||||
{}) {
|
||||
viewModel.updateWatchStatus(WatchType.values()[it])
|
||||
bookmarksUpdatedEvent(true)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isTvSettings()) // dont want this clickable on tv layout
|
||||
resultview_preview_description?.setOnClickListener { view ->
|
||||
view.context?.let { ctx ->
|
||||
val builder: AlertDialog.Builder =
|
||||
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
|
||||
builder.setMessage(d.plotText.asString(ctx).html())
|
||||
.setTitle(d.plotHeaderText.asString(ctx))
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
resultview_preview_more_info?.setOnClickListener {
|
||||
hidePreviewPopupDialog()
|
||||
lastPopup?.let {
|
||||
loadSearchResult(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ioSafe {
|
||||
// val plugins =
|
||||
|
@ -743,7 +903,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
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
|
||||
|
||||
}
|
||||
nav_rail?.setOnItemSelectedListener { item ->
|
||||
onNavDestinationSelected(
|
||||
item,
|
||||
|
@ -912,8 +1077,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
// Used to check current focus for TV
|
||||
// main {
|
||||
// while (true) {
|
||||
// delay(1000)
|
||||
// delay(5000)
|
||||
// println("Current focus: $currentFocus")
|
||||
// showToast(this, currentFocus.toString(), Toast.LENGTH_LONG)
|
||||
// }
|
||||
// }
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ open class Acefile : ExtractorApi() {
|
|||
res.substringAfter("\"file\":\"").substringBefore("\","),
|
||||
"$mainUrl/",
|
||||
Qualities.Unknown.value,
|
||||
headers = mapOf("range" to "bytes=0-")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
|
||||
open class ByteShare : ExtractorApi() {
|
||||
override val name = "ByteShare"
|
||||
override val mainUrl = "https://byteshare.net"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val sources = mutableListOf<ExtractorLink>()
|
||||
sources.add(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
url.replace("/embed/", "/download/"),
|
||||
"",
|
||||
Qualities.Unknown.value,
|
||||
)
|
||||
)
|
||||
return sources
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import android.util.Log
|
||||
import java.net.URLDecoder
|
||||
|
||||
open class Cda: ExtractorApi() {
|
||||
override var mainUrl = "https://ebd.cda.pl"
|
||||
override var name = "Cda"
|
||||
override val requiresReferer = false
|
||||
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||
val mediaId = url
|
||||
.split("/").last()
|
||||
.split("?").first()
|
||||
val doc = app.get("https://ebd.cda.pl/647x500/$mediaId", headers=mapOf(
|
||||
"Referer" to "https://ebd.cda.pl/647x500/$mediaId",
|
||||
"User-Agent" to USER_AGENT,
|
||||
"Cookie" to "cda.player=html5"
|
||||
)).document
|
||||
val dataRaw = doc.selectFirst("[player_data]")?.attr("player_data") ?: return null
|
||||
val playerData = tryParseJson<PlayerData>(dataRaw) ?: return null
|
||||
return listOf(ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
getFile(playerData.video.file),
|
||||
referer = "https://ebd.cda.pl/647x500/$mediaId",
|
||||
quality = Qualities.Unknown.value
|
||||
))
|
||||
}
|
||||
|
||||
private fun rot13(a: String): String {
|
||||
return a.map {
|
||||
when {
|
||||
it in 'A'..'M' || it in 'a'..'m' -> it + 13
|
||||
it in 'N'..'Z' || it in 'n'..'z' -> it - 13
|
||||
else -> it
|
||||
}
|
||||
}.joinToString("")
|
||||
}
|
||||
|
||||
private fun cdaUggc(a: String): String {
|
||||
val decoded = rot13(a)
|
||||
return if (decoded.endsWith("adc.mp4")) decoded.replace("adc.mp4",".mp4")
|
||||
else decoded
|
||||
}
|
||||
|
||||
private fun cdaDecrypt(b: String): String {
|
||||
var a = b
|
||||
.replace("_XDDD", "")
|
||||
.replace("_CDA", "")
|
||||
.replace("_ADC", "")
|
||||
.replace("_CXD", "")
|
||||
.replace("_QWE", "")
|
||||
.replace("_Q5", "")
|
||||
.replace("_IKSDE", "")
|
||||
a = URLDecoder.decode(a, "UTF-8")
|
||||
a = a.map { char ->
|
||||
if (32 < char.toInt() && char.toInt() < 127) {
|
||||
return@map String.format("%c", 33 + (char.toInt() + 14) % 94)
|
||||
} else {
|
||||
return@map char
|
||||
}
|
||||
}.joinToString("")
|
||||
a = a
|
||||
.replace(".cda.mp4", "")
|
||||
.replace(".2cda.pl", ".cda.pl")
|
||||
.replace(".3cda.pl", ".cda.pl")
|
||||
return if (a.contains("/upstream")) "https://" + a.replace("/upstream", ".mp4/upstream")
|
||||
else "https://${a}.mp4"
|
||||
}
|
||||
|
||||
private fun getFile(a: String) = when {
|
||||
a.startsWith("uggc") -> cdaUggc(a)
|
||||
!a.startsWith("http") -> cdaDecrypt(a)
|
||||
else -> a
|
||||
}
|
||||
|
||||
data class VideoPlayerData(
|
||||
val file: String,
|
||||
val qualities: Map<String, String> = mapOf(),
|
||||
val quality: String?,
|
||||
val ts: Int?,
|
||||
val hash2: String?
|
||||
)
|
||||
|
||||
data class PlayerData(
|
||||
val video: VideoPlayerData
|
||||
)
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
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.Qualities
|
||||
import java.net.URL
|
||||
|
||||
open class Dailymotion : ExtractorApi() {
|
||||
override val mainUrl = "https://www.dailymotion.com"
|
||||
override val name = "Dailymotion"
|
||||
override val requiresReferer = false
|
||||
|
||||
@Suppress("RegExpSimplifiable")
|
||||
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
|
||||
|
||||
// https://www.dailymotion.com/video/k3JAHfletwk94ayCVIu
|
||||
// https://www.dailymotion.com/embed/video/k3JAHfletwk94ayCVIu
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val embedUrl = getEmbedUrl(url) ?: return
|
||||
val doc = app.get(embedUrl).document
|
||||
val prefix = "window.__PLAYER_CONFIG__ = "
|
||||
val configStr = doc.selectFirst("script:containsData($prefix)")?.data() ?: return
|
||||
val config = tryParseJson<Config>(configStr.substringAfter(prefix)) ?: return
|
||||
val id = getVideoId(embedUrl) ?: return
|
||||
val dmV1st = config.dmInternalData.v1st
|
||||
val dmTs = config.dmInternalData.ts
|
||||
val metaDataUrl =
|
||||
"$mainUrl/player/metadata/video/$id?locale=en&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
||||
val cookies = mapOf(
|
||||
"v1st" to dmV1st,
|
||||
"dmvk" to config.context.dmvk,
|
||||
"ts" to dmTs.toString()
|
||||
)
|
||||
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = cookies)
|
||||
.parsedSafe<MetaData>() ?: return
|
||||
metaData.qualities.forEach { (key, video) ->
|
||||
video.forEach {
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
name,
|
||||
"$name $key",
|
||||
it.url,
|
||||
"",
|
||||
Qualities.Unknown.value,
|
||||
true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEmbedUrl(url: String): String? {
|
||||
if (url.contains("/embed/")) {
|
||||
return url
|
||||
}
|
||||
val vid = getVideoId(url) ?: return null
|
||||
return "$mainUrl/embed/video/$vid"
|
||||
}
|
||||
|
||||
private fun getVideoId(url: String): String? {
|
||||
val path = URL(url).path
|
||||
val id = path.substringAfter("video/")
|
||||
if (id.matches(videoIdRegex)) {
|
||||
return id
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
data class Config(
|
||||
val context: Context,
|
||||
val dmInternalData: InternalData
|
||||
)
|
||||
|
||||
data class InternalData(
|
||||
val ts: Int,
|
||||
val v1st: String
|
||||
)
|
||||
|
||||
data class Context(
|
||||
@JsonProperty("access_token") val accessToken: String?,
|
||||
val dmvk: String,
|
||||
)
|
||||
|
||||
data class MetaData(
|
||||
val qualities: Map<String, List<VideoLink>>
|
||||
)
|
||||
|
||||
data class VideoLink(
|
||||
val type: String,
|
||||
val url: String
|
||||
)
|
||||
|
||||
}
|
|
@ -5,35 +5,50 @@ import com.lagradost.cloudstream3.app
|
|||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||
import com.lagradost.cloudstream3.utils.getAndUnpack
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
open class Fastream: ExtractorApi() {
|
||||
override var mainUrl = "https://fastream.to"
|
||||
override var name = "Fastream"
|
||||
override val requiresReferer = false
|
||||
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||
val id = Regex("emb\\.html\\?(.*)\\=(enc|)").find(url)?.destructured?.component1() ?: return emptyList()
|
||||
val sources = mutableListOf<ExtractorLink>()
|
||||
val response = app.post("$mainUrl/dl",
|
||||
data = mapOf(
|
||||
Pair("op","embed"),
|
||||
Pair("file_code",id),
|
||||
Pair("auto","1")
|
||||
)).document
|
||||
suspend fun getstream(
|
||||
response: Document,
|
||||
sources: ArrayList<ExtractorLink>): Boolean{
|
||||
response.select("script").amap { script ->
|
||||
if (script.data().contains("sources")) {
|
||||
val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
|
||||
val m3u8 = m3u8regex.find(script.data())?.value ?: return@amap
|
||||
if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) {
|
||||
val unpacked = getAndUnpack(script.data())
|
||||
//val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
|
||||
val newm3u8link = unpacked.substringAfter("file:\"").substringBefore("\"")
|
||||
//val m3u8link = m3u8regex.find(unpacked)?.value ?: return@forEach
|
||||
generateM3u8(
|
||||
name,
|
||||
m3u8,
|
||||
newm3u8link,
|
||||
mainUrl
|
||||
).forEach { link ->
|
||||
sources.add(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val sources = ArrayList<ExtractorLink>()
|
||||
val idregex = Regex("emb.html\\?(.*)=")
|
||||
if (url.contains(Regex("(emb.html.*fastream)"))) {
|
||||
val id = idregex.find(url)?.destructured?.component1() ?: ""
|
||||
val response = app.post("https://fastream.to/dl", allowRedirects = false,
|
||||
data = mapOf(
|
||||
"op" to "embed",
|
||||
"file_code" to id,
|
||||
"auto" to "1"
|
||||
)
|
||||
).document
|
||||
getstream(response, sources)
|
||||
}
|
||||
val response = app.get(url, referer = url).document
|
||||
getstream(response, sources)
|
||||
return sources
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
|
@ -11,12 +12,15 @@ open class Linkbox : ExtractorApi() {
|
|||
override val mainUrl = "https://www.linkbox.to"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val id = url.substringAfter("id=")
|
||||
val sources = mutableListOf<ExtractorLink>()
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val id = Regex("""(/file/|id=)(\S+)[&/?]""").find(url)?.groupValues?.get(2)
|
||||
app.get("$mainUrl/api/open/get_url?itemId=$id", referer=url).parsedSafe<Responses>()?.data?.rList?.map { link ->
|
||||
sources.add(
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
|
@ -26,8 +30,6 @@ open class Linkbox : ExtractorApi() {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
return sources
|
||||
}
|
||||
|
||||
data class RList(
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
|
||||
data class Okrulinkdata (
|
||||
@JsonProperty("status" ) var status : String? = null,
|
||||
@JsonProperty("url" ) var url : String? = null
|
||||
)
|
||||
|
||||
open class Okrulink: ExtractorApi() {
|
||||
override var mainUrl = "https://okru.link"
|
||||
override var name = "Okrulink"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val sources = mutableListOf<ExtractorLink>()
|
||||
val key = url.substringAfter("html?t=")
|
||||
val request = app.post("https://apizz.okru.link/decoding", allowRedirects = false,
|
||||
data = mapOf("video" to key)
|
||||
).parsedSafe<Okrulinkdata>()
|
||||
if (request?.url != null) {
|
||||
sources.add(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
request.url!!,
|
||||
"",
|
||||
Qualities.Unknown.value,
|
||||
isM3u8 = false
|
||||
)
|
||||
)
|
||||
}
|
||||
return sources
|
||||
}
|
||||
}
|
|
@ -130,7 +130,7 @@ open class StreamSB : ExtractorApi() {
|
|||
it.value.replace(Regex("(embed-|/e/)"), "")
|
||||
}.first()
|
||||
// val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
|
||||
val master = "$mainUrl/sources49/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
|
||||
val master = "$mainUrl/sources50/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
|
||||
val headers = mapOf(
|
||||
"watchsb" to "sbstream",
|
||||
)
|
||||
|
|
|
@ -1,41 +1,64 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.lagradost.cloudstream3.USER_AGENT
|
||||
import com.lagradost.cloudstream3.mapper
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
|
||||
|
||||
class Cinestart: Tomatomatela() {
|
||||
override var name = "Cinestart"
|
||||
override var mainUrl = "https://cinestart.net"
|
||||
override var name: String = "Cinestart"
|
||||
override val mainUrl: String = "https://cinestart.net"
|
||||
override val details = "vr.php?v="
|
||||
}
|
||||
|
||||
class TomatomatelalClub: Tomatomatela() {
|
||||
override var name: String = "Tomatomatela"
|
||||
override val mainUrl: String = "https://tomatomatela.club"
|
||||
}
|
||||
|
||||
open class Tomatomatela : ExtractorApi() {
|
||||
override var name = "Tomatomatela"
|
||||
override var mainUrl = "https://tomatomatela.com"
|
||||
override val mainUrl = "https://tomatomatela.com"
|
||||
override val requiresReferer = false
|
||||
private data class Tomato (
|
||||
@JsonProperty("status") val status: Int,
|
||||
@JsonProperty("file") val file: String
|
||||
@JsonProperty("file") val file: String?
|
||||
)
|
||||
open val details = "details.php?v="
|
||||
open val embeddetails = "/embed.html#"
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||
val link = url.replace("$mainUrl/embed.html#","$mainUrl/$details")
|
||||
val server = app.get(link, allowRedirects = false).text
|
||||
val json = parseJson<Tomato>(server)
|
||||
if (json.status == 200) return listOf(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
json.file,
|
||||
"",
|
||||
Qualities.Unknown.value,
|
||||
isM3u8 = false
|
||||
val link = url.replace("$mainUrl$embeddetails","$mainUrl/$details")
|
||||
val sources = ArrayList<ExtractorLink>()
|
||||
val server = app.get(link, allowRedirects = false,
|
||||
headers = mapOf(
|
||||
"User-Agent" to USER_AGENT,
|
||||
"Accept" to "application/json, text/javascript, */*; q=0.01",
|
||||
"Accept-Language" to "en-US,en;q=0.5",
|
||||
"X-Requested-With" to "XMLHttpRequest",
|
||||
"DNT" to "1",
|
||||
"Connection" to "keep-alive",
|
||||
"Sec-Fetch-Dest" to "empty",
|
||||
"Sec-Fetch-Mode" to "cors",
|
||||
"Sec-Fetch-Site" to "same-origin"
|
||||
|
||||
)
|
||||
)
|
||||
return null
|
||||
).parsedSafe<Tomato>()
|
||||
if (server?.file != null) {
|
||||
sources.add(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
server.file,
|
||||
"",
|
||||
Qualities.Unknown.value,
|
||||
isM3u8 = false
|
||||
)
|
||||
)
|
||||
}
|
||||
return sources
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package com.lagradost.cloudstream3.metaproviders
|
||||
|
||||
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
|
||||
import com.lagradost.cloudstream3.utils.SyncUtil
|
||||
|
||||
object SyncRedirector {
|
||||
val syncApis = SyncApis
|
||||
|
||||
suspend fun redirect(url: String, preferredUrl: String): String {
|
||||
for (api in syncApis) {
|
||||
if (url.contains(api.mainUrl)) {
|
||||
val otherApi = when (api.name) {
|
||||
aniListApi.name -> "anilist"
|
||||
malApi.name -> "myanimelist"
|
||||
else -> return url
|
||||
}
|
||||
|
||||
return SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl ->
|
||||
realUrl.contains(preferredUrl)
|
||||
} ?: run {
|
||||
throw ErrorLoadingException("Page does not exist on $preferredUrl")
|
||||
}
|
||||
}
|
||||
}
|
||||
return url
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package com.lagradost.cloudstream3.metaproviders
|
||||
|
||||
import com.lagradost.cloudstream3.MainAPI
|
||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||
|
||||
object SyncRedirector {
|
||||
val syncApis = SyncApis
|
||||
private val syncIds =
|
||||
listOf(
|
||||
SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""),
|
||||
SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""")
|
||||
)
|
||||
|
||||
suspend fun redirect(
|
||||
url: String,
|
||||
providerApi: MainAPI
|
||||
): String {
|
||||
// Deprecated since providers should do this instead!
|
||||
|
||||
// Tries built in ID -> ProviderUrl
|
||||
/*
|
||||
for (api in syncApis) {
|
||||
if (url.contains(api.mainUrl)) {
|
||||
val otherApi = when (api.name) {
|
||||
aniListApi.name -> "anilist"
|
||||
malApi.name -> "myanimelist"
|
||||
else -> return url
|
||||
}
|
||||
|
||||
SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl ->
|
||||
realUrl.contains(providerApi.mainUrl)
|
||||
}?.let {
|
||||
return it
|
||||
}
|
||||
// ?: run {
|
||||
// throw ErrorLoadingException("Page does not exist on $preferredUrl")
|
||||
// }
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Tries provider solution
|
||||
// This goes through all sync ids and finds supported id by said provider
|
||||
return syncIds.firstNotNullOfOrNull { (syncName, syncRegex) ->
|
||||
if (providerApi.supportedSyncNames.contains(syncName)) {
|
||||
syncRegex.find(url)?.value?.let {
|
||||
suspendSafeApiCall {
|
||||
providerApi.getLoadUrl(syncName, it)
|
||||
}
|
||||
}
|
||||
} else null
|
||||
} ?: url
|
||||
}
|
||||
}
|
|
@ -49,10 +49,14 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) {
|
|||
}
|
||||
}
|
||||
|
||||
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
||||
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
||||
liveData.observe(this) { it?.let { t -> action(t) } }
|
||||
}
|
||||
|
||||
fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
||||
liveData.observe(this) { action(it) }
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> some(value: T?): Some<T> {
|
||||
return if (value == null) {
|
||||
Some.None
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.webkit.CookieManager
|
|||
import androidx.annotation.AnyThread
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.debugWarning
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.nicehttp.Requests.Companion.await
|
||||
import com.lagradost.nicehttp.cookies
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
@ -26,7 +27,10 @@ class CloudflareKiller : Interceptor {
|
|||
|
||||
init {
|
||||
// Needs to clear cookies between sessions to generate new cookies.
|
||||
CookieManager.getInstance().removeAllCookies(null)
|
||||
normalSafeApiCall {
|
||||
// This can throw an exception on unsupported devices :(
|
||||
CookieManager.getInstance().removeAllCookies(null)
|
||||
}
|
||||
}
|
||||
|
||||
val savedCookies: MutableMap<String, Map<String, String>> = mutableMapOf()
|
||||
|
@ -35,7 +39,7 @@ class CloudflareKiller : Interceptor {
|
|||
* Gets the headers with cookies, webview user agent included!
|
||||
* */
|
||||
fun getCookieHeaders(url: String): Headers {
|
||||
val userAgentHeaders = WebViewResolver.webViewUserAgent?.let {
|
||||
val userAgentHeaders = WebViewResolver.webViewUserAgent?.let {
|
||||
mapOf("user-agent" to it)
|
||||
} ?: emptyMap()
|
||||
|
||||
|
@ -60,7 +64,9 @@ class CloudflareKiller : Interceptor {
|
|||
}
|
||||
|
||||
private fun getWebViewCookie(url: String): String? {
|
||||
return CookieManager.getInstance()?.getCookie(url)
|
||||
return normalSafeApiCall {
|
||||
CookieManager.getInstance()?.getCookie(url)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -255,11 +255,12 @@ object PluginManager {
|
|||
//updatedPlugins.add(activity.getString(R.string.single_plugin_disabled, pluginData.onlineData.second.name))
|
||||
unloadPlugin(pluginData.savedData.filePath)
|
||||
} else if (pluginData.isOutdated) {
|
||||
downloadAndLoadPlugin(
|
||||
downloadPlugin(
|
||||
activity,
|
||||
pluginData.onlineData.second.url,
|
||||
pluginData.savedData.internalName,
|
||||
File(pluginData.savedData.filePath)
|
||||
File(pluginData.savedData.filePath),
|
||||
true
|
||||
).let { success ->
|
||||
if (success)
|
||||
updatedPlugins.add(pluginData.onlineData.second.name)
|
||||
|
@ -341,11 +342,12 @@ object PluginManager {
|
|||
//Log.i(TAG, "notDownloadedPlugins => ${notDownloadedPlugins.toJson()}")
|
||||
|
||||
notDownloadedPlugins.apmap { pluginData ->
|
||||
downloadAndLoadPlugin(
|
||||
downloadPlugin(
|
||||
activity,
|
||||
pluginData.onlineData.second.url,
|
||||
pluginData.savedData.internalName,
|
||||
pluginData.onlineData.first
|
||||
pluginData.onlineData.first,
|
||||
!pluginData.isDisabled
|
||||
).let { success ->
|
||||
if (success)
|
||||
newDownloadPlugins.add(pluginData.onlineData.second.name)
|
||||
|
@ -496,7 +498,7 @@ object PluginManager {
|
|||
}
|
||||
}
|
||||
|
||||
private fun unloadPlugin(absolutePath: String) {
|
||||
fun unloadPlugin(absolutePath: String) {
|
||||
Log.i(TAG, "Unloading plugin: $absolutePath")
|
||||
val plugin = plugins[absolutePath]
|
||||
if (plugin == null) {
|
||||
|
@ -547,49 +549,48 @@ object PluginManager {
|
|||
return File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}/${folderName}/$fileName.cs3")
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for fresh installs
|
||||
* */
|
||||
suspend fun downloadAndLoadPlugin(
|
||||
suspend fun downloadPlugin(
|
||||
activity: Activity,
|
||||
pluginUrl: String,
|
||||
internalName: String,
|
||||
repositoryUrl: String
|
||||
repositoryUrl: String,
|
||||
loadPlugin: Boolean
|
||||
): Boolean {
|
||||
val file = getPluginPath(activity, internalName, repositoryUrl)
|
||||
downloadAndLoadPlugin(activity, pluginUrl, internalName, file)
|
||||
return true
|
||||
return downloadPlugin(activity, pluginUrl, internalName, file, loadPlugin)
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for updates.
|
||||
*
|
||||
* Uses a file instead of repository url, as extensions can get moved it is better to directly
|
||||
* update the files instead of getting the filepath from repo url.
|
||||
* */
|
||||
private suspend fun downloadAndLoadPlugin(
|
||||
suspend fun downloadPlugin(
|
||||
activity: Activity,
|
||||
pluginUrl: String,
|
||||
internalName: String,
|
||||
file: File,
|
||||
loadPlugin: Boolean
|
||||
): Boolean {
|
||||
try {
|
||||
unloadPlugin(file.absolutePath)
|
||||
|
||||
Log.d(TAG, "Downloading plugin: $pluginUrl to ${file.absolutePath}")
|
||||
// The plugin file needs to be salted with the repository url hash as to allow multiple repositories with the same internal plugin names
|
||||
val newFile = downloadPluginToFile(pluginUrl, file)
|
||||
return loadPlugin(
|
||||
activity,
|
||||
newFile ?: return false,
|
||||
PluginData(
|
||||
internalName,
|
||||
pluginUrl,
|
||||
true,
|
||||
newFile.absolutePath,
|
||||
PLUGIN_VERSION_NOT_SET
|
||||
)
|
||||
val newFile = downloadPluginToFile(pluginUrl, file) ?: return false
|
||||
|
||||
val data = PluginData(
|
||||
internalName,
|
||||
pluginUrl,
|
||||
true,
|
||||
newFile.absolutePath,
|
||||
PLUGIN_VERSION_NOT_SET
|
||||
)
|
||||
|
||||
return if (loadPlugin) {
|
||||
unloadPlugin(file.absolutePath)
|
||||
loadPlugin(
|
||||
activity,
|
||||
newFile,
|
||||
data
|
||||
)
|
||||
} else {
|
||||
setPluginData(data)
|
||||
true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
return false
|
||||
|
|
|
@ -7,8 +7,10 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
|||
import com.lagradost.cloudstream3.amap
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginSanitizedFileName
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager.unloadPlugin
|
||||
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
|
||||
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
|
@ -77,7 +79,7 @@ object RepositoryManager {
|
|||
} else if (fixedUrl.contains("^(cloudstreamrepo://)|(https://cs\\.repo/\\??)".toRegex())) {
|
||||
fixedUrl.replace("^(cloudstreamrepo://)|(https://cs\\.repo/\\??)".toRegex(), "").let {
|
||||
return@let if (!it.contains("^https?://".toRegex()))
|
||||
"https://${it}"
|
||||
"https://${it}"
|
||||
else fixedUrl
|
||||
}
|
||||
} else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) {
|
||||
|
@ -132,7 +134,9 @@ object RepositoryManager {
|
|||
file.mkdirs()
|
||||
|
||||
// Overwrite if exists
|
||||
if (file.exists()) { file.delete() }
|
||||
if (file.exists()) {
|
||||
file.delete()
|
||||
}
|
||||
file.createNewFile()
|
||||
|
||||
val body = app.get(pluginUrl).okhttpResponse.body
|
||||
|
@ -141,34 +145,6 @@ object RepositoryManager {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun downloadPluginToFile(
|
||||
context: Context,
|
||||
pluginUrl: String,
|
||||
/** Filename without .cs3 */
|
||||
fileName: String,
|
||||
folder: String
|
||||
): File? {
|
||||
return suspendSafeApiCall {
|
||||
val extensionsDir = File(context.filesDir, ONLINE_PLUGINS_FOLDER)
|
||||
if (!extensionsDir.exists())
|
||||
extensionsDir.mkdirs()
|
||||
|
||||
val newDir = File(extensionsDir, folder)
|
||||
newDir.mkdirs()
|
||||
|
||||
val newFile = File(newDir, "${fileName}.cs3")
|
||||
// Overwrite if exists
|
||||
if (newFile.exists()) {
|
||||
newFile.delete()
|
||||
}
|
||||
newFile.createNewFile()
|
||||
|
||||
val body = app.get(pluginUrl).okhttpResponse.body
|
||||
write(body.byteStream(), newFile.outputStream())
|
||||
newFile
|
||||
}
|
||||
}
|
||||
|
||||
fun getRepositories(): Array<RepositoryData> {
|
||||
return getKey(REPOSITORIES_KEY) ?: emptyArray()
|
||||
}
|
||||
|
@ -200,9 +176,17 @@ object RepositoryManager {
|
|||
extensionsDir,
|
||||
getPluginSanitizedFileName(repository.url)
|
||||
)
|
||||
PluginManager.deleteRepositoryData(file.absolutePath)
|
||||
|
||||
file.delete()
|
||||
// Unload all plugins, not using deletePlugin since we
|
||||
// delete all data and files in deleteRepositoryData
|
||||
normalSafeApiCall {
|
||||
file.listFiles { plugin: File ->
|
||||
unloadPlugin(plugin.absolutePath)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
PluginManager.deleteRepositoryData(file.absolutePath)
|
||||
}
|
||||
|
||||
private fun write(stream: InputStream, output: OutputStream) {
|
||||
|
|
|
@ -13,6 +13,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
|||
val openSubtitlesApi = OpenSubtitlesApi(0)
|
||||
val indexSubtitlesApi = IndexSubtitleApi()
|
||||
val addic7ed = Addic7ed()
|
||||
val localListApi = LocalList()
|
||||
|
||||
// used to login via app intent
|
||||
val OAuth2Apis
|
||||
|
@ -29,7 +30,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
|||
// used for active syncing
|
||||
val SyncApis
|
||||
get() = listOf(
|
||||
SyncRepo(malApi), SyncRepo(aniListApi)
|
||||
SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi)
|
||||
)
|
||||
|
||||
val inAppAuths
|
||||
|
|
|
@ -1,10 +1,31 @@
|
|||
package com.lagradost.cloudstream3.syncproviders
|
||||
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||
import com.lagradost.cloudstream3.ui.result.UiText
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
|
||||
enum class SyncIdName {
|
||||
Anilist,
|
||||
MyAnimeList,
|
||||
Trakt,
|
||||
Imdb,
|
||||
LocalList
|
||||
}
|
||||
|
||||
interface SyncAPI : OAuth2API {
|
||||
/**
|
||||
* Set this to true if the user updates something on the list like watch status or score
|
||||
**/
|
||||
var requireLibraryRefresh: Boolean
|
||||
val mainUrl: String
|
||||
|
||||
/**
|
||||
* Allows certain providers to open pages from
|
||||
* library links.
|
||||
**/
|
||||
val syncIdName: SyncIdName
|
||||
|
||||
/**
|
||||
-1 -> None
|
||||
0 -> Watching
|
||||
|
@ -22,7 +43,9 @@ interface SyncAPI : OAuth2API {
|
|||
|
||||
suspend fun search(name: String): List<SyncSearchResult>?
|
||||
|
||||
fun getIdFromUrl(url : String) : String
|
||||
suspend fun getPersonalLibrary(): LibraryMetadata?
|
||||
|
||||
fun getIdFromUrl(url: String): String
|
||||
|
||||
data class SyncSearchResult(
|
||||
override val name: String,
|
||||
|
@ -42,7 +65,7 @@ interface SyncAPI : OAuth2API {
|
|||
val score: Int?,
|
||||
val watchedEpisodes: Int?,
|
||||
var isFavorite: Boolean? = null,
|
||||
var maxEpisodes : Int? = null,
|
||||
var maxEpisodes: Int? = null,
|
||||
)
|
||||
|
||||
data class SyncResult(
|
||||
|
@ -63,9 +86,9 @@ interface SyncAPI : OAuth2API {
|
|||
var genres: List<String>? = null,
|
||||
var synonyms: List<String>? = null,
|
||||
var trailers: List<String>? = null,
|
||||
var isAdult : Boolean? = null,
|
||||
var isAdult: Boolean? = null,
|
||||
var posterUrl: String? = null,
|
||||
var backgroundPosterUrl : String? = null,
|
||||
var backgroundPosterUrl: String? = null,
|
||||
|
||||
/** In unixtime */
|
||||
var startDate: Long? = null,
|
||||
|
@ -76,4 +99,61 @@ interface SyncAPI : OAuth2API {
|
|||
var prevSeason: SyncSearchResult? = null,
|
||||
var actors: List<ActorData>? = null,
|
||||
)
|
||||
|
||||
|
||||
data class Page(
|
||||
val title: UiText, var items: List<LibraryItem>
|
||||
) {
|
||||
fun sort(method: ListSorting?, query: String? = null) {
|
||||
items = when (method) {
|
||||
ListSorting.Query ->
|
||||
if (query != null) {
|
||||
items.sortedBy {
|
||||
-FuzzySearch.partialRatio(
|
||||
query.lowercase(), it.name.lowercase()
|
||||
)
|
||||
}
|
||||
} else items
|
||||
ListSorting.RatingHigh -> items.sortedBy { -(it.personalRating ?: 0) }
|
||||
ListSorting.RatingLow -> items.sortedBy { (it.personalRating ?: 0) }
|
||||
ListSorting.AlphabeticalA -> items.sortedBy { it.name }
|
||||
ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed()
|
||||
ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) }
|
||||
ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime }
|
||||
else -> items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class LibraryMetadata(
|
||||
val allLibraryLists: List<LibraryList>,
|
||||
val supportedListSorting: Set<ListSorting>
|
||||
)
|
||||
|
||||
data class LibraryList(
|
||||
val name: UiText,
|
||||
val items: List<LibraryItem>
|
||||
)
|
||||
|
||||
data class LibraryItem(
|
||||
override val name: String,
|
||||
override val url: String,
|
||||
/**
|
||||
* Unique unchanging string used for data storage.
|
||||
* This should be the actual id when you change scores and status
|
||||
* since score changes from library might get added in the future.
|
||||
**/
|
||||
val syncId: String,
|
||||
val episodesCompleted: Int?,
|
||||
val episodesTotal: Int?,
|
||||
/** Out of 100 */
|
||||
val personalRating: Int?,
|
||||
val lastUpdatedUnixTime: Long?,
|
||||
override val apiName: String,
|
||||
override var type: TvType?,
|
||||
override var posterUrl: String?,
|
||||
override var posterHeaders: Map<String, String>?,
|
||||
override var quality: SearchQuality?,
|
||||
override var id: Int? = null,
|
||||
) : SearchResponse
|
||||
}
|
|
@ -11,26 +11,38 @@ class SyncRepo(private val repo: SyncAPI) {
|
|||
val icon = repo.icon
|
||||
val mainUrl = repo.mainUrl
|
||||
val requiresLogin = repo.requiresLogin
|
||||
val syncIdName = repo.syncIdName
|
||||
var requireLibraryRefresh: Boolean
|
||||
get() = repo.requireLibraryRefresh
|
||||
set(value) {
|
||||
repo.requireLibraryRefresh = value
|
||||
}
|
||||
|
||||
suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> {
|
||||
return safeApiCall { repo.score(id, status) }
|
||||
}
|
||||
|
||||
suspend fun getStatus(id : String) : Resource<SyncAPI.SyncStatus> {
|
||||
suspend fun getStatus(id: String): Resource<SyncAPI.SyncStatus> {
|
||||
return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") }
|
||||
}
|
||||
|
||||
suspend fun getResult(id : String) : Resource<SyncAPI.SyncResult> {
|
||||
suspend fun getResult(id: String): Resource<SyncAPI.SyncResult> {
|
||||
return safeApiCall { repo.getResult(id) ?: throw ErrorLoadingException("No data") }
|
||||
}
|
||||
|
||||
suspend fun search(query : String) : Resource<List<SyncAPI.SyncSearchResult>> {
|
||||
suspend fun search(query: String): Resource<List<SyncAPI.SyncSearchResult>> {
|
||||
return safeApiCall { repo.search(query) ?: throw ErrorLoadingException() }
|
||||
}
|
||||
|
||||
fun hasAccount() : Boolean {
|
||||
suspend fun getPersonalLibrary(): Resource<SyncAPI.LibraryMetadata> {
|
||||
return safeApiCall { repo.getPersonalLibrary() ?: throw ErrorLoadingException() }
|
||||
}
|
||||
|
||||
fun hasAccount(): Boolean {
|
||||
return normalSafeApiCall { repo.loginInfo() != null } ?: false
|
||||
}
|
||||
|
||||
fun getIdFromUrl(url : String) : String = repo.getIdFromUrl(url)
|
||||
fun getIdFromUrl(url: String): String? = normalSafeApiCall {
|
||||
repo.getIdFromUrl(url)
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
package com.lagradost.cloudstream3.syncproviders.providers
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
|
@ -12,6 +12,9 @@ import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
|||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
|
@ -27,10 +30,12 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
override val key = "6871"
|
||||
override val redirectUrl = "anilistlogin"
|
||||
override val idPrefix = "anilist"
|
||||
override var requireLibraryRefresh = true
|
||||
override var mainUrl = "https://anilist.co"
|
||||
override val icon = R.drawable.ic_anilist_icon
|
||||
override val requiresLogin = false
|
||||
override val createAccountUrl = "$mainUrl/signup"
|
||||
override val syncIdName = SyncIdName.Anilist
|
||||
|
||||
override fun loginInfo(): AuthAPI.LoginInfo? {
|
||||
// context.getUser(true)?.
|
||||
|
@ -45,6 +50,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
}
|
||||
|
||||
override fun logOut() {
|
||||
requireLibraryRefresh = true
|
||||
removeAccountKeys()
|
||||
}
|
||||
|
||||
|
@ -64,8 +70,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
switchToNewAccount()
|
||||
setKey(accountId, ANILIST_UNIXTIME_KEY, endTime)
|
||||
setKey(accountId, ANILIST_TOKEN_KEY, token)
|
||||
setKey(ANILIST_SHOULD_UPDATE_LIST, true)
|
||||
val user = getUser()
|
||||
requireLibraryRefresh = true
|
||||
return user != null
|
||||
}
|
||||
|
||||
|
@ -140,7 +146,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
this.name,
|
||||
recMedia.id?.toString() ?: return@mapNotNull null,
|
||||
getUrlFromId(recMedia.id),
|
||||
recMedia.coverImage?.large ?: recMedia.coverImage?.medium
|
||||
recMedia.coverImage?.extraLarge ?: recMedia.coverImage?.large
|
||||
?: recMedia.coverImage?.medium
|
||||
)
|
||||
},
|
||||
trailers = when (season.trailer?.site?.lowercase()?.trim()) {
|
||||
|
@ -170,7 +177,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
fromIntToAnimeStatus(status.status),
|
||||
status.score,
|
||||
status.watchedEpisodes
|
||||
)
|
||||
).also {
|
||||
requireLibraryRefresh = requireLibraryRefresh || it
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -181,7 +190,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
const val ANILIST_TOKEN_KEY: String = "anilist_token" // anilist token for api
|
||||
const val ANILIST_USER_KEY: String = "anilist_user" // user data like profile
|
||||
const val ANILIST_CACHED_LIST: String = "anilist_cached_list"
|
||||
const val ANILIST_SHOULD_UPDATE_LIST: String = "anilist_should_update_list"
|
||||
|
||||
private fun fixName(name: String): String {
|
||||
return name.lowercase(Locale.ROOT).replace(" ", "")
|
||||
|
@ -219,7 +227,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
romaji
|
||||
}
|
||||
idMal
|
||||
coverImage { medium large }
|
||||
coverImage { medium large extraLarge }
|
||||
averageScore
|
||||
}
|
||||
}
|
||||
|
@ -232,7 +240,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
format
|
||||
id
|
||||
idMal
|
||||
coverImage { medium large }
|
||||
coverImage { medium large extraLarge }
|
||||
averageScore
|
||||
title {
|
||||
english
|
||||
|
@ -292,15 +300,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
val shows = searchShows(name.replace(blackListRegex, ""))
|
||||
|
||||
shows?.data?.Page?.media?.find {
|
||||
malId ?: "NONE" == it.idMal.toString()
|
||||
(malId ?: "NONE") == it.idMal.toString()
|
||||
}?.let { return it }
|
||||
|
||||
val filtered =
|
||||
shows?.data?.Page?.media?.filter {
|
||||
(
|
||||
it.startDate.year ?: year.toString() == year.toString()
|
||||
|| year == null
|
||||
)
|
||||
(((it.startDate.year ?: year.toString()) == year.toString()
|
||||
|| year == null))
|
||||
}
|
||||
filtered?.forEach {
|
||||
it.title.romaji?.let { romaji ->
|
||||
|
@ -312,14 +318,14 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
}
|
||||
|
||||
// Changing names of these will show up in UI
|
||||
enum class AniListStatusType(var value: Int) {
|
||||
Watching(0),
|
||||
Completed(1),
|
||||
Paused(2),
|
||||
Dropped(3),
|
||||
Planning(4),
|
||||
ReWatching(5),
|
||||
None(-1)
|
||||
enum class AniListStatusType(var value: Int, @StringRes val stringRes: Int) {
|
||||
Watching(0, R.string.type_watching),
|
||||
Completed(1, R.string.type_completed),
|
||||
Paused(2, R.string.type_on_hold),
|
||||
Dropped(3, R.string.type_dropped),
|
||||
Planning(4, R.string.type_plan_to_watch),
|
||||
ReWatching(5, R.string.type_re_watching),
|
||||
None(-1, R.string.none)
|
||||
}
|
||||
|
||||
fun fromIntToAnimeStatus(inp: Int): AniListStatusType {//= AniListStatusType.values().first { it.value == inp }
|
||||
|
@ -335,7 +341,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
}
|
||||
}
|
||||
|
||||
fun convertAnilistStringToStatus(string: String): AniListStatusType {
|
||||
fun convertAniListStringToStatus(string: String): AniListStatusType {
|
||||
return fromIntToAnimeStatus(aniListStatusString.indexOf(string))
|
||||
}
|
||||
|
||||
|
@ -526,7 +532,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
app.post(
|
||||
"https://graphql.anilist.co/",
|
||||
headers = mapOf(
|
||||
"Authorization" to "Bearer " + (getAuth() ?: return@suspendSafeApiCall null),
|
||||
"Authorization" to "Bearer " + (getAuth()
|
||||
?: return@suspendSafeApiCall null),
|
||||
if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
|
||||
),
|
||||
cacheTime = 0,
|
||||
|
@ -575,7 +582,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
|
||||
data class CoverImage(
|
||||
@JsonProperty("medium") val medium: String?,
|
||||
@JsonProperty("large") val large: String?
|
||||
@JsonProperty("large") val large: String?,
|
||||
@JsonProperty("extraLarge") val extraLarge: String?
|
||||
)
|
||||
|
||||
data class Media(
|
||||
|
@ -602,7 +610,29 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
@JsonProperty("score") val score: Int,
|
||||
@JsonProperty("private") val private: Boolean,
|
||||
@JsonProperty("media") val media: Media
|
||||
)
|
||||
) {
|
||||
fun toLibraryItem(): SyncAPI.LibraryItem {
|
||||
return SyncAPI.LibraryItem(
|
||||
// English title first
|
||||
this.media.title.english ?: this.media.title.romaji
|
||||
?: this.media.synonyms.firstOrNull()
|
||||
?: "",
|
||||
"https://anilist.co/anime/${this.media.id}/",
|
||||
this.media.id.toString(),
|
||||
this.progress,
|
||||
this.media.episodes,
|
||||
this.score,
|
||||
this.updatedAt.toLong(),
|
||||
"AniList",
|
||||
TvType.Anime,
|
||||
this.media.coverImage.extraLarge ?: this.media.coverImage.large
|
||||
?: this.media.coverImage.medium,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Lists(
|
||||
@JsonProperty("status") val status: String?,
|
||||
|
@ -617,40 +647,59 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
@JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection
|
||||
)
|
||||
|
||||
fun getAnilistListCached(): Array<Lists>? {
|
||||
private fun getAniListListCached(): Array<Lists>? {
|
||||
return getKey(ANILIST_CACHED_LIST) as? Array<Lists>
|
||||
}
|
||||
|
||||
suspend fun getAnilistAnimeListSmart(): Array<Lists>? {
|
||||
private suspend fun getAniListAnimeListSmart(): Array<Lists>? {
|
||||
if (getAuth() == null) return null
|
||||
|
||||
if (checkToken()) return null
|
||||
return if (getKey(ANILIST_SHOULD_UPDATE_LIST, true) == true) {
|
||||
val list = getFullAnilistList()?.data?.MediaListCollection?.lists?.toTypedArray()
|
||||
return if (requireLibraryRefresh) {
|
||||
val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray()
|
||||
if (list != null) {
|
||||
setKey(ANILIST_CACHED_LIST, list)
|
||||
setKey(ANILIST_SHOULD_UPDATE_LIST, false)
|
||||
}
|
||||
list
|
||||
} else {
|
||||
getAnilistListCached()
|
||||
getAniListListCached()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getFullAnilistList(): FullAnilistList? {
|
||||
var userID: Int? = null
|
||||
/** WARNING ASSUMES ONE USER! **/
|
||||
getKeys(ANILIST_USER_KEY)?.forEach { key ->
|
||||
getKey<AniListUser>(key, null)?.let {
|
||||
userID = it.id
|
||||
}
|
||||
}
|
||||
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
|
||||
val list = getAniListAnimeListSmart()?.groupBy {
|
||||
convertAniListStringToStatus(it.status ?: "").stringRes
|
||||
}?.mapValues { group ->
|
||||
group.value.map { it.entries.map { entry -> entry.toLibraryItem() } }.flatten()
|
||||
} ?: emptyMap()
|
||||
|
||||
val fixedUserID = userID ?: return null
|
||||
// To fill empty lists when AniList does not return them
|
||||
val baseMap =
|
||||
AniListStatusType.values().filter { it.value >= 0 }.associate {
|
||||
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
||||
}
|
||||
|
||||
return SyncAPI.LibraryMetadata(
|
||||
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
|
||||
setOf(
|
||||
ListSorting.AlphabeticalA,
|
||||
ListSorting.AlphabeticalZ,
|
||||
ListSorting.UpdatedNew,
|
||||
ListSorting.UpdatedOld,
|
||||
ListSorting.RatingHigh,
|
||||
ListSorting.RatingLow,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getFullAniListList(): FullAnilistList? {
|
||||
/** WARNING ASSUMES ONE USER! **/
|
||||
|
||||
val userID = getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.id ?: return null
|
||||
val mediaType = "ANIME"
|
||||
|
||||
val query = """
|
||||
query (${'$'}userID: Int = $fixedUserID, ${'$'}MEDIA: MediaType = $mediaType) {
|
||||
query (${'$'}userID: Int = $userID, ${'$'}MEDIA: MediaType = $mediaType) {
|
||||
MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) {
|
||||
lists {
|
||||
status
|
||||
|
@ -661,7 +710,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
startedAt { year month day }
|
||||
updatedAt
|
||||
progress
|
||||
score
|
||||
score (format: POINT_100)
|
||||
private
|
||||
media
|
||||
{
|
||||
|
@ -677,7 +726,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
english
|
||||
romaji
|
||||
}
|
||||
coverImage { medium }
|
||||
coverImage { extraLarge large medium }
|
||||
synonyms
|
||||
nextAiringEpisode {
|
||||
timeUntilAiring
|
||||
|
|
|
@ -4,7 +4,6 @@ import android.util.Log
|
|||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.imdbUrlToIdNullable
|
||||
import com.lagradost.cloudstream3.network.CloudflareKiller
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||
|
@ -22,7 +21,7 @@ class IndexSubtitleApi : AbstractSubApi {
|
|||
|
||||
|
||||
companion object {
|
||||
const val host = "https://subscene.cyou"
|
||||
const val host = "https://indexsubtitle.com"
|
||||
const val TAG = "INDEXSUBS"
|
||||
}
|
||||
|
||||
|
@ -242,7 +241,7 @@ class IndexSubtitleApi : AbstractSubApi {
|
|||
document.selectFirst("div.my-3.p-3 div.media a")!!.attr("href")
|
||||
)
|
||||
} else {
|
||||
document.select("div.my-3.p-3 div.media").mapNotNull { block ->
|
||||
document.select("div.my-3.p-3 div.media").firstNotNullOf { block ->
|
||||
val name =
|
||||
block.selectFirst("strong.d-block")?.text()?.trim().toString()
|
||||
if (seasonNum!! > 0) {
|
||||
|
@ -254,7 +253,7 @@ class IndexSubtitleApi : AbstractSubApi {
|
|||
} else {
|
||||
fixUrl(block.selectFirst("a")!!.attr("href"))
|
||||
}
|
||||
}.first()
|
||||
}
|
||||
}
|
||||
return link
|
||||
}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
package com.lagradost.cloudstream3.syncproviders.providers
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
|
||||
|
||||
class LocalList : SyncAPI {
|
||||
override val name = "Local"
|
||||
override val icon: Int = R.drawable.ic_baseline_storage_24
|
||||
override val requiresLogin = false
|
||||
override val createAccountUrl: Nothing? = null
|
||||
override val idPrefix = "local"
|
||||
override var requireLibraryRefresh = true
|
||||
|
||||
override fun loginInfo(): AuthAPI.LoginInfo {
|
||||
return AuthAPI.LoginInfo(
|
||||
null,
|
||||
null,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
override fun logOut() {
|
||||
|
||||
}
|
||||
|
||||
override val key: String = ""
|
||||
override val redirectUrl = ""
|
||||
override suspend fun handleRedirect(url: String): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun authenticate(activity: FragmentActivity?) {
|
||||
}
|
||||
|
||||
override val mainUrl = ""
|
||||
override val syncIdName = SyncIdName.LocalList
|
||||
override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun getStatus(id: String): SyncAPI.SyncStatus? {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun getResult(id: String): SyncAPI.SyncResult? {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata? {
|
||||
val watchStatusIds = ioWork {
|
||||
getAllWatchStateIds()?.map { id ->
|
||||
Pair(id, getResultWatchState(id))
|
||||
}
|
||||
}?.distinctBy { it.first } ?: return null
|
||||
|
||||
val list = ioWork {
|
||||
watchStatusIds.groupBy {
|
||||
it.second.stringRes
|
||||
}.mapValues { group ->
|
||||
group.value.mapNotNull {
|
||||
getBookmarkedData(it.first)?.toLibraryItem(it.first.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
|
||||
// None is not something to display
|
||||
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
||||
}
|
||||
return SyncAPI.LibraryMetadata(
|
||||
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
|
||||
setOf(
|
||||
ListSorting.AlphabeticalA,
|
||||
ListSorting.AlphabeticalZ,
|
||||
// ListSorting.UpdatedNew,
|
||||
// ListSorting.UpdatedOld,
|
||||
// ListSorting.RatingHigh,
|
||||
// ListSorting.RatingLow,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getIdFromUrl(url: String): String {
|
||||
return url
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.lagradost.cloudstream3.syncproviders.providers
|
||||
|
||||
import android.util.Base64
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
|
@ -8,11 +9,15 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.ShowStatus
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
|
||||
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
|
||||
|
@ -31,13 +36,15 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
override val redirectUrl = "mallogin"
|
||||
override val idPrefix = "mal"
|
||||
override var mainUrl = "https://myanimelist.net"
|
||||
val apiUrl = "https://api.myanimelist.net"
|
||||
private val apiUrl = "https://api.myanimelist.net"
|
||||
override val icon = R.drawable.mal_logo
|
||||
override val requiresLogin = false
|
||||
|
||||
override val syncIdName = SyncIdName.MyAnimeList
|
||||
override var requireLibraryRefresh = true
|
||||
override val createAccountUrl = "$mainUrl/register.php"
|
||||
|
||||
override fun logOut() {
|
||||
requireLibraryRefresh = true
|
||||
removeAccountKeys()
|
||||
}
|
||||
|
||||
|
@ -90,7 +97,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
fromIntToAnimeStatus(status.status),
|
||||
status.score,
|
||||
status.watchedEpisodes
|
||||
)
|
||||
).also {
|
||||
requireLibraryRefresh = requireLibraryRefresh || it
|
||||
}
|
||||
}
|
||||
|
||||
data class MalAnime(
|
||||
|
@ -248,10 +257,45 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
|
||||
const val MAL_USER_KEY: String = "mal_user" // user data like profile
|
||||
const val MAL_CACHED_LIST: String = "mal_cached_list"
|
||||
const val MAL_SHOULD_UPDATE_LIST: String = "mal_should_update_list"
|
||||
const val MAL_UNIXTIME_KEY: String = "mal_unixtime" // When token expires
|
||||
const val MAL_REFRESH_TOKEN_KEY: String = "mal_refresh_token" // refresh token
|
||||
const val MAL_TOKEN_KEY: String = "mal_token" // anilist token for api
|
||||
|
||||
fun convertToStatus(string: String): MalStatusType {
|
||||
return fromIntToAnimeStatus(malStatusAsString.indexOf(string))
|
||||
}
|
||||
|
||||
enum class MalStatusType(var value: Int, @StringRes val stringRes: Int) {
|
||||
Watching(0, R.string.type_watching),
|
||||
Completed(1, R.string.type_completed),
|
||||
OnHold(2, R.string.type_on_hold),
|
||||
Dropped(3, R.string.type_dropped),
|
||||
PlanToWatch(4, R.string.type_plan_to_watch),
|
||||
None(-1, R.string.type_none)
|
||||
}
|
||||
|
||||
private fun fromIntToAnimeStatus(inp: Int): MalStatusType {//= AniListStatusType.values().first { it.value == inp }
|
||||
return when (inp) {
|
||||
-1 -> MalStatusType.None
|
||||
0 -> MalStatusType.Watching
|
||||
1 -> MalStatusType.Completed
|
||||
2 -> MalStatusType.OnHold
|
||||
3 -> MalStatusType.Dropped
|
||||
4 -> MalStatusType.PlanToWatch
|
||||
5 -> MalStatusType.Watching
|
||||
else -> MalStatusType.None
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDateLong(string: String?): Long? {
|
||||
return try {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(
|
||||
string ?: return null
|
||||
)?.time?.div(1000)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun handleRedirect(url: String): Boolean {
|
||||
|
@ -275,7 +319,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
switchToNewAccount()
|
||||
storeToken(res)
|
||||
val user = getMalUser()
|
||||
setKey(MAL_SHOULD_UPDATE_LIST, true)
|
||||
requireLibraryRefresh = true
|
||||
return user != null
|
||||
}
|
||||
}
|
||||
|
@ -308,9 +352,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime))
|
||||
setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token)
|
||||
setKey(accountId, MAL_TOKEN_KEY, token.access_token)
|
||||
requireLibraryRefresh = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -329,7 +374,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
).text
|
||||
storeToken(res)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -382,7 +427,24 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
data class Data(
|
||||
@JsonProperty("node") val node: Node,
|
||||
@JsonProperty("list_status") val list_status: ListStatus?,
|
||||
)
|
||||
) {
|
||||
fun toLibraryItem(): SyncAPI.LibraryItem {
|
||||
return SyncAPI.LibraryItem(
|
||||
this.node.title,
|
||||
"https://myanimelist.net/anime/${this.node.id}/",
|
||||
this.node.id.toString(),
|
||||
this.list_status?.num_episodes_watched,
|
||||
this.node.num_episodes,
|
||||
this.list_status?.score?.times(10),
|
||||
parseDateLong(this.list_status?.updated_at),
|
||||
"MAL",
|
||||
TvType.Anime,
|
||||
this.node.main_picture?.large ?: this.node.main_picture?.medium,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Paging(
|
||||
@JsonProperty("next") val next: String?
|
||||
|
@ -413,18 +475,43 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
return getKey(MAL_CACHED_LIST) as? Array<Data>
|
||||
}
|
||||
|
||||
suspend fun getMalAnimeListSmart(): Array<Data>? {
|
||||
private suspend fun getMalAnimeListSmart(): Array<Data>? {
|
||||
if (getAuth() == null) return null
|
||||
return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) {
|
||||
return if (requireLibraryRefresh) {
|
||||
val list = getMalAnimeList()
|
||||
setKey(MAL_CACHED_LIST, list)
|
||||
setKey(MAL_SHOULD_UPDATE_LIST, false)
|
||||
list
|
||||
} else {
|
||||
getMalAnimeListCached()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
|
||||
val list = getMalAnimeListSmart()?.groupBy {
|
||||
convertToStatus(it.list_status?.status ?: "").stringRes
|
||||
}?.mapValues { group ->
|
||||
group.value.map { it.toLibraryItem() }
|
||||
} ?: emptyMap()
|
||||
|
||||
// To fill empty lists when MAL does not return them
|
||||
val baseMap =
|
||||
MalStatusType.values().filter { it.value >= 0 }.associate {
|
||||
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
||||
}
|
||||
|
||||
return SyncAPI.LibraryMetadata(
|
||||
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
|
||||
setOf(
|
||||
ListSorting.AlphabeticalA,
|
||||
ListSorting.AlphabeticalZ,
|
||||
ListSorting.UpdatedNew,
|
||||
ListSorting.UpdatedOld,
|
||||
ListSorting.RatingHigh,
|
||||
ListSorting.RatingLow,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getMalAnimeList(): Array<Data> {
|
||||
checkMalToken()
|
||||
var offset = 0
|
||||
|
@ -440,10 +527,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
return fullList.toTypedArray()
|
||||
}
|
||||
|
||||
fun convertToStatus(string: String): MalStatusType {
|
||||
return fromIntToAnimeStatus(malStatusAsString.indexOf(string))
|
||||
}
|
||||
|
||||
private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? {
|
||||
val user = "@me"
|
||||
val auth = getAuth() ?: return null
|
||||
|
@ -557,28 +640,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
return user
|
||||
}
|
||||
|
||||
enum class MalStatusType(var value: Int) {
|
||||
Watching(0),
|
||||
Completed(1),
|
||||
OnHold(2),
|
||||
Dropped(3),
|
||||
PlanToWatch(4),
|
||||
None(-1)
|
||||
}
|
||||
|
||||
private fun fromIntToAnimeStatus(inp: Int): MalStatusType {//= AniListStatusType.values().first { it.value == inp }
|
||||
return when (inp) {
|
||||
-1 -> MalStatusType.None
|
||||
0 -> MalStatusType.Watching
|
||||
1 -> MalStatusType.Completed
|
||||
2 -> MalStatusType.OnHold
|
||||
3 -> MalStatusType.Dropped
|
||||
4 -> MalStatusType.PlanToWatch
|
||||
5 -> MalStatusType.Watching
|
||||
else -> MalStatusType.None
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setScoreRequest(
|
||||
id: Int,
|
||||
status: MalStatusType? = null,
|
||||
|
|
|
@ -166,7 +166,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
|||
val fixedLang = fixLanguage(query.lang)
|
||||
|
||||
val imdbId = query.imdb ?: 0
|
||||
val queryText = query.query.replace(" ", "+")
|
||||
val queryText = query.query
|
||||
val epNum = query.epNumber ?: 0
|
||||
val seasonNum = query.seasonNumber ?: 0
|
||||
val yearNum = query.year ?: 0
|
||||
|
@ -177,7 +177,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
|||
val searchQueryUrl = when (imdbId > 0) {
|
||||
//Use imdb_id to search if its valid
|
||||
true -> "$host/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
|
||||
false -> "$host/subtitles?query=${URLEncoder.encode(queryText.lowercase(), StandardCharsets.UTF_8.toString())}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
|
||||
false -> "$host/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
|
||||
}
|
||||
|
||||
val req = app.get(
|
||||
|
@ -206,7 +206,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
|||
}
|
||||
//Use any valid name/title in hierarchy
|
||||
val name = filename ?: featureDetails?.movieName ?: featureDetails?.title
|
||||
?: featureDetails?.parentTitle ?: attr.release ?: ""
|
||||
?: featureDetails?.parentTitle ?: attr.release ?: query.query
|
||||
val lang = fixLanguageReverse(attr.language)?: ""
|
||||
val resEpNum = featureDetails?.episodeNumber ?: query.epNumber
|
||||
val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber
|
||||
|
|
|
@ -81,6 +81,8 @@ class APIRepository(val api: MainAPI) {
|
|||
}
|
||||
|
||||
api.load(fixedUrl)?.also { response ->
|
||||
// Remove all blank tags as early as possible
|
||||
response.tags = response.tags?.filter { it.isNotBlank() }
|
||||
val add = SavedLoadResponse(unixTime, response, lookingForHash)
|
||||
|
||||
synchronized(cache) {
|
||||
|
@ -122,7 +124,6 @@ class APIRepository(val api: MainAPI) {
|
|||
delay(delta)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
suspend fun getMainPage(page: Int, nameIndex: Int? = null): Resource<List<HomePageResponse?>> {
|
||||
return safeApiCall {
|
||||
api.lastHomepageRequest = unixTimeMS
|
||||
|
|
|
@ -7,7 +7,8 @@ import androidx.recyclerview.widget.GridLayoutManager
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlin.math.abs
|
||||
|
||||
class GrdLayoutManager(val context: Context, _spanCount: Int) : GridLayoutManager(context, _spanCount) {
|
||||
class GrdLayoutManager(val context: Context, _spanCount: Int) :
|
||||
GridLayoutManager(context, _spanCount) {
|
||||
override fun onFocusSearchFailed(
|
||||
focused: View,
|
||||
focusDirection: Int,
|
||||
|
@ -34,7 +35,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : GridLayoutManage
|
|||
val pos = maxOf(0, getPosition(focused!!) - 2)
|
||||
parent.scrollToPosition(pos)
|
||||
super.onRequestChildFocus(parent, state, child, focused)
|
||||
} catch (e: Exception){
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
package com.lagradost.cloudstream3.ui
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class HeaderViewDecoration(private val customView: View) : RecyclerView.ItemDecoration() {
|
||||
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
super.onDraw(c, parent, state)
|
||||
customView.layout(parent.left, 0, parent.right, customView.measuredHeight)
|
||||
for (i in 0 until parent.childCount) {
|
||||
val view = parent.getChildAt(i)
|
||||
if (parent.getChildAdapterPosition(view) == 0) {
|
||||
c.save()
|
||||
val height = customView.measuredHeight
|
||||
val top = view.top - height
|
||||
c.translate(0f, top.toFloat())
|
||||
customView.draw(c)
|
||||
c.restore()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
if (parent.getChildAdapterPosition(view) == 0) {
|
||||
customView.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.AT_MOST),
|
||||
View.MeasureSpec.makeMeasureSpec(parent.measuredHeight, View.MeasureSpec.AT_MOST)
|
||||
)
|
||||
outRect.set(0, customView.measuredHeight, 0, 0)
|
||||
} else {
|
||||
outRect.setEmpty()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import com.lagradost.cloudstream3.mvvm.logError
|
|||
import com.lagradost.cloudstream3.ui.player.DownloadFileGenerator
|
||||
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
|
||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
|
@ -49,7 +50,7 @@ object DownloadButtonSetup {
|
|||
)
|
||||
.setPositiveButton(R.string.delete, dialogClickListener)
|
||||
.setNegativeButton(R.string.cancel, dialogClickListener)
|
||||
.show()
|
||||
.show().setDefaultFocus()
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
// ye you somehow fucked up formatting did you?
|
||||
|
|
|
@ -40,6 +40,7 @@ import kotlinx.android.synthetic.main.stream_input.*
|
|||
import android.text.format.Formatter.formatShortFileSize
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import java.net.URI
|
||||
|
||||
|
||||
|
@ -178,7 +179,9 @@ class DownloadFragment : Fragment() {
|
|||
|
||||
download_list?.adapter = adapter
|
||||
download_list?.layoutManager = GridLayoutManager(context, 1)
|
||||
download_stream_button?.isGone = isTvSettings()
|
||||
|
||||
// Should be visible in emulator layout
|
||||
download_stream_button?.isGone = isTrueTvSettings()
|
||||
download_stream_button?.setOnClickListener {
|
||||
val dialog =
|
||||
Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom)
|
||||
|
|
|
@ -14,19 +14,13 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.content.ContextCompat.getDrawable
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.*
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.ChangeBounds
|
||||
import androidx.transition.TransitionManager
|
||||
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.button.MaterialButton
|
||||
|
@ -35,24 +29,19 @@ import com.google.android.material.chip.ChipGroup
|
|||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.apis
|
||||
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
||||
import com.lagradost.cloudstream3.APIHolder.getId
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
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.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
|
||||
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.ResultViewModel2.Companion.updateWatchStatus
|
||||
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
|
||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||
import com.lagradost.cloudstream3.ui.search.*
|
||||
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
|
@ -61,54 +50,34 @@ import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching
|
|||
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.setMaxViewPoolSize
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.ownHide
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.ownShow
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllBookmarkedData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllResumeStateIds
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.removeLastWatched
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
|
||||
import com.lagradost.cloudstream3.utils.Event
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImageBlur
|
||||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
||||
import com.lagradost.cloudstream3.widget.CenterZoomLayoutManager
|
||||
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_bookmarked_child_recyclerview
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_bookmarked_holder
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_change_api_loading
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_loaded
|
||||
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_plan_to_watch_btt
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_provider_meta_info
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_provider_name
|
||||
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.home_type_completed_btt
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_type_dropped_btt
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_type_on_hold_btt
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_type_watching_btt
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_watch_child_recyclerview
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_watch_holder
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_watch_parent_item_title
|
||||
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.*
|
||||
|
@ -140,26 +109,32 @@ class HomeFragment : Fragment() {
|
|||
|
||||
val errorProfilePic = errorProfilePics.random()
|
||||
|
||||
fun Activity.loadHomepageList(
|
||||
item: HomePageList,
|
||||
deleteCallback: (() -> Unit)? = null,
|
||||
) {
|
||||
loadHomepageList(
|
||||
expand = HomeViewModel.ExpandableHomepageList(item, 1, false),
|
||||
deleteCallback = deleteCallback,
|
||||
expandCallback = null
|
||||
)
|
||||
}
|
||||
//fun Activity.loadHomepageList(
|
||||
// item: HomePageList,
|
||||
// deleteCallback: (() -> Unit)? = null,
|
||||
//) {
|
||||
// loadHomepageList(
|
||||
// expand = HomeViewModel.ExpandableHomepageList(item, 1, false),
|
||||
// deleteCallback = deleteCallback,
|
||||
// expandCallback = null
|
||||
// )
|
||||
//}
|
||||
|
||||
// returns a BottomSheetDialog that will be hidden with OwnHidden upon hide, and must be saved to be able call ownShow in onCreateView
|
||||
fun Activity.loadHomepageList(
|
||||
expand: HomeViewModel.ExpandableHomepageList,
|
||||
deleteCallback: (() -> Unit)? = null,
|
||||
expandCallback: (suspend (String) -> HomeViewModel.ExpandableHomepageList?)? = null
|
||||
) {
|
||||
expandCallback: (suspend (String) -> HomeViewModel.ExpandableHomepageList?)? = null,
|
||||
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)!!
|
||||
|
||||
//title.findViewTreeLifecycleOwner().lifecycle.addObserver()
|
||||
|
||||
val item = expand.list
|
||||
title.text = item.name
|
||||
val recycle =
|
||||
|
@ -167,6 +142,23 @@ class HomeFragment : Fragment() {
|
|||
val titleHolder =
|
||||
bottomSheetDialogBuilder.findViewById<FrameLayout>(R.id.home_expanded_drag_down)!!
|
||||
|
||||
// main {
|
||||
//(bottomSheetDialogBuilder.ownerActivity as androidx.fragment.app.FragmentActivity?)?.supportFragmentManager?.fragments?.lastOrNull()?.viewLifecycleOwner?.apply {
|
||||
// println("GOT LIFE: lifecycle $this")
|
||||
// this.lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
// override fun onResume(owner: LifecycleOwner) {
|
||||
// super.onResume(owner)
|
||||
// println("onResume!!!!")
|
||||
// bottomSheetDialogBuilder?.ownShow()
|
||||
// }
|
||||
|
||||
// override fun onStop(owner: LifecycleOwner) {
|
||||
// super.onStop(owner)
|
||||
// bottomSheetDialogBuilder?.ownHide()
|
||||
// }
|
||||
// })
|
||||
//}
|
||||
// }
|
||||
val delete = bottomSheetDialogBuilder.home_expanded_delete
|
||||
delete.isGone = deleteCallback == null
|
||||
if (deleteCallback != null) {
|
||||
|
@ -192,7 +184,7 @@ class HomeFragment : Fragment() {
|
|||
)
|
||||
.setPositiveButton(R.string.delete, dialogClickListener)
|
||||
.setNegativeButton(R.string.cancel, dialogClickListener)
|
||||
.show()
|
||||
.show().setDefaultFocus()
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
// ye you somehow fucked up formatting did you?
|
||||
|
@ -211,7 +203,8 @@ class HomeFragment : Fragment() {
|
|||
recycle.adapter = SearchAdapter(item.list.toMutableList(), recycle) { callback ->
|
||||
handleSearchClickCallback(this, callback)
|
||||
if (callback.action == SEARCH_ACTION_LOAD || callback.action == SEARCH_ACTION_PLAY_FILE) {
|
||||
bottomSheetDialogBuilder.dismissSafe(this)
|
||||
bottomSheetDialogBuilder.ownHide() // we hide here because we want to resume it later
|
||||
//bottomSheetDialogBuilder.dismissSafe(this)
|
||||
}
|
||||
}.apply {
|
||||
hasNext = expand.hasNext
|
||||
|
@ -252,12 +245,14 @@ class HomeFragment : Fragment() {
|
|||
configEvent += spanListener
|
||||
|
||||
bottomSheetDialogBuilder.setOnDismissListener {
|
||||
dismissCallback.invoke()
|
||||
configEvent -= spanListener
|
||||
}
|
||||
|
||||
//(recycle.adapter as SearchAdapter).notifyDataSetChanged()
|
||||
|
||||
bottomSheetDialogBuilder.show()
|
||||
return bottomSheetDialogBuilder
|
||||
}
|
||||
|
||||
fun getPairList(
|
||||
|
@ -435,16 +430,17 @@ 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)
|
||||
}
|
||||
|
||||
private fun toggleMainVisibility(visible: Boolean) {
|
||||
home_main_poster_recyclerview?.isVisible = visible
|
||||
override fun onDestroyView() {
|
||||
bottomSheetDialog?.ownHide()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged") // we need to notify to change poster
|
||||
private fun fixGrid() {
|
||||
activity?.getSpanCount()?.let {
|
||||
currentSpan = it
|
||||
|
@ -467,18 +463,24 @@ class HomeFragment : Fragment() {
|
|||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.notifyDataSetChanged()
|
||||
//(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.notifyDataSetChanged()
|
||||
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()
|
||||
|
@ -510,20 +512,9 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
/*private fun handleBack(poppedFragment: Boolean) {
|
||||
if (poppedFragment) {
|
||||
reloadStored()
|
||||
}
|
||||
}*/
|
||||
|
||||
private fun focusCallback(card: SearchResponse) {
|
||||
home_focus_text?.text = card.name
|
||||
home_blur_poster?.setImageBlur(card.posterUrl, 50)
|
||||
}
|
||||
|
||||
private fun homeHandleSearch(callback: SearchClickCallback) {
|
||||
if (callback.action == SEARCH_ACTION_FOCUSED) {
|
||||
focusCallback(callback.card)
|
||||
//focusCallback(callback.card)
|
||||
} else {
|
||||
handleSearchClickCallback(activity, callback)
|
||||
}
|
||||
|
@ -532,12 +523,13 @@ class HomeFragment : Fragment() {
|
|||
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?.setOnClickListener(apiChangeClickListener)
|
||||
home_change_api_loading?.setOnClickListener(apiChangeClickListener)
|
||||
home_api_fab?.setOnClickListener(apiChangeClickListener)
|
||||
home_random?.setOnClickListener {
|
||||
|
@ -555,211 +547,19 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
|
||||
observe(homeViewModel.preview) { preview ->
|
||||
// Always reset the padding, otherwise the will move lower and lower
|
||||
// home_fix_padding?.setPadding(0, 0, 0, 0)
|
||||
home_fix_padding?.let { v ->
|
||||
val params = v.layoutParams
|
||||
params.height = 0
|
||||
v.layoutParams = params
|
||||
}
|
||||
|
||||
when (preview) {
|
||||
is Resource.Success -> {
|
||||
home_preview?.isVisible = true
|
||||
(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.apply {
|
||||
if (!setItems(preview.value.second, preview.value.first)) {
|
||||
home_preview_viewpager?.setCurrentItem(0, false)
|
||||
}
|
||||
// home_preview_viewpager?.setCurrentItem(1000, false)
|
||||
}
|
||||
|
||||
//.also {
|
||||
//home_preview_viewpager?.adapter =
|
||||
//}
|
||||
}
|
||||
else -> {
|
||||
(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.setItems(
|
||||
listOf(),
|
||||
false
|
||||
)
|
||||
home_preview?.isVisible = false
|
||||
context?.fixPaddingStatusbarView(home_fix_padding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val searchText =
|
||||
home_search?.findViewById<SearchView.SearchAutoComplete>(androidx.appcompat.R.id.search_src_text)
|
||||
searchText?.context?.getResourceColor(R.attr.white)?.let { color ->
|
||||
searchText.setTextColor(color)
|
||||
searchText.setHintTextColor(color)
|
||||
}
|
||||
|
||||
home_preview_viewpager?.apply {
|
||||
setPageTransformer(HomeScrollTransformer())
|
||||
val callback: OnPageChangeCallback = object : OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
|
||||
// home_search?.isIconified = true
|
||||
//home_search?.isVisible = true
|
||||
//home_search?.clearFocus()
|
||||
|
||||
(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.apply {
|
||||
if (position >= itemCount - 1 && hasMoreItems) {
|
||||
hasMoreItems = false // dont make two requests
|
||||
homeViewModel.loadMoreHomeScrollResponses()
|
||||
}
|
||||
|
||||
getItem(position)
|
||||
?.apply {
|
||||
home_preview_title_holder?.let { parent ->
|
||||
TransitionManager.beginDelayedTransition(parent, ChangeBounds())
|
||||
}
|
||||
|
||||
// home_preview_tags?.text = tags?.joinToString(" • ") ?: ""
|
||||
// home_preview_tags?.isGone = tags.isNullOrEmpty()
|
||||
// home_preview_image?.setImage(posterUrl, posterHeaders)
|
||||
// home_preview_title?.text = name
|
||||
|
||||
home_preview_play?.setOnClickListener {
|
||||
activity?.loadResult(url, apiName, START_ACTION_RESUME_LATEST)
|
||||
//activity.loadSearchResult(url, START_ACTION_RESUME_LATEST)
|
||||
}
|
||||
home_preview_info?.setOnClickListener {
|
||||
activity?.loadResult(url, apiName)
|
||||
//activity.loadSearchResult(random)
|
||||
}
|
||||
// very ugly code, but I dont care
|
||||
val watchType = DataStoreHelper.getResultWatchState(this.getId())
|
||||
home_preview_bookmark?.setText(watchType.stringRes)
|
||||
home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(
|
||||
null,
|
||||
getDrawable(home_preview_bookmark.context, watchType.iconRes),
|
||||
null,
|
||||
null
|
||||
)
|
||||
home_preview_bookmark?.setOnClickListener { fab ->
|
||||
activity?.showBottomDialog(
|
||||
WatchType.values()
|
||||
.map { fab.context.getString(it.stringRes) }
|
||||
.toList(),
|
||||
DataStoreHelper.getResultWatchState(this.getId()).ordinal,
|
||||
fab.context.getString(R.string.action_add_to_bookmarks),
|
||||
showApply = false,
|
||||
{}) {
|
||||
val newValue = WatchType.values()[it]
|
||||
home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(
|
||||
null,
|
||||
getDrawable(
|
||||
home_preview_bookmark.context,
|
||||
newValue.iconRes
|
||||
),
|
||||
null,
|
||||
null
|
||||
)
|
||||
home_preview_bookmark?.setText(newValue.stringRes)
|
||||
|
||||
updateWatchStatus(this, newValue)
|
||||
reloadStored()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
registerOnPageChangeCallback(callback)
|
||||
adapter = HomeScrollAdapter()
|
||||
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setPreviewData(
|
||||
preview
|
||||
)
|
||||
}
|
||||
|
||||
observe(homeViewModel.apiName) { apiName ->
|
||||
currentApiName = apiName
|
||||
// setKey(USER_SELECTED_HOMEPAGE_API, apiName)
|
||||
home_api_fab?.text = apiName
|
||||
home_provider_name?.text = apiName
|
||||
try {
|
||||
home_search?.queryHint = getString(R.string.search_hint_site).format(apiName)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
home_provider_meta_info?.isVisible = false
|
||||
|
||||
getApiFromNameNull(apiName)?.let { currentApi ->
|
||||
val typeChoices = listOf(
|
||||
Pair(R.string.movies, listOf(TvType.Movie)),
|
||||
Pair(R.string.tv_series, listOf(TvType.TvSeries)),
|
||||
Pair(R.string.documentaries, listOf(TvType.Documentary)),
|
||||
Pair(R.string.cartoons, listOf(TvType.Cartoon)),
|
||||
Pair(R.string.anime, listOf(TvType.Anime, TvType.OVA, TvType.AnimeMovie)),
|
||||
Pair(R.string.torrent, listOf(TvType.Torrent)),
|
||||
Pair(R.string.asian_drama, listOf(TvType.AsianDrama)),
|
||||
).filter { item -> currentApi.supportedTypes.any { type -> item.second.contains(type) } }
|
||||
home_provider_meta_info?.text =
|
||||
typeChoices.joinToString(separator = ", ") { getString(it.first) }
|
||||
home_provider_meta_info?.isVisible = true
|
||||
}
|
||||
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setApiName(
|
||||
apiName
|
||||
)
|
||||
}
|
||||
|
||||
home_main_poster_recyclerview?.adapter =
|
||||
HomeChildItemAdapter(
|
||||
mutableListOf(),
|
||||
R.layout.home_result_big_grid,
|
||||
nextFocusUp = home_main_poster_recyclerview?.nextFocusUpId,
|
||||
nextFocusDown = home_main_poster_recyclerview?.nextFocusDownId
|
||||
) { callback ->
|
||||
homeHandleSearch(callback)
|
||||
}
|
||||
home_main_poster_recyclerview?.setLinearListLayout()
|
||||
observe(homeViewModel.randomItems) { items ->
|
||||
if (items.isNullOrEmpty()) {
|
||||
toggleMainVisibility(false)
|
||||
} else {
|
||||
val tempAdapter = home_main_poster_recyclerview?.adapter as? HomeChildItemAdapter?
|
||||
// no need to reload if it has the same data
|
||||
if (tempAdapter != null && tempAdapter.cardList == items) {
|
||||
toggleMainVisibility(true)
|
||||
return@observe
|
||||
}
|
||||
|
||||
val randomSize = items.size
|
||||
tempAdapter?.updateList(items)
|
||||
if (!isTvSettings()) {
|
||||
home_main_poster_recyclerview?.post {
|
||||
(home_main_poster_recyclerview?.layoutManager as CenterZoomLayoutManager?)?.let { manager ->
|
||||
manager.updateSize(forceUpdate = true)
|
||||
if (randomSize > 2) {
|
||||
manager.scrollToPosition(randomSize / 2)
|
||||
manager.snap { dx ->
|
||||
home_main_poster_recyclerview?.post {
|
||||
// this is the best I can do, fuck android for not including instant scroll
|
||||
home_main_poster_recyclerview?.smoothScrollBy(dx, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
items.firstOrNull()?.let {
|
||||
focusCallback(it)
|
||||
}
|
||||
}
|
||||
toggleMainVisibility(true)
|
||||
}
|
||||
}
|
||||
|
||||
home_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
QuickSearchFragment.pushSearch(activity, query, currentApiName?.let { arrayOf(it) })
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String): Boolean {
|
||||
//searchViewModel.quickSearch(newText)
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
observe(homeViewModel.page) { data ->
|
||||
when (data) {
|
||||
is Resource.Success -> {
|
||||
|
@ -769,15 +569,15 @@ class HomeFragment : Fragment() {
|
|||
val mutableListOfResponse = mutableListOf<SearchResponse>()
|
||||
listHomepageItems.clear()
|
||||
|
||||
// println("ITEMCOUNT: ${d.values.size} ${home_master_recycler?.adapter?.itemCount}")
|
||||
(home_master_recycler?.adapter as? ParentItemAdapter?)?.updateList(
|
||||
(home_master_recycler?.adapter as? ParentItemAdapter)?.updateList(
|
||||
d.values.toMutableList(),
|
||||
home_master_recycler
|
||||
)
|
||||
|
||||
home_loading?.isVisible = false
|
||||
home_loading_error?.isVisible = false
|
||||
home_loaded?.isVisible = true
|
||||
home_master_recycler?.isVisible = true
|
||||
//home_loaded?.isVisible = true
|
||||
if (toggleRandomButton) {
|
||||
//Flatten list
|
||||
d.values.forEach { dlist ->
|
||||
|
@ -817,120 +617,42 @@ class HomeFragment : Fragment() {
|
|||
|
||||
home_loading?.isVisible = false
|
||||
home_loading_error?.isVisible = true
|
||||
home_loaded?.isVisible = false
|
||||
home_master_recycler?.isVisible = false
|
||||
//home_loaded?.isVisible = false
|
||||
}
|
||||
is Resource.Loading -> {
|
||||
(home_master_recycler?.adapter as? ParentItemAdapter?)?.updateList(listOf())
|
||||
(home_master_recycler?.adapter as? ParentItemAdapter)?.updateList(listOf())
|
||||
home_loading_shimmer?.startShimmer()
|
||||
home_loading?.isVisible = true
|
||||
home_loading_error?.isVisible = false
|
||||
home_loaded?.isVisible = false
|
||||
home_master_recycler?.isVisible = false
|
||||
//home_loaded?.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val toggleList = listOf(
|
||||
Pair(home_type_watching_btt, WatchType.WATCHING),
|
||||
Pair(home_type_completed_btt, WatchType.COMPLETED),
|
||||
Pair(home_type_dropped_btt, WatchType.DROPPED),
|
||||
Pair(home_type_on_hold_btt, WatchType.ONHOLD),
|
||||
Pair(home_plan_to_watch_btt, WatchType.PLANTOWATCH),
|
||||
)
|
||||
val currentSet = getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)
|
||||
?.map { WatchType.fromInternalId(it) }?.toSet() ?: emptySet()
|
||||
|
||||
for ((chip, watch) in toggleList) {
|
||||
chip.isChecked = currentSet.contains(watch)
|
||||
chip?.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
homeViewModel.loadStoredData(
|
||||
setOf(watch)
|
||||
// If we filter all buttons then two can be checked at the same time
|
||||
// Revert this if you want to go back to multi selection
|
||||
// toggleList.filter { it.first?.isChecked == true }.map { it.second }.toSet()
|
||||
)
|
||||
}
|
||||
// Else if all are unchecked -> Do not load data
|
||||
else if (toggleList.all { it.first?.isChecked != true }) {
|
||||
homeViewModel.loadStoredData(emptySet())
|
||||
}
|
||||
}
|
||||
/*chip?.setOnClickListener {
|
||||
|
||||
|
||||
homeViewModel.loadStoredData(EnumSet.of(watch))
|
||||
}
|
||||
|
||||
chip?.setOnLongClickListener { itemView ->
|
||||
val list = EnumSet.noneOf(WatchType::class.java)
|
||||
itemView.context.getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)
|
||||
?.map { WatchType.fromInternalId(it) }?.let {
|
||||
list.addAll(it)
|
||||
}
|
||||
|
||||
if (list.contains(watch)) {
|
||||
list.remove(watch)
|
||||
} else {
|
||||
list.add(watch)
|
||||
}
|
||||
homeViewModel.loadStoredData(list)
|
||||
return@setOnLongClickListener true
|
||||
}*/
|
||||
}
|
||||
|
||||
observe(homeViewModel.availableWatchStatusTypes) { availableWatchStatusTypes ->
|
||||
context?.setKey(
|
||||
HOME_BOOKMARK_VALUE_LIST,
|
||||
availableWatchStatusTypes.first.map { it.internalId }.toIntArray()
|
||||
)
|
||||
|
||||
for (item in toggleList) {
|
||||
val watch = item.second
|
||||
item.first?.apply {
|
||||
isVisible = availableWatchStatusTypes.second.contains(watch)
|
||||
isSelected = availableWatchStatusTypes.first.contains(watch)
|
||||
}
|
||||
}
|
||||
|
||||
/*home_bookmark_select?.setOnClickListener {
|
||||
it.popupMenuNoIcons(availableWatchStatusTypes.second.map { type ->
|
||||
Pair(
|
||||
type.internalId,
|
||||
type.stringRes
|
||||
)
|
||||
}) {
|
||||
homeViewModel.loadStoredData(it.context, WatchType.fromInternalId(this.itemId))
|
||||
}
|
||||
}
|
||||
home_bookmarked_parent_item_title?.text = getString(availableWatchStatusTypes.first.stringRes)*/
|
||||
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setAvailableWatchStatusTypes(
|
||||
availableWatchStatusTypes
|
||||
)
|
||||
}
|
||||
|
||||
observe(homeViewModel.bookmarks) { (isVis, bookmarks) ->
|
||||
home_bookmarked_holder.isVisible = isVis
|
||||
|
||||
(home_bookmarked_child_recyclerview?.adapter as? HomeChildItemAdapter?)?.updateList(
|
||||
bookmarks
|
||||
observe(homeViewModel.bookmarks) { data ->
|
||||
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setBookmarkData(
|
||||
data
|
||||
)
|
||||
|
||||
home_bookmarked_child_more_info?.setOnClickListener {
|
||||
activity?.loadHomepageList(
|
||||
HomePageList(
|
||||
getString(R.string.error_bookmarks_text), //home_bookmarked_parent_item_title?.text?.toString() ?: getString(R.string.error_bookmarks_text),
|
||||
bookmarks
|
||||
)
|
||||
) {
|
||||
deleteAllBookmarkedData()
|
||||
homeViewModel.loadStoredData(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observe(homeViewModel.resumeWatching) { resumeWatching ->
|
||||
home_watch_holder?.isVisible = resumeWatching.isNotEmpty()
|
||||
(home_watch_child_recyclerview?.adapter as? HomeChildItemAdapter?)?.updateList(
|
||||
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setResumeWatchingData(
|
||||
resumeWatching
|
||||
)
|
||||
|
||||
if (isTrueTvSettings()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ioSafe {
|
||||
|
@ -938,227 +660,79 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
home_watch_child_more_info?.setOnClickListener {
|
||||
activity?.loadHomepageList(
|
||||
HomePageList(
|
||||
home_watch_parent_item_title?.text?.toString()
|
||||
?: getString(R.string.continue_watching),
|
||||
resumeWatching
|
||||
)
|
||||
) {
|
||||
deleteAllResumeStateIds()
|
||||
homeViewModel.loadResumeWatching()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
home_bookmarked_child_recyclerview.adapter = HomeChildItemAdapter(
|
||||
ArrayList(),
|
||||
nextFocusUp = home_bookmarked_child_recyclerview?.nextFocusUpId,
|
||||
nextFocusDown = home_bookmarked_child_recyclerview?.nextFocusDownId
|
||||
) { callback ->
|
||||
if (callback.action == SEARCH_ACTION_SHOW_METADATA) {
|
||||
activity?.showOptionSelectStringRes(
|
||||
callback.view,
|
||||
callback.card.posterUrl,
|
||||
listOf(
|
||||
R.string.action_open_watching,
|
||||
R.string.action_remove_from_bookmarks,
|
||||
),
|
||||
listOf(
|
||||
R.string.action_open_play,
|
||||
R.string.action_open_watching,
|
||||
R.string.action_remove_from_bookmarks
|
||||
)
|
||||
) { (isTv, actionId) ->
|
||||
fun play() {
|
||||
activity.loadSearchResult(callback.card, START_ACTION_RESUME_LATEST)
|
||||
reloadStored()
|
||||
}
|
||||
|
||||
fun remove() {
|
||||
setResultWatchState(callback.card.id, WatchType.NONE.internalId)
|
||||
reloadStored()
|
||||
}
|
||||
|
||||
fun info() {
|
||||
handleSearchClickCallback(
|
||||
activity,
|
||||
SearchClickCallback(
|
||||
SEARCH_ACTION_LOAD,
|
||||
callback.view,
|
||||
-1,
|
||||
callback.card
|
||||
)
|
||||
)
|
||||
reloadStored()
|
||||
}
|
||||
|
||||
if (isTv) {
|
||||
when (actionId) {
|
||||
0 -> {
|
||||
play()
|
||||
}
|
||||
1 -> {
|
||||
info()
|
||||
}
|
||||
2 -> {
|
||||
remove()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when (actionId) {
|
||||
0 -> {
|
||||
info()
|
||||
}
|
||||
1 -> {
|
||||
remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
homeHandleSearch(callback)
|
||||
}
|
||||
}
|
||||
home_watch_child_recyclerview.setLinearListLayout()
|
||||
home_bookmarked_child_recyclerview.setLinearListLayout()
|
||||
|
||||
home_watch_child_recyclerview?.adapter = HomeChildItemAdapter(
|
||||
ArrayList(),
|
||||
nextFocusUp = home_watch_child_recyclerview?.nextFocusUpId,
|
||||
nextFocusDown = home_watch_child_recyclerview?.nextFocusDownId
|
||||
) { callback ->
|
||||
if (callback.action == SEARCH_ACTION_SHOW_METADATA) {
|
||||
activity?.showOptionSelectStringRes(
|
||||
callback.view,
|
||||
callback.card.posterUrl,
|
||||
listOf(
|
||||
R.string.action_open_watching,
|
||||
R.string.action_remove_watching
|
||||
),
|
||||
listOf(
|
||||
R.string.action_open_play,
|
||||
R.string.action_open_watching,
|
||||
R.string.action_remove_watching
|
||||
)
|
||||
) { (isTv, actionId) ->
|
||||
fun play() {
|
||||
activity.loadSearchResult(callback.card, START_ACTION_RESUME_LATEST)
|
||||
reloadStored()
|
||||
}
|
||||
|
||||
fun remove() {
|
||||
val card = callback.card
|
||||
if (card is DataStoreHelper.ResumeWatchingResult) {
|
||||
removeLastWatched(card.parentId)
|
||||
reloadStored()
|
||||
}
|
||||
}
|
||||
|
||||
fun info() {
|
||||
handleSearchClickCallback(
|
||||
activity,
|
||||
SearchClickCallback(
|
||||
SEARCH_ACTION_LOAD,
|
||||
callback.view,
|
||||
-1,
|
||||
callback.card
|
||||
)
|
||||
)
|
||||
reloadStored()
|
||||
}
|
||||
|
||||
if (isTv) {
|
||||
when (actionId) {
|
||||
0 -> {
|
||||
play()
|
||||
}
|
||||
1 -> {
|
||||
info()
|
||||
}
|
||||
2 -> {
|
||||
remove()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when (actionId) {
|
||||
0 -> {
|
||||
info()
|
||||
}
|
||||
1 -> {
|
||||
remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
homeHandleSearch(callback)
|
||||
}
|
||||
}
|
||||
|
||||
//context?.fixPaddingStatusbarView(home_statusbar)
|
||||
context?.fixPaddingStatusbar(home_padding)
|
||||
//context?.fixPaddingStatusbar(home_padding)
|
||||
context?.fixPaddingStatusbar(home_loading_statusbar)
|
||||
|
||||
home_master_recycler.adapter =
|
||||
ParentItemAdapter(mutableListOf(), { callback ->
|
||||
home_master_recycler?.adapter =
|
||||
HomeParentItemAdapterPreview(mutableListOf(), { callback ->
|
||||
homeHandleSearch(callback)
|
||||
}, { item ->
|
||||
activity?.loadHomepageList(item, expandCallback = {
|
||||
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) })
|
||||
}
|
||||
})
|
||||
home_master_recycler.setLinearListLayout()
|
||||
home_master_recycler?.setMaxViewPoolSize(0, Int.MAX_VALUE)
|
||||
home_master_recycler.layoutManager = object : LinearLayoutManager(context) {
|
||||
override fun supportsPredictiveItemAnimations(): Boolean {
|
||||
return false
|
||||
}
|
||||
} // GridLayoutManager(context, 1).also { it.supportsPredictiveItemAnimations() }
|
||||
|
||||
reloadStored()
|
||||
loadHomePage(false)
|
||||
|
||||
home_loaded.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { v, _, scrollY, _, oldScrollY ->
|
||||
val dy = scrollY - oldScrollY
|
||||
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()
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
// nice profile pic on homepage
|
||||
home_profile_picture_holder?.isVisible = false
|
||||
//home_profile_picture_holder?.isVisible = false
|
||||
// just in case
|
||||
if (isTvSettings()) {
|
||||
home_api_fab?.isVisible = false
|
||||
home_change_api?.isVisible = true
|
||||
if (isTrueTvSettings()) {
|
||||
home_change_api_loading?.isVisible = true
|
||||
home_change_api_loading?.isFocusable = true
|
||||
home_change_api_loading?.isFocusableInTouchMode = true
|
||||
home_change_api?.isFocusable = true
|
||||
home_change_api?.isFocusableInTouchMode = true
|
||||
}
|
||||
// home_bookmark_select?.isFocusable = true
|
||||
// home_bookmark_select?.isFocusableInTouchMode = true
|
||||
} else {
|
||||
home_api_fab?.isVisible = true
|
||||
home_change_api?.isVisible = false
|
||||
home_change_api_loading?.isVisible = false
|
||||
}
|
||||
|
||||
for (syncApi in OAuth2Apis) {
|
||||
//TODO READD THIS
|
||||
/*for (syncApi in OAuth2Apis) {
|
||||
val login = syncApi.loginInfo()
|
||||
val pic = login?.profilePicture
|
||||
if (home_profile_picture?.setImage(
|
||||
|
@ -1169,6 +743,6 @@ class HomeFragment : Fragment() {
|
|||
home_profile_picture_holder?.isVisible = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,34 +4,70 @@ 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.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,
|
||||
val view: View,
|
||||
val position: Int,
|
||||
val response: LoadResponse
|
||||
)
|
||||
|
||||
class ParentItemAdapter(
|
||||
open class ParentItemAdapter(
|
||||
private var items: MutableList<HomeViewModel.ExpandableHomepageList>,
|
||||
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, i: Int): ParentViewHolder {
|
||||
//println("onCreateViewHolder $i")
|
||||
val layout =
|
||||
if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return ParentViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(layout, parent, false),
|
||||
LayoutInflater.from(parent.context).inflate(
|
||||
if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent,
|
||||
parent,
|
||||
false
|
||||
),
|
||||
clickCallback,
|
||||
moreInfoClickCallback,
|
||||
expandCallback
|
||||
|
@ -39,8 +75,6 @@ class ParentItemAdapter(
|
|||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
//println("onBindViewHolder $position")
|
||||
|
||||
when (holder) {
|
||||
is ParentViewHolder -> {
|
||||
holder.bind(items[position])
|
||||
|
@ -81,47 +115,60 @@ class ParentItemAdapter(
|
|||
items.clear()
|
||||
items.addAll(new)
|
||||
|
||||
val mAdapter = this
|
||||
//val mAdapter = this
|
||||
val delta = if (this@ParentItemAdapter is HomeParentItemAdapterPreview) {
|
||||
headItems
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
diffResult.dispatchUpdatesTo(object : ListUpdateCallback {
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
mAdapter.notifyItemRangeInserted(position, count)
|
||||
//notifyItemRangeChanged(position + delta, count)
|
||||
notifyItemRangeInserted(position + delta, count)
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
mAdapter.notifyItemRangeRemoved(position, count)
|
||||
notifyItemRangeRemoved(position + delta, count)
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
mAdapter.notifyItemMoved(fromPosition, toPosition)
|
||||
notifyItemMoved(fromPosition + delta, toPosition + delta)
|
||||
}
|
||||
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
override fun onChanged(_position: Int, count: Int, payload: Any?) {
|
||||
|
||||
val position = _position + delta
|
||||
|
||||
// I know kinda messy, what this does is using the update or bind instead of onCreateViewHolder -> bind
|
||||
recyclerView?.apply {
|
||||
// this loops every viewHolder in the recycle view and checks the position to see if it is within the update range
|
||||
val missingUpdates = (position until (position + count)).toMutableSet()
|
||||
for (i in 0 until itemCount) {
|
||||
val viewHolder = getChildViewHolder(getChildAt(i))
|
||||
val absolutePosition = viewHolder.absoluteAdapterPosition
|
||||
val child = getChildAt(i) ?: continue
|
||||
val viewHolder = getChildViewHolder(child) ?: continue
|
||||
if (viewHolder !is ParentViewHolder) continue
|
||||
|
||||
val absolutePosition = viewHolder.bindingAdapterPosition
|
||||
if (absolutePosition >= position && absolutePosition < position + count) {
|
||||
val expand = items.getOrNull(absolutePosition) ?: continue
|
||||
if (viewHolder is ParentViewHolder) {
|
||||
missingUpdates -= absolutePosition
|
||||
if (viewHolder.title.text == expand.list.name) {
|
||||
viewHolder.update(expand)
|
||||
} else {
|
||||
viewHolder.bind(expand)
|
||||
}
|
||||
val expand = items.getOrNull(absolutePosition - delta) ?: continue
|
||||
missingUpdates -= absolutePosition
|
||||
//println("Updating ${viewHolder.title.text} ($absolutePosition $position) -> ${expand.list.name}")
|
||||
if (viewHolder.title.text == expand.list.name) {
|
||||
viewHolder.update(expand)
|
||||
} else {
|
||||
viewHolder.bind(expand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// just in case some item did not get updated
|
||||
for (i in missingUpdates) {
|
||||
mAdapter.notifyItemChanged(i, payload)
|
||||
notifyItemChanged(i, payload)
|
||||
}
|
||||
} ?: run { // in case we don't have a nice
|
||||
mAdapter.notifyItemRangeChanged(position, count, payload)
|
||||
} ?: run {
|
||||
// in case we don't have a nice
|
||||
notifyItemRangeChanged(position, count, payload)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -137,9 +184,8 @@ class ParentItemAdapter(
|
|||
private val expandCallback: ((String) -> Unit)? = null,
|
||||
) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
val title: TextView = itemView.home_parent_item_title
|
||||
val title: TextView = itemView.home_child_more_info
|
||||
val recyclerView: RecyclerView = itemView.home_child_recyclerview
|
||||
private val moreInfo: FrameLayout? = itemView.home_child_more_info
|
||||
|
||||
fun update(expand: HomeViewModel.ExpandableHomepageList) {
|
||||
val info = expand.list
|
||||
|
@ -201,9 +247,10 @@ class ParentItemAdapter(
|
|||
})
|
||||
|
||||
//(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
|
||||
|
||||
moreInfo?.setOnClickListener {
|
||||
moreInfoClickCallback.invoke(expand)
|
||||
if (!isTvSettings()) {
|
||||
title.setOnClickListener {
|
||||
moreInfoClickCallback.invoke(expand)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,658 @@
|
|||
package com.lagradost.cloudstream3.ui.home
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.ResultViewModel2
|
||||
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
|
||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import kotlinx.android.synthetic.main.activity_main.view.*
|
||||
import kotlinx.android.synthetic.main.fragment_home_head.view.*
|
||||
import kotlinx.android.synthetic.main.fragment_home_head.view.home_bookmarked_child_recyclerview
|
||||
import kotlinx.android.synthetic.main.fragment_home_head.view.home_watch_parent_item_title
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.*
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_bookmarked_holder
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_none_padding
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_plan_to_watch_btt
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_viewpager
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_completed_btt
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_dropped_btt
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_on_hold_btt
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_watching_btt
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_watch_child_recyclerview
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_watch_holder
|
||||
import kotlinx.android.synthetic.main.toast.view.*
|
||||
|
||||
class HomeParentItemAdapterPreview(
|
||||
items: MutableList<HomeViewModel.ExpandableHomepageList>,
|
||||
val clickCallback: (SearchClickCallback) -> Unit,
|
||||
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
|
||||
expandCallback: ((String) -> Unit)? = null,
|
||||
private val loadCallback: (LoadClickCallback) -> Unit,
|
||||
private val loadMoreCallback: (() -> Unit),
|
||||
private val changeHomePageCallback: ((View) -> Unit),
|
||||
private val reloadStored: (() -> Unit),
|
||||
private val loadStoredData: ((Set<WatchType>) -> Unit),
|
||||
private val searchQueryCallback: ((Pair<Boolean, String>) -> Unit)
|
||||
) : ParentItemAdapter(items, clickCallback, moreInfoClickCallback, expandCallback) {
|
||||
private var previewData: Resource<Pair<Boolean, List<LoadResponse>>> = Resource.Loading()
|
||||
private var resumeWatchingData: List<SearchResponse> = listOf()
|
||||
private var bookmarkData: Pair<Boolean, List<SearchResponse>> =
|
||||
false to listOf()
|
||||
private var apiName: String = "NONE"
|
||||
|
||||
val headItems = 1
|
||||
|
||||
private var availableWatchStatusTypes: Pair<Set<WatchType>, Set<WatchType>> =
|
||||
setOf<WatchType>() to setOf()
|
||||
|
||||
fun setAvailableWatchStatusTypes(data: Pair<Set<WatchType>, Set<WatchType>>) {
|
||||
availableWatchStatusTypes = data
|
||||
holder?.setAvailableWatchStatusTypes(data)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VIEW_TYPE_HEADER = 2
|
||||
private const val VIEW_TYPE_ITEM = 1
|
||||
}
|
||||
|
||||
fun setResumeWatchingData(resumeWatching: List<SearchResponse>) {
|
||||
resumeWatchingData = resumeWatching
|
||||
holder?.updateResume(resumeWatchingData)
|
||||
}
|
||||
|
||||
fun setPreviewData(preview: Resource<Pair<Boolean, List<LoadResponse>>>) {
|
||||
previewData = preview
|
||||
holder?.updatePreview(preview)
|
||||
}
|
||||
|
||||
fun setApiName(name: String) {
|
||||
apiName = name
|
||||
holder?.updateApiName(name)
|
||||
}
|
||||
|
||||
fun setBookmarkData(data: Pair<Boolean, List<SearchResponse>>) {
|
||||
bookmarkData = data
|
||||
holder?.updateBookmarks(data)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int) = when (position) {
|
||||
0 -> VIEW_TYPE_HEADER
|
||||
else -> VIEW_TYPE_ITEM
|
||||
}
|
||||
|
||||
var holder: HeaderViewHolder? = null
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is HeaderViewHolder -> {
|
||||
holder.updatePreview(previewData)
|
||||
holder.updateResume(resumeWatchingData)
|
||||
holder.updateBookmarks(bookmarkData)
|
||||
holder.setAvailableWatchStatusTypes(availableWatchStatusTypes)
|
||||
holder.updateApiName(apiName)
|
||||
}
|
||||
else -> super.onBindViewHolder(holder, position - 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
println("onCreateViewHolder $viewType")
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_HEADER -> HeaderViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(
|
||||
if (isTvSettings()) R.layout.fragment_home_head_tv else R.layout.fragment_home_head,
|
||||
parent,
|
||||
false
|
||||
),
|
||||
loadCallback,
|
||||
loadMoreCallback,
|
||||
changeHomePageCallback,
|
||||
clickCallback,
|
||||
reloadStored,
|
||||
loadStoredData,
|
||||
searchQueryCallback,
|
||||
moreInfoClickCallback
|
||||
).also {
|
||||
this.holder = it
|
||||
}
|
||||
VIEW_TYPE_ITEM -> super.onCreateViewHolder(parent, viewType)
|
||||
else -> error("Unhandled viewType=$viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return super.getItemCount() + headItems
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
if (position == 0) return previewData.hashCode().toLong()
|
||||
return super.getItemId(position - headItems)
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
|
||||
when (holder) {
|
||||
is HeaderViewHolder -> {
|
||||
holder.onViewDetachedFromWindow()
|
||||
}
|
||||
else -> super.onViewDetachedFromWindow(holder)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
|
||||
when (holder) {
|
||||
is HeaderViewHolder -> {
|
||||
holder.onViewAttachedToWindow()
|
||||
}
|
||||
else -> super.onViewAttachedToWindow(holder)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class HeaderViewHolder
|
||||
constructor(
|
||||
itemView: View,
|
||||
private val clickCallback: ((LoadClickCallback) -> Unit)?,
|
||||
private val loadMoreCallback: (() -> Unit),
|
||||
private val changeHomePageCallback: ((View) -> Unit),
|
||||
private val searchClickCallback: (SearchClickCallback) -> Unit,
|
||||
private val reloadStored: () -> Unit,
|
||||
private val loadStoredData: ((Set<WatchType>) -> Unit),
|
||||
private val searchQueryCallback: ((Pair<Boolean, String>) -> Unit),
|
||||
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
private var previewAdapter: HomeScrollAdapter? = null
|
||||
private val previewViewpager: ViewPager2? = itemView.home_preview_viewpager
|
||||
private val previewHeader: FrameLayout? = itemView.home_preview
|
||||
private val previewCallback: ViewPager2.OnPageChangeCallback =
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
// home_search?.isIconified = true
|
||||
//home_search?.isVisible = true
|
||||
//home_search?.clearFocus()
|
||||
|
||||
previewAdapter?.apply {
|
||||
if (position >= itemCount - 1 && hasMoreItems) {
|
||||
hasMoreItems = false // dont make two requests
|
||||
loadMoreCallback()
|
||||
//homeViewModel.loadMoreHomeScrollResponses()
|
||||
}
|
||||
}
|
||||
previewAdapter?.getItem(position)
|
||||
?.apply {
|
||||
//itemView.home_preview_title_holder?.let { parent ->
|
||||
// TransitionManager.beginDelayedTransition(
|
||||
// parent,
|
||||
// ChangeBounds()
|
||||
// )
|
||||
//}
|
||||
itemView.home_preview_description?.isGone =
|
||||
this.plot.isNullOrBlank()
|
||||
itemView.home_preview_description?.text =
|
||||
this.plot ?: ""
|
||||
itemView.home_preview_text?.text = this.name
|
||||
itemView.home_preview_tags?.apply {
|
||||
removeAllViews()
|
||||
tags?.forEach { tag ->
|
||||
val chip = Chip(context)
|
||||
val chipDrawable =
|
||||
ChipDrawable.createFromAttributes(
|
||||
context,
|
||||
null,
|
||||
0,
|
||||
R.style.ChipFilledSemiTransparent
|
||||
)
|
||||
chip.setChipDrawable(chipDrawable)
|
||||
chip.text = tag
|
||||
chip.isChecked = false
|
||||
chip.isCheckable = false
|
||||
chip.isFocusable = false
|
||||
chip.isClickable = false
|
||||
addView(chip)
|
||||
}
|
||||
}
|
||||
itemView.home_preview_tags?.isGone =
|
||||
tags.isNullOrEmpty()
|
||||
itemView.home_preview_image?.setImage(
|
||||
posterUrl,
|
||||
posterHeaders
|
||||
)
|
||||
// itemView.home_preview_title?.text = name
|
||||
|
||||
itemView.home_preview_play?.setOnClickListener { view ->
|
||||
clickCallback?.invoke(
|
||||
LoadClickCallback(
|
||||
START_ACTION_RESUME_LATEST,
|
||||
view,
|
||||
position,
|
||||
this
|
||||
)
|
||||
)
|
||||
}
|
||||
itemView.home_preview_info?.setOnClickListener { view ->
|
||||
clickCallback?.invoke(
|
||||
LoadClickCallback(0, view, position, this)
|
||||
)
|
||||
}
|
||||
|
||||
itemView.home_preview_play_btt?.setOnClickListener { view ->
|
||||
clickCallback?.invoke(
|
||||
LoadClickCallback(
|
||||
START_ACTION_RESUME_LATEST,
|
||||
view,
|
||||
position,
|
||||
this
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// This makes the hidden next buttons only available when on the info button
|
||||
// Otherwise you might be able to go to the next item without being at the info button
|
||||
itemView.home_preview_info_btt?.setOnFocusChangeListener { _, hasFocus ->
|
||||
itemView.home_preview_hidden_next_focus?.isFocusable = hasFocus
|
||||
}
|
||||
itemView.home_preview_play_btt?.setOnFocusChangeListener { _, hasFocus ->
|
||||
itemView.home_preview_hidden_prev_focus?.isFocusable = hasFocus
|
||||
}
|
||||
|
||||
|
||||
itemView.home_preview_info_btt?.setOnClickListener { view ->
|
||||
clickCallback?.invoke(
|
||||
LoadClickCallback(0, view, position, this)
|
||||
)
|
||||
}
|
||||
|
||||
itemView.home_preview_hidden_next_focus?.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
previewViewpager?.apply {
|
||||
setCurrentItem(currentItem + 1, true)
|
||||
}
|
||||
itemView.home_preview_info_btt?.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
itemView.home_preview_hidden_prev_focus?.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
previewViewpager?.apply {
|
||||
if (currentItem <= 0) {
|
||||
nav_rail_view?.menu?.getItem(0)?.actionView?.requestFocus()
|
||||
} else {
|
||||
setCurrentItem(currentItem - 1, true)
|
||||
itemView.home_preview_play_btt?.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// very ugly code, but I dont care
|
||||
val watchType =
|
||||
DataStoreHelper.getResultWatchState(this.getId())
|
||||
itemView.home_preview_bookmark?.setText(watchType.stringRes)
|
||||
itemView.home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(
|
||||
null,
|
||||
ContextCompat.getDrawable(
|
||||
itemView.home_preview_bookmark.context,
|
||||
watchType.iconRes
|
||||
),
|
||||
null,
|
||||
null
|
||||
)
|
||||
itemView.home_preview_bookmark?.setOnClickListener { fab ->
|
||||
fab.context.getActivity()?.showBottomDialog(
|
||||
WatchType.values()
|
||||
.map { fab.context.getString(it.stringRes) }
|
||||
.toList(),
|
||||
DataStoreHelper.getResultWatchState(this.getId()).ordinal,
|
||||
fab.context.getString(R.string.action_add_to_bookmarks),
|
||||
showApply = false,
|
||||
{}) {
|
||||
val newValue = WatchType.values()[it]
|
||||
itemView.home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(
|
||||
null,
|
||||
ContextCompat.getDrawable(
|
||||
itemView.home_preview_bookmark.context,
|
||||
newValue.iconRes
|
||||
),
|
||||
null,
|
||||
null
|
||||
)
|
||||
itemView.home_preview_bookmark?.setText(newValue.stringRes)
|
||||
|
||||
ResultViewModel2.updateWatchStatus(
|
||||
this,
|
||||
newValue
|
||||
)
|
||||
reloadStored()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var resumeAdapter: HomeChildItemAdapter? = null
|
||||
private var resumeHolder: View? = itemView.home_watch_holder
|
||||
private var resumeRecyclerView: RecyclerView? = itemView.home_watch_child_recyclerview
|
||||
|
||||
private var bookmarkHolder: View? = itemView.home_bookmarked_holder
|
||||
private var bookmarkAdapter: HomeChildItemAdapter? = null
|
||||
private var bookmarkRecyclerView: RecyclerView? =
|
||||
itemView.home_bookmarked_child_recyclerview
|
||||
|
||||
fun onViewDetachedFromWindow() {
|
||||
previewViewpager?.unregisterOnPageChangeCallback(previewCallback)
|
||||
}
|
||||
|
||||
fun onViewAttachedToWindow() {
|
||||
previewViewpager?.registerOnPageChangeCallback(previewCallback)
|
||||
}
|
||||
|
||||
private val toggleList = listOf(
|
||||
Pair(itemView.home_type_watching_btt, WatchType.WATCHING),
|
||||
Pair(itemView.home_type_completed_btt, WatchType.COMPLETED),
|
||||
Pair(itemView.home_type_dropped_btt, WatchType.DROPPED),
|
||||
Pair(itemView.home_type_on_hold_btt, WatchType.ONHOLD),
|
||||
Pair(itemView.home_plan_to_watch_btt, WatchType.PLANTOWATCH),
|
||||
)
|
||||
|
||||
init {
|
||||
itemView.home_preview_change_api?.setOnClickListener { view ->
|
||||
changeHomePageCallback(view)
|
||||
}
|
||||
itemView.home_preview_change_api2?.setOnClickListener { view ->
|
||||
changeHomePageCallback(view)
|
||||
}
|
||||
|
||||
previewViewpager?.apply {
|
||||
//if (!isTvSettings())
|
||||
setPageTransformer(HomeScrollTransformer())
|
||||
//else
|
||||
// setPageTransformer(null)
|
||||
|
||||
if (adapter == null)
|
||||
adapter = HomeScrollAdapter(
|
||||
if (isTvSettings()) R.layout.home_scroll_view_tv else R.layout.home_scroll_view,
|
||||
if (isTvSettings()) true else null
|
||||
)
|
||||
}
|
||||
previewAdapter = previewViewpager?.adapter as? HomeScrollAdapter?
|
||||
// previewViewpager?.registerOnPageChangeCallback(previewCallback)
|
||||
|
||||
if (resumeAdapter == null) {
|
||||
resumeRecyclerView?.adapter = HomeChildItemAdapter(
|
||||
ArrayList(),
|
||||
nextFocusUp = itemView.nextFocusUpId,
|
||||
nextFocusDown = itemView.nextFocusDownId
|
||||
) { callback ->
|
||||
if (callback.action != SEARCH_ACTION_SHOW_METADATA) {
|
||||
searchClickCallback(callback)
|
||||
return@HomeChildItemAdapter
|
||||
}
|
||||
callback.view.context?.getActivity()?.showOptionSelectStringRes(
|
||||
callback.view,
|
||||
callback.card.posterUrl,
|
||||
listOf(
|
||||
R.string.action_open_watching,
|
||||
R.string.action_remove_watching
|
||||
),
|
||||
listOf(
|
||||
R.string.action_open_play,
|
||||
R.string.action_open_watching,
|
||||
R.string.action_remove_watching
|
||||
)
|
||||
) { (isTv, actionId) ->
|
||||
when (actionId + if (isTv) 0 else 1) {
|
||||
// play
|
||||
0 -> {
|
||||
searchClickCallback.invoke(
|
||||
SearchClickCallback(
|
||||
START_ACTION_RESUME_LATEST,
|
||||
callback.view,
|
||||
-1,
|
||||
callback.card
|
||||
)
|
||||
)
|
||||
reloadStored()
|
||||
}
|
||||
//info
|
||||
1 -> {
|
||||
searchClickCallback(
|
||||
SearchClickCallback(
|
||||
SEARCH_ACTION_LOAD,
|
||||
callback.view,
|
||||
-1,
|
||||
callback.card
|
||||
)
|
||||
)
|
||||
|
||||
reloadStored()
|
||||
}
|
||||
// remove
|
||||
2 -> {
|
||||
val card = callback.card
|
||||
if (card is DataStoreHelper.ResumeWatchingResult) {
|
||||
DataStoreHelper.removeLastWatched(card.parentId)
|
||||
reloadStored()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
resumeAdapter = resumeRecyclerView?.adapter as? HomeChildItemAdapter
|
||||
if (bookmarkAdapter == null) {
|
||||
bookmarkRecyclerView?.adapter = HomeChildItemAdapter(
|
||||
ArrayList(),
|
||||
nextFocusUp = itemView.nextFocusUpId,
|
||||
nextFocusDown = itemView.nextFocusDownId
|
||||
) { callback ->
|
||||
if (callback.action != SEARCH_ACTION_SHOW_METADATA) {
|
||||
searchClickCallback(callback)
|
||||
return@HomeChildItemAdapter
|
||||
}
|
||||
callback.view.context?.getActivity()?.showOptionSelectStringRes(
|
||||
callback.view,
|
||||
callback.card.posterUrl,
|
||||
listOf(
|
||||
R.string.action_open_watching,
|
||||
R.string.action_remove_from_bookmarks,
|
||||
),
|
||||
listOf(
|
||||
R.string.action_open_play,
|
||||
R.string.action_open_watching,
|
||||
R.string.action_remove_from_bookmarks
|
||||
)
|
||||
) { (isTv, actionId) ->
|
||||
when (actionId + if (isTv) 0 else 1) { // play
|
||||
0 -> {
|
||||
searchClickCallback.invoke(
|
||||
SearchClickCallback(
|
||||
START_ACTION_RESUME_LATEST,
|
||||
callback.view,
|
||||
-1,
|
||||
callback.card
|
||||
)
|
||||
)
|
||||
reloadStored()
|
||||
}
|
||||
1 -> { // info
|
||||
searchClickCallback(
|
||||
SearchClickCallback(
|
||||
SEARCH_ACTION_LOAD,
|
||||
callback.view,
|
||||
-1,
|
||||
callback.card
|
||||
)
|
||||
)
|
||||
|
||||
reloadStored()
|
||||
}
|
||||
2 -> { // remove
|
||||
DataStoreHelper.setResultWatchState(
|
||||
callback.card.id,
|
||||
WatchType.NONE.internalId
|
||||
)
|
||||
reloadStored()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bookmarkAdapter = bookmarkRecyclerView?.adapter as? HomeChildItemAdapter
|
||||
|
||||
for ((chip, watch) in toggleList) {
|
||||
chip?.isChecked = false
|
||||
chip?.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
loadStoredData(
|
||||
setOf(watch)
|
||||
// If we filter all buttons then two can be checked at the same time
|
||||
// Revert this if you want to go back to multi selection
|
||||
// toggleList.filter { it.first?.isChecked == true }.map { it.second }.toSet()
|
||||
)
|
||||
}
|
||||
// Else if all are unchecked -> Do not load data
|
||||
else if (toggleList.all { it.first?.isChecked != true }) {
|
||||
loadStoredData(emptySet())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemView.home_search?.context?.fixPaddingStatusbar(itemView.home_search)
|
||||
|
||||
itemView.home_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
searchQueryCallback.invoke(false to query)
|
||||
//QuickSearchFragment.pushSearch(activity, query, currentApiName?.let { arrayOf(it) }
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String): Boolean {
|
||||
searchQueryCallback.invoke(true to newText)
|
||||
//searchViewModel.quickSearch(newText)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun updateApiName(name: String) {
|
||||
itemView.home_preview_change_api2?.text = name
|
||||
itemView.home_preview_change_api?.text = name
|
||||
}
|
||||
|
||||
fun updatePreview(preview: Resource<Pair<Boolean, List<LoadResponse>>>) {
|
||||
itemView.home_preview_change_api2?.isGone = preview is Resource.Success
|
||||
if (preview is Resource.Success) {
|
||||
itemView.home_none_padding?.apply {
|
||||
val params = layoutParams
|
||||
params.height = 0
|
||||
layoutParams = params
|
||||
}
|
||||
} else {
|
||||
itemView.home_none_padding?.context?.fixPaddingStatusbarView(itemView.home_none_padding)
|
||||
}
|
||||
when (preview) {
|
||||
is Resource.Success -> {
|
||||
if (true != previewAdapter?.setItems(
|
||||
preview.value.second,
|
||||
preview.value.first
|
||||
)
|
||||
) {
|
||||
// this might seam weird and useless, however this prevents a very weird andrid bug were the viewpager is not rendered properly
|
||||
// I have no idea why that happens, but this is my ducktape solution
|
||||
previewViewpager?.setCurrentItem(0, false)
|
||||
previewViewpager?.beginFakeDrag()
|
||||
previewViewpager?.fakeDragBy(1f)
|
||||
previewViewpager?.endFakeDrag()
|
||||
previewCallback.onPageSelected(0)
|
||||
previewHeader?.isVisible = true
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
previewAdapter?.setItems(listOf(), false)
|
||||
previewViewpager?.setCurrentItem(0, false)
|
||||
previewHeader?.isVisible = false
|
||||
}
|
||||
}
|
||||
// previewViewpager?.postDelayed({ previewViewpager?.scr(100, 0) }, 1000)
|
||||
//previewViewpager?.postInvalidate()
|
||||
}
|
||||
|
||||
fun updateResume(resumeWatching: List<SearchResponse>) {
|
||||
resumeHolder?.isVisible = resumeWatching.isNotEmpty()
|
||||
resumeAdapter?.updateList(resumeWatching)
|
||||
|
||||
if (!isTvSettings()) {
|
||||
itemView.home_watch_parent_item_title?.setOnClickListener {
|
||||
moreInfoClickCallback.invoke(
|
||||
HomeViewModel.ExpandableHomepageList(
|
||||
HomePageList(
|
||||
itemView.home_watch_parent_item_title?.text.toString(),
|
||||
resumeWatching,
|
||||
false
|
||||
), 1, false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateBookmarks(data: Pair<Boolean, List<SearchResponse>>) {
|
||||
bookmarkHolder?.isVisible = data.first
|
||||
bookmarkAdapter?.updateList(data.second)
|
||||
if (!isTvSettings()) {
|
||||
itemView.home_bookmark_parent_item_title?.setOnClickListener {
|
||||
val items = toggleList.mapNotNull { it.first }.filter { it.isChecked }
|
||||
if (items.isEmpty()) return@setOnClickListener // we don't want to show an empty dialog
|
||||
val textSum = items
|
||||
.mapNotNull { it.text }.joinToString()
|
||||
|
||||
moreInfoClickCallback.invoke(
|
||||
HomeViewModel.ExpandableHomepageList(
|
||||
HomePageList(
|
||||
textSum,
|
||||
data.second,
|
||||
false
|
||||
), 1, false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setAvailableWatchStatusTypes(availableWatchStatusTypes: Pair<Set<WatchType>, Set<WatchType>>) {
|
||||
for ((chip, watch) in toggleList) {
|
||||
chip?.apply {
|
||||
isVisible = availableWatchStatusTypes.second.contains(watch)
|
||||
isChecked = availableWatchStatusTypes.first.contains(watch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,16 +4,22 @@ 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 com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.*
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.*
|
||||
import kotlinx.android.synthetic.main.home_scroll_view.view.*
|
||||
|
||||
|
||||
class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
class HomeScrollAdapter(
|
||||
@LayoutRes val layout: Int = R.layout.home_scroll_view,
|
||||
private val forceHorizontalPosters: Boolean? = null
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
private var items: MutableList<LoadResponse> = mutableListOf()
|
||||
var hasMoreItems: Boolean = false
|
||||
|
||||
|
@ -40,7 +46,8 @@ class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return CardViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.home_scroll_view, parent, false),
|
||||
LayoutInflater.from(parent.context).inflate(layout, parent, false),
|
||||
forceHorizontalPosters
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -55,13 +62,15 @@ class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||
class CardViewHolder
|
||||
constructor(
|
||||
itemView: View,
|
||||
private val forceHorizontalPosters: Boolean? = null
|
||||
) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
fun bind(card: LoadResponse) {
|
||||
card.apply {
|
||||
val isHorizontal =
|
||||
itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
(forceHorizontalPosters == true) || ((forceHorizontalPosters != false) && itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||
|
||||
val posterUrl = if (isHorizontal) backgroundPosterUrl ?: posterUrl else posterUrl
|
||||
?: backgroundPosterUrl
|
||||
itemView.home_scroll_preview_tags?.text = tags?.joinToString(" • ") ?: ""
|
||||
|
|
|
@ -128,6 +128,7 @@ class HomeViewModel : ViewModel() {
|
|||
currentWatchTypes.remove(WatchType.NONE)
|
||||
|
||||
if (currentWatchTypes.size <= 0) {
|
||||
_availableWatchStatusTypes.postValue(setOf<WatchType>() to setOf())
|
||||
_bookmarks.postValue(Pair(false, ArrayList()))
|
||||
return@launchSafe
|
||||
}
|
||||
|
@ -264,80 +265,83 @@ class HomeViewModel : ViewModel() {
|
|||
_apiName.postValue(repo?.name)
|
||||
_randomItems.postValue(listOf())
|
||||
|
||||
if (repo?.hasMainPage == true) {
|
||||
_page.postValue(Resource.Loading())
|
||||
_preview.postValue(Resource.Loading())
|
||||
addJob?.cancel()
|
||||
|
||||
when (val data = repo?.getMainPage(1, null)) {
|
||||
is Resource.Success -> {
|
||||
try {
|
||||
expandable.clear()
|
||||
data.value.forEach { home ->
|
||||
home?.items?.forEach { list ->
|
||||
val filteredList =
|
||||
context?.filterHomePageListByFilmQuality(list) ?: list
|
||||
expandable[list.name] =
|
||||
ExpandableHomepageList(filteredList, 1, home.hasNext)
|
||||
}
|
||||
}
|
||||
|
||||
val items = data.value.mapNotNull { it?.items }.flatten()
|
||||
|
||||
|
||||
previewResponses.clear()
|
||||
previewResponsesAdded.clear()
|
||||
|
||||
//val home = data.value
|
||||
if (items.isNotEmpty()) {
|
||||
val currentList =
|
||||
items.shuffled().filter { it.list.isNotEmpty() }
|
||||
.flatMap { it.list }
|
||||
.distinctBy { it.url }
|
||||
.toList()
|
||||
|
||||
if (currentList.isNotEmpty()) {
|
||||
val randomItems =
|
||||
context?.filterSearchResultByFilmQuality(currentList.shuffled())
|
||||
?: currentList.shuffled()
|
||||
|
||||
updatePreviewResponses(
|
||||
previewResponses,
|
||||
previewResponsesAdded,
|
||||
randomItems,
|
||||
3
|
||||
)
|
||||
|
||||
_randomItems.postValue(randomItems)
|
||||
currentShuffledList = randomItems
|
||||
}
|
||||
}
|
||||
if (previewResponses.isEmpty()) {
|
||||
_preview.postValue(
|
||||
Resource.Failure(
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
"No homepage responses"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
_preview.postValue(Resource.Success((previewResponsesAdded.size < currentShuffledList.size) to previewResponses))
|
||||
}
|
||||
_page.postValue(Resource.Success(expandable))
|
||||
} catch (e: Exception) {
|
||||
_randomItems.postValue(emptyList())
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
is Resource.Failure -> {
|
||||
_page.postValue(data!!)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
} else {
|
||||
if (repo?.hasMainPage != true) {
|
||||
_page.postValue(Resource.Success(emptyMap()))
|
||||
_preview.postValue(Resource.Failure(false, null, null, "No homepage"))
|
||||
return@ioSafe
|
||||
}
|
||||
|
||||
|
||||
_page.postValue(Resource.Loading())
|
||||
_preview.postValue(Resource.Loading())
|
||||
addJob?.cancel()
|
||||
|
||||
when (val data = repo?.getMainPage(1, null)) {
|
||||
is Resource.Success -> {
|
||||
try {
|
||||
expandable.clear()
|
||||
data.value.forEach { home ->
|
||||
home?.items?.forEach { list ->
|
||||
val filteredList =
|
||||
context?.filterHomePageListByFilmQuality(list) ?: list
|
||||
expandable[list.name] =
|
||||
ExpandableHomepageList(filteredList, 1, home.hasNext)
|
||||
}
|
||||
}
|
||||
|
||||
val items = data.value.mapNotNull { it?.items }.flatten()
|
||||
|
||||
|
||||
previewResponses.clear()
|
||||
previewResponsesAdded.clear()
|
||||
|
||||
//val home = data.value
|
||||
if (items.isNotEmpty()) {
|
||||
val currentList =
|
||||
items.shuffled().filter { it.list.isNotEmpty() }
|
||||
.flatMap { it.list }
|
||||
.distinctBy { it.url }
|
||||
.toList()
|
||||
|
||||
if (currentList.isNotEmpty()) {
|
||||
val randomItems =
|
||||
context?.filterSearchResultByFilmQuality(currentList.shuffled())
|
||||
?: currentList.shuffled()
|
||||
|
||||
updatePreviewResponses(
|
||||
previewResponses,
|
||||
previewResponsesAdded,
|
||||
randomItems,
|
||||
3
|
||||
)
|
||||
|
||||
_randomItems.postValue(randomItems)
|
||||
currentShuffledList = randomItems
|
||||
}
|
||||
}
|
||||
if (previewResponses.isEmpty()) {
|
||||
_preview.postValue(
|
||||
Resource.Failure(
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
"No homepage responses"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
_preview.postValue(Resource.Success((previewResponsesAdded.size < currentShuffledList.size) to previewResponses))
|
||||
}
|
||||
_page.postValue(Resource.Success(expandable))
|
||||
} catch (e: Exception) {
|
||||
_randomItems.postValue(emptyList())
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
is Resource.Failure -> {
|
||||
_page.postValue(data!!)
|
||||
_preview.postValue(data!!)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,393 @@
|
|||
package com.lagradost.cloudstream3.ui.library
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
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
|
||||
import android.view.animation.AlphaAnimation
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.lagradost.cloudstream3.APIHolder
|
||||
import com.lagradost.cloudstream3.APIHolder.allProviders
|
||||
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.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.debugAssert
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
|
||||
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"
|
||||
|
||||
|
||||
enum class LibraryOpenerType(@StringRes val stringRes: Int) {
|
||||
Default(R.string.default_subtitles), // TODO FIX AFTER MERGE
|
||||
Provider(R.string.none),
|
||||
Browser(R.string.browser),
|
||||
Search(R.string.search),
|
||||
None(R.string.none),
|
||||
}
|
||||
|
||||
/** Used to store how the user wants to open said poster */
|
||||
data class LibraryOpener(
|
||||
val openType: LibraryOpenerType,
|
||||
val providerData: ProviderLibraryData?,
|
||||
)
|
||||
|
||||
data class ProviderLibraryData(
|
||||
val apiName: String
|
||||
)
|
||||
|
||||
class LibraryFragment : Fragment() {
|
||||
companion object {
|
||||
fun newInstance() = LibraryFragment()
|
||||
|
||||
/**
|
||||
* Store which page was last seen when exiting the fragment and returning
|
||||
**/
|
||||
const val VIEWPAGER_ITEM_KEY = "viewpager_item"
|
||||
}
|
||||
|
||||
private val libraryViewModel: LibraryViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_library, container, false)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
viewpager?.currentItem?.let { currentItem ->
|
||||
outState.putInt(VIEWPAGER_ITEM_KEY, currentItem)
|
||||
}
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
context?.fixPaddingStatusbar(search_status_bar_padding)
|
||||
|
||||
sort_fab?.setOnClickListener {
|
||||
val methods = libraryViewModel.sortingMethods.map {
|
||||
txt(it.stringRes).asString(view.context)
|
||||
}
|
||||
|
||||
activity?.showBottomDialog(methods,
|
||||
libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod),
|
||||
txt(R.string.sort_by).asString(view.context),
|
||||
false,
|
||||
{},
|
||||
{
|
||||
val method = libraryViewModel.sortingMethods[it]
|
||||
libraryViewModel.sort(method)
|
||||
})
|
||||
}
|
||||
|
||||
main_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
libraryViewModel.sort(ListSorting.Query, query)
|
||||
return true
|
||||
}
|
||||
|
||||
// This is required to prevent the first text change
|
||||
// When this is attached it'll immediately send a onQueryTextChange("")
|
||||
// Which we do not want
|
||||
var hasInitialized = false
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
if (!hasInitialized) {
|
||||
hasInitialized = true
|
||||
return true
|
||||
}
|
||||
|
||||
libraryViewModel.sort(ListSorting.Query, newText)
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
libraryViewModel.reloadPages(false)
|
||||
|
||||
list_selector?.setOnClickListener {
|
||||
val items = libraryViewModel.availableApiNames
|
||||
val currentItem = libraryViewModel.currentApiName.value
|
||||
|
||||
activity?.showBottomDialog(items,
|
||||
items.indexOf(currentItem),
|
||||
txt(R.string.select_library).asString(it.context),
|
||||
false,
|
||||
{}) { index ->
|
||||
val selectedItem = items.getOrNull(index) ?: return@showBottomDialog
|
||||
libraryViewModel.switchList(selectedItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shows a plugin selection dialogue and saves the response
|
||||
**/
|
||||
fun Activity.showPluginSelectionDialog(
|
||||
key: String,
|
||||
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 baseOptions = listOf(
|
||||
LibraryOpenerType.Default,
|
||||
LibraryOpenerType.None,
|
||||
LibraryOpenerType.Browser,
|
||||
LibraryOpenerType.Search
|
||||
)
|
||||
|
||||
val items = baseOptions.map { txt(it.stringRes).asString(this) } + availableProviders
|
||||
|
||||
val savedSelection = getKey<LibraryOpener>(LIBRARY_FOLDER, key)
|
||||
val selectedIndex =
|
||||
when {
|
||||
savedSelection == null -> 0
|
||||
// If provider
|
||||
savedSelection.openType == LibraryOpenerType.Provider
|
||||
&& savedSelection.providerData?.apiName != null -> {
|
||||
availableProviders.indexOf(savedSelection.providerData.apiName)
|
||||
.takeIf { it != -1 }
|
||||
?.plus(baseOptions.size) ?: 0
|
||||
}
|
||||
// Else base option
|
||||
else -> baseOptions.indexOf(savedSelection.openType)
|
||||
}
|
||||
|
||||
this.showBottomDialog(
|
||||
items,
|
||||
selectedIndex,
|
||||
txt(R.string.open_with).asString(this),
|
||||
false,
|
||||
{},
|
||||
) {
|
||||
val savedData = if (it < baseOptions.size) {
|
||||
LibraryOpener(
|
||||
baseOptions[it],
|
||||
null
|
||||
)
|
||||
} else {
|
||||
LibraryOpener(
|
||||
LibraryOpenerType.Provider,
|
||||
ProviderLibraryData(items[it])
|
||||
)
|
||||
}
|
||||
|
||||
setKey(
|
||||
LIBRARY_FOLDER,
|
||||
key,
|
||||
savedData,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
provider_selector?.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 ->
|
||||
// To prevent future accidents
|
||||
debugAssert({
|
||||
searchClickCallback.card !is SyncAPI.LibraryItem
|
||||
}, {
|
||||
"searchClickCallback ${searchClickCallback.card} is not a LibraryItem"
|
||||
})
|
||||
|
||||
val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId
|
||||
val syncName =
|
||||
libraryViewModel.currentSyncApi?.syncIdName ?: return@callback
|
||||
|
||||
when (searchClickCallback.action) {
|
||||
SEARCH_ACTION_SHOW_METADATA -> {
|
||||
activity?.showPluginSelectionDialog(
|
||||
syncId,
|
||||
syncName,
|
||||
searchClickCallback.card.apiName
|
||||
)
|
||||
}
|
||||
|
||||
SEARCH_ACTION_LOAD -> {
|
||||
// This basically first selects the individual opener and if that is default then
|
||||
// selects the whole list opener
|
||||
val savedListSelection =
|
||||
getKey<LibraryOpener>(LIBRARY_FOLDER, syncName.name)
|
||||
val savedSelection = getKey<LibraryOpener>(LIBRARY_FOLDER, syncId).takeIf {
|
||||
it?.openType != LibraryOpenerType.Default
|
||||
} ?: savedListSelection
|
||||
|
||||
when (savedSelection?.openType) {
|
||||
null, LibraryOpenerType.Default -> {
|
||||
// Prevents opening MAL/AniList as a provider
|
||||
if (APIHolder.getApiFromNameNull(searchClickCallback.card.apiName) != null) {
|
||||
activity?.loadSearchResult(
|
||||
searchClickCallback.card
|
||||
)
|
||||
} else {
|
||||
// Search when no provider can open
|
||||
QuickSearchFragment.pushSearch(
|
||||
activity,
|
||||
searchClickCallback.card.name
|
||||
)
|
||||
}
|
||||
}
|
||||
LibraryOpenerType.None -> {}
|
||||
LibraryOpenerType.Provider ->
|
||||
savedSelection.providerData?.apiName?.let { apiName ->
|
||||
activity?.loadResult(
|
||||
searchClickCallback.card.url,
|
||||
apiName,
|
||||
)
|
||||
}
|
||||
LibraryOpenerType.Browser ->
|
||||
openBrowser(searchClickCallback.card.url)
|
||||
LibraryOpenerType.Search -> {
|
||||
QuickSearchFragment.pushSearch(
|
||||
activity,
|
||||
searchClickCallback.card.name
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
val stopLoading = Runnable {
|
||||
gridview?.adapter = null
|
||||
library_loading_overlay?.isVisible = false
|
||||
library_loading_shimmer?.stopShimmer()
|
||||
}
|
||||
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
observe(libraryViewModel.pages) { resource ->
|
||||
when (resource) {
|
||||
is Resource.Success -> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
(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 ->
|
||||
viewpager?.setCurrentItem(currentPos, false)
|
||||
savedInstanceState.remove(VIEWPAGER_ITEM_KEY)
|
||||
}
|
||||
|
||||
// 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 :(
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
(viewpager.adapter as? ViewpagerAdapter)?.rebind()
|
||||
super.onConfigurationChanged(newConfig)
|
||||
}
|
||||
}
|
||||
|
||||
class MenuSearchView(context: Context) : SearchView(context) {
|
||||
override fun onActionViewCollapsed() {
|
||||
super.onActionViewCollapsed()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
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 kotlin.math.roundToInt
|
||||
|
||||
class LibraryScrollTransformer : ViewPager2.PageTransformer {
|
||||
override fun transformPage(page: View, position: Float) {
|
||||
val padding = (-position * page.width).roundToInt()
|
||||
page.page_recyclerview.setPadding(
|
||||
padding, 0,
|
||||
-padding, 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package com.lagradost.cloudstream3.ui.library
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.R
|
||||
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),
|
||||
RatingHigh(R.string.sort_rating_desc),
|
||||
RatingLow(R.string.sort_rating_asc),
|
||||
UpdatedNew(R.string.sort_updated_new),
|
||||
UpdatedOld(R.string.sort_updated_old),
|
||||
AlphabeticalA(R.string.sort_alphabetical_a),
|
||||
AlphabeticalZ(R.string.sort_alphabetical_z),
|
||||
}
|
||||
|
||||
const val LAST_SYNC_API_KEY = "last_sync_api"
|
||||
|
||||
class LibraryViewModel : ViewModel() {
|
||||
private val _pages: MutableLiveData<Resource<List<SyncAPI.Page>>> = MutableLiveData(null)
|
||||
val pages: LiveData<Resource<List<SyncAPI.Page>>> = _pages
|
||||
|
||||
private val _currentApiName: MutableLiveData<String> = MutableLiveData("")
|
||||
val currentApiName: LiveData<String> = _currentApiName
|
||||
|
||||
private val availableSyncApis
|
||||
get() = SyncApis.filter { it.hasAccount() }
|
||||
|
||||
var currentSyncApi = availableSyncApis.let { allApis ->
|
||||
val lastSelection = getKey<String>(LAST_SYNC_API_KEY)
|
||||
availableSyncApis.firstOrNull { it.name == lastSelection } ?: allApis.firstOrNull()
|
||||
}
|
||||
private set(value) {
|
||||
field = value
|
||||
setKey(LAST_SYNC_API_KEY, field?.name)
|
||||
}
|
||||
|
||||
val availableApiNames: List<String>
|
||||
get() = availableSyncApis.map { it.name }
|
||||
|
||||
var sortingMethods = emptyList<ListSorting>()
|
||||
private set
|
||||
|
||||
var currentSortingMethod: ListSorting? = sortingMethods.firstOrNull()
|
||||
private set
|
||||
|
||||
fun switchList(name: String) {
|
||||
currentSyncApi = availableSyncApis[availableApiNames.indexOf(name)]
|
||||
_currentApiName.postValue(currentSyncApi?.name)
|
||||
reloadPages(true)
|
||||
}
|
||||
|
||||
fun sort(method: ListSorting, query: String? = null) {
|
||||
val currentList = pages.value ?: return
|
||||
currentSortingMethod = method
|
||||
(currentList as? Resource.Success)?.value?.forEachIndexed { _, page ->
|
||||
page.sort(method, query)
|
||||
}
|
||||
_pages.postValue(currentList)
|
||||
}
|
||||
|
||||
fun reloadPages(forceReload: Boolean) {
|
||||
// Only skip loading if its not forced and pages is not empty
|
||||
if (!forceReload && (pages.value as? Resource.Success)?.value?.isNotEmpty() == true &&
|
||||
currentSyncApi?.requireLibraryRefresh != true
|
||||
) return
|
||||
|
||||
ioSafe {
|
||||
currentSyncApi?.let { repo ->
|
||||
_currentApiName.postValue(repo.name)
|
||||
_pages.postValue(Resource.Loading())
|
||||
val libraryResource = repo.getPersonalLibrary()
|
||||
if (libraryResource is Resource.Failure) {
|
||||
_pages.postValue(libraryResource)
|
||||
return@let
|
||||
}
|
||||
val library = (libraryResource as? Resource.Success)?.value ?: return@let
|
||||
|
||||
sortingMethods = library.supportedListSorting.toList()
|
||||
currentSortingMethod = null
|
||||
|
||||
repo.requireLibraryRefresh = false
|
||||
|
||||
val pages = library.allLibraryLists.map {
|
||||
SyncAPI.Page(
|
||||
it.name,
|
||||
it.items
|
||||
)
|
||||
}
|
||||
|
||||
_pages.postValue(Resource.Success(pages))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.lagradost.cloudstream3.ui.library
|
||||
|
||||
import android.content.Context
|
||||
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() {
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
|
||||
override fun getCount(): Int {
|
||||
return itemCount
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Any? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
return convertView ?: inflater.inflate(R.layout.loading_poster_dynamic, parent, false)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
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.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
|
||||
|
||||
|
||||
class PageAdapter(
|
||||
override val items: MutableList<SyncAPI.LibraryItem>,
|
||||
private val resView: AutofitRecyclerView,
|
||||
val clickCallback: (SearchClickCallback) -> Unit
|
||||
) :
|
||||
AppUtils.DiffAdapter<SyncAPI.LibraryItem>(items) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return LibraryItemViewHolder(
|
||||
LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.search_result_grid_expanded, parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is LibraryItemViewHolder -> {
|
||||
holder.bind(items[position], position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isDark(color: Int): Boolean {
|
||||
return ColorUtils.calculateLuminance(color) < 0.5
|
||||
}
|
||||
|
||||
fun getDifferentColor(color: Int, ratio: Float = 0.7f): Int {
|
||||
return if (isDark(color)) {
|
||||
ColorUtils.blendARGB(color, Color.WHITE, ratio)
|
||||
} else {
|
||||
ColorUtils.blendARGB(color, Color.BLACK, ratio)
|
||||
}
|
||||
}
|
||||
|
||||
inner class LibraryItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val cardView: ImageView = itemView.imageView
|
||||
|
||||
private val compactView = false//itemView.context.getGridIsCompact()
|
||||
private val coverHeight: Int =
|
||||
if (compactView) 80.toPx else (resView.itemWidth / 0.68).roundToInt()
|
||||
|
||||
fun bind(item: SyncAPI.LibraryItem, position: Int) {
|
||||
/** https://stackoverflow.com/questions/8817522/how-to-get-color-code-of-image-view */
|
||||
|
||||
SearchResultBuilder.bind(
|
||||
this@PageAdapter.clickCallback,
|
||||
item,
|
||||
position,
|
||||
itemView,
|
||||
colorCallback = { palette ->
|
||||
AcraApplication.context?.let { ctx ->
|
||||
val defColor = ContextCompat.getColor(ctx, R.color.ratingColorBg)
|
||||
var bg = palette.getDarkVibrantColor(defColor)
|
||||
if (bg == defColor) {
|
||||
bg = palette.getDarkMutedColor(defColor)
|
||||
}
|
||||
if (bg == defColor) {
|
||||
bg = palette.getVibrantColor(defColor)
|
||||
}
|
||||
|
||||
val fg =
|
||||
getDifferentColor(bg)//palette.getVibrantColor(ContextCompat.getColor(ctx,R.color.ratingColor))
|
||||
itemView.text_rating.apply {
|
||||
setTextColor(ColorStateList.valueOf(fg))
|
||||
}
|
||||
itemView.text_rating_holder?.backgroundTintList = ColorStateList.valueOf(bg)
|
||||
itemView.watchProgress?.apply {
|
||||
progressTintList = ColorStateList.valueOf(fg)
|
||||
progressBackgroundTintList = ColorStateList.valueOf(bg)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// See searchAdaptor for this, it basically fixes the height
|
||||
if (!compactView) {
|
||||
cardView.apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
coverHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val showProgress = item.episodesCompleted != null && item.episodesTotal != null
|
||||
itemView.watchProgress.isVisible = showProgress
|
||||
if (showProgress) {
|
||||
itemView.watchProgress.max = item.episodesTotal!!
|
||||
itemView.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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
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.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>,
|
||||
val scrollCallback: (isScrollingDown: Boolean) -> Unit,
|
||||
val clickCallback: (SearchClickCallback) -> Unit
|
||||
) : 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)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is PageViewHolder -> {
|
||||
holder.bind(pages[position], unbound.remove(position))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
**/
|
||||
fun rebind() {
|
||||
unbound.addAll(0..pages.size)
|
||||
this.notifyItemRangeChanged(0, pages.size)
|
||||
}
|
||||
|
||||
inner class PageViewHolder(private val itemViewTest: View) :
|
||||
RecyclerView.ViewHolder(itemViewTest) {
|
||||
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
|
||||
)
|
||||
}
|
||||
} 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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return pages.size
|
||||
}
|
||||
}
|
|
@ -690,6 +690,8 @@ class CS3IPlayer : IPlayer {
|
|||
maxVideoHeight
|
||||
)
|
||||
)
|
||||
// Allows any seeking to be +- 0.3s to allow for faster seeking
|
||||
.setSeekParameters(SeekParameters(300_000, 300_000))
|
||||
.setLoadControl(
|
||||
DefaultLoadControl.Builder()
|
||||
.setTargetBufferBytes(
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||
|
||||
class ExtractorLinkGenerator(
|
||||
private val links: List<ExtractorLink>,
|
||||
private val subtitles: List<SubtitleData>,
|
||||
) : IGenerator {
|
||||
override val hasCache = false
|
||||
|
||||
override fun getCurrentId(): Int? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getAll(): List<Any>? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun hasPrev(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getCurrent(offset: Int): Any? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun goto(index: Int) {}
|
||||
|
||||
override fun next() {}
|
||||
|
||||
override fun prev() {}
|
||||
|
||||
override suspend fun generateLinks(
|
||||
clearCache: Boolean,
|
||||
isCasting: Boolean,
|
||||
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
|
||||
subtitleCallback: (SubtitleData) -> Unit,
|
||||
offset: Int
|
||||
): Boolean {
|
||||
subtitles.forEach(subtitleCallback)
|
||||
links.forEach {
|
||||
callback.invoke(it to null)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -605,7 +605,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
player_top_holder?.isGone = isGone
|
||||
//player_episodes_button?.isVisible = !isGone && hasEpisodes
|
||||
player_video_title?.isGone = togglePlayerTitleGone
|
||||
player_video_title_rez?.isGone = isGone
|
||||
// player_video_title_rez?.isGone = isGone
|
||||
player_episode_filler?.isGone = isGone
|
||||
player_center_menu?.isGone = isGone
|
||||
player_lock?.isGone = !isShowing
|
||||
|
|
|
@ -11,7 +11,9 @@ import android.util.Log
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.*
|
||||
import android.widget.TextView.OnEditorActionListener
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.animation.addListener
|
||||
|
@ -26,19 +28,13 @@ 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.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.mvvm.*
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders
|
||||
import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioTrackLanguage
|
||||
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
|
||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||
import com.lagradost.cloudstream3.ui.result.SyncViewModel
|
||||
import com.lagradost.cloudstream3.ui.result.setText
|
||||
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
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
|
@ -61,6 +57,9 @@ 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
|
||||
|
||||
class GeneratorPlayer : FullScreenPlayer() {
|
||||
companion object {
|
||||
|
@ -330,17 +329,54 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
dialog.search_loading_bar.progressTintList = color
|
||||
dialog.search_loading_bar.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)
|
||||
}
|
||||
|
||||
dialog.year_btt?.setOnClickListener {
|
||||
val none = txt(R.string.none).asString(context)
|
||||
val currentYear = Calendar.getInstance().get(Calendar.YEAR)
|
||||
val earliestYear = 1900
|
||||
|
||||
val years = (currentYear downTo earliestYear).toList()
|
||||
val options = listOf(none) + years.map {
|
||||
it.toString()
|
||||
}
|
||||
|
||||
val selectedIndex = viewModel.currentSubtitleYear.value
|
||||
?.let {
|
||||
// + 1 since none also takes a space
|
||||
years.indexOf(it) + 1
|
||||
}
|
||||
?.takeIf { it >= 0 } ?: 0
|
||||
|
||||
activity?.showDialog(
|
||||
options,
|
||||
selectedIndex,
|
||||
txt(R.string.year).asString(context),
|
||||
true, {
|
||||
}, { index ->
|
||||
viewModel.setSubtitleYear(years.getOrNull(index - 1))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
dialog.subtitles_search.setOnQueryTextListener(object :
|
||||
androidx.appcompat.widget.SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
dialog.search_loading_bar?.show()
|
||||
ioSafe {
|
||||
val search =
|
||||
AbstractSubtitleEntities.SubtitleSearch(query = query ?: return@ioSafe,
|
||||
AbstractSubtitleEntities.SubtitleSearch(
|
||||
query = query ?: return@ioSafe,
|
||||
imdb = imdbId,
|
||||
epNumber = currentTempMeta.episode,
|
||||
seasonNumber = currentTempMeta.season,
|
||||
lang = currentLanguageTwoLetters.ifBlank { null })
|
||||
lang = currentLanguageTwoLetters.ifBlank { null },
|
||||
year = viewModel.currentSubtitleYear.value
|
||||
)
|
||||
val results = providers.amap {
|
||||
try {
|
||||
it.search(search)
|
||||
|
@ -348,7 +384,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
null
|
||||
}
|
||||
}.filterNotNull()
|
||||
val max = results.map { it.size }.maxOrNull() ?: return@ioSafe
|
||||
val max = results.maxOfOrNull { it.size } ?: return@ioSafe
|
||||
|
||||
// very ugly
|
||||
val items = ArrayList<AbstractSubtitleEntities.SubtitleEntity>()
|
||||
|
@ -414,6 +450,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
dialog.show()
|
||||
dialog.subtitles_search.setQuery(currentTempMeta.name, true)
|
||||
//TODO: Set year text from currently loaded movie on Player
|
||||
//dialog.subtitles_search_year?.setText(currentTempMeta.year)
|
||||
}
|
||||
|
||||
private fun openSubPicker() {
|
||||
|
@ -1111,13 +1149,15 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
val source = currentSelectedLink?.first?.name ?: currentSelectedLink?.second?.name ?: "NULL"
|
||||
|
||||
player_video_title_rez?.text = when (titleRez) {
|
||||
val title = when (titleRez) {
|
||||
0 -> ""
|
||||
1 -> extra
|
||||
2 -> source
|
||||
3 -> "$source - $extra"
|
||||
else -> ""
|
||||
}
|
||||
player_video_title_rez?.text = title
|
||||
player_video_title_rez?.isVisible = title.isNotBlank()
|
||||
}
|
||||
|
||||
override fun playerDimensionsLoaded(widthHeight: Pair<Int, Int>) {
|
||||
|
|
|
@ -35,6 +35,13 @@ class PlayerGeneratorViewModel : ViewModel() {
|
|||
private val _currentStamps = MutableLiveData<List<EpisodeSkip.SkipStamp>>(emptyList())
|
||||
val currentStamps: LiveData<List<EpisodeSkip.SkipStamp>> = _currentStamps
|
||||
|
||||
private val _currentSubtitleYear = MutableLiveData<Int?>(null)
|
||||
val currentSubtitleYear: LiveData<Int?> = _currentSubtitleYear
|
||||
|
||||
fun setSubtitleYear(year: Int?) {
|
||||
_currentSubtitleYear.postValue(year)
|
||||
}
|
||||
|
||||
fun getId(): Int? {
|
||||
return generator?.getCurrentId()
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import androidx.core.view.isVisible
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
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
|
||||
|
@ -30,6 +31,7 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter
|
|||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
||||
import com.lagradost.cloudstream3.ui.search.SearchViewModel
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.ownShow
|
||||
import com.lagradost.cloudstream3.utils.UIHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
||||
|
@ -71,6 +73,8 @@ class QuickSearchFragment : Fragment() {
|
|||
private var providers: Set<String>? = null
|
||||
private lateinit var searchViewModel: SearchViewModel
|
||||
|
||||
private var bottomSheetDialog: BottomSheetDialog? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -80,7 +84,7 @@ class QuickSearchFragment : Fragment() {
|
|||
WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||
)
|
||||
searchViewModel = ViewModelProvider(this)[SearchViewModel::class.java]
|
||||
|
||||
bottomSheetDialog?.ownShow()
|
||||
return inflater.inflate(R.layout.quick_search, container, false)
|
||||
}
|
||||
|
||||
|
@ -156,7 +160,9 @@ class QuickSearchFragment : Fragment() {
|
|||
// else -> SearchHelper.handleSearchClickCallback(activity, callback)
|
||||
//}
|
||||
}, { item ->
|
||||
activity?.loadHomepageList(item)
|
||||
bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = {
|
||||
bottomSheetDialog = null
|
||||
})
|
||||
})
|
||||
quick_search_master_recycler?.layoutManager = GridLayoutManager(context, 1)
|
||||
}
|
||||
|
@ -214,7 +220,7 @@ class QuickSearchFragment : Fragment() {
|
|||
when (it) {
|
||||
is Resource.Success -> {
|
||||
it.value.let { data ->
|
||||
(quick_search_autofit_results?.adapter as? SearchAdapter?)?.updateList(
|
||||
(quick_search_autofit_results?.adapter as? SearchAdapter)?.updateList(
|
||||
context?.filterSearchResultByFilmQuality(data) ?: data
|
||||
)
|
||||
}
|
||||
|
|
|
@ -218,10 +218,18 @@ class EpisodeAdapter(
|
|||
name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name
|
||||
episodeText.isSelected = true // is needed for text repeating
|
||||
|
||||
val displayPos = card.getDisplayPosition()
|
||||
episodeProgress?.max = (card.duration / 1000).toInt()
|
||||
episodeProgress?.progress = (displayPos / 1000).toInt()
|
||||
episodeProgress?.isVisible = displayPos > 0L
|
||||
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
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadCache
|
|||
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getVideoWatchState
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
|
@ -83,6 +84,8 @@ import kotlinx.android.synthetic.main.fragment_result.result_next_airing
|
|||
import kotlinx.android.synthetic.main.fragment_result.result_next_airing_time
|
||||
import kotlinx.android.synthetic.main.fragment_result.result_no_episodes
|
||||
import kotlinx.android.synthetic.main.fragment_result.result_play_movie
|
||||
import kotlinx.android.synthetic.main.fragment_result.result_poster
|
||||
import kotlinx.android.synthetic.main.fragment_result.result_poster_holder
|
||||
import kotlinx.android.synthetic.main.fragment_result.result_reload_connection_open_in_browser
|
||||
import kotlinx.android.synthetic.main.fragment_result.result_reload_connectionerror
|
||||
import kotlinx.android.synthetic.main.fragment_result.result_resume_parent
|
||||
|
@ -104,6 +107,15 @@ import kotlinx.coroutines.runBlocking
|
|||
const val START_ACTION_RESUME_LATEST = 1
|
||||
const val START_ACTION_LOAD_EP = 2
|
||||
|
||||
/**
|
||||
* Future proofed way to mark episodes as watched
|
||||
**/
|
||||
enum class VideoWatchState {
|
||||
/** Default value when no key is set */
|
||||
None,
|
||||
Watched
|
||||
}
|
||||
|
||||
data class ResultEpisode(
|
||||
val headerName: String,
|
||||
val name: String?,
|
||||
|
@ -122,6 +134,10 @@ data class ResultEpisode(
|
|||
val isFiller: Boolean?,
|
||||
val tvType: TvType,
|
||||
val parentId: Int,
|
||||
/**
|
||||
* Conveys if the episode itself is marked as watched
|
||||
**/
|
||||
val videoWatchState: VideoWatchState
|
||||
)
|
||||
|
||||
fun ResultEpisode.getRealPosition(): Long {
|
||||
|
@ -158,6 +174,7 @@ fun buildResultEpisode(
|
|||
parentId: Int,
|
||||
): ResultEpisode {
|
||||
val posDur = getViewPos(id)
|
||||
val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None
|
||||
return ResultEpisode(
|
||||
headerName,
|
||||
name,
|
||||
|
@ -176,6 +193,7 @@ fun buildResultEpisode(
|
|||
isFiller,
|
||||
tvType,
|
||||
parentId,
|
||||
videoWatchState
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -259,7 +277,7 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
private var downloadButton: EasyDownloadButton? = null
|
||||
override fun onDestroyView() {
|
||||
updateUIListener = null
|
||||
(result_episodes?.adapter as EpisodeAdapter?)?.killAdapter()
|
||||
(result_episodes?.adapter as? EpisodeAdapter)?.killAdapter()
|
||||
downloadButton?.dispose()
|
||||
|
||||
super.onDestroyView()
|
||||
|
@ -440,7 +458,7 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
temporary_no_focus?.requestFocus()
|
||||
}
|
||||
|
||||
(result_episodes?.adapter as? EpisodeAdapter?)?.updateList(episodes.value)
|
||||
(result_episodes?.adapter as? EpisodeAdapter)?.updateList(episodes.value)
|
||||
|
||||
if (isTv && hasEpisodes) main {
|
||||
delay(500)
|
||||
|
@ -557,6 +575,19 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
)
|
||||
|
||||
|
||||
observe(viewModel.episodeSynopsis) { description ->
|
||||
view.context?.let { ctx ->
|
||||
val builder: AlertDialog.Builder =
|
||||
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
|
||||
builder.setMessage(description.html())
|
||||
.setTitle(R.string.synopsis)
|
||||
.setOnDismissListener {
|
||||
viewModel.releaseEpisodeSynopsis()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
observe(viewModel.watchStatus) { watchType ->
|
||||
result_bookmark_button?.text = getString(watchType.stringRes)
|
||||
result_bookmark_fab?.text = getString(watchType.stringRes)
|
||||
|
@ -656,7 +687,7 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
val newList = list.filter { it.isSynced && it.hasAccount }
|
||||
|
||||
result_mini_sync?.isVisible = newList.isNotEmpty()
|
||||
(result_mini_sync?.adapter as? ImageAdapter?)?.updateList(newList.mapNotNull { it.icon })
|
||||
(result_mini_sync?.adapter as? ImageAdapter)?.updateList(newList.mapNotNull { it.icon })
|
||||
}
|
||||
|
||||
var currentSyncProgress = 0
|
||||
|
@ -819,6 +850,7 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
}
|
||||
|
||||
observe(viewModel.page) { data ->
|
||||
if(data == null) return@observe
|
||||
when (data) {
|
||||
is Resource.Success -> {
|
||||
val d = data.value
|
||||
|
@ -868,7 +900,7 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
|
||||
|
||||
result_cast_items?.isVisible = d.actors != null
|
||||
(result_cast_items?.adapter as ActorAdaptor?)?.apply {
|
||||
(result_cast_items?.adapter as? ActorAdaptor)?.apply {
|
||||
updateList(d.actors ?: emptyList())
|
||||
}
|
||||
|
||||
|
@ -919,8 +951,6 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
}
|
||||
|
||||
|
||||
result_tag?.removeAllViews()
|
||||
|
||||
d.comingSoon.let { soon ->
|
||||
result_coming_soon?.isVisible = soon
|
||||
result_data_holder?.isGone = soon
|
||||
|
@ -929,6 +959,7 @@ open class ResultFragment : ResultTrailerPlayer() {
|
|||
val tags = d.tags
|
||||
result_tag_holder?.isVisible = tags.isNotEmpty()
|
||||
result_tag?.apply {
|
||||
removeAllViews()
|
||||
tags.forEach { tag ->
|
||||
val chip = Chip(context)
|
||||
val chipDrawable = ChipDrawable.createFromAttributes(
|
||||
|
|
|
@ -485,7 +485,7 @@ class ResultFragmentPhone : ResultFragment() {
|
|||
|
||||
result_recommendations?.post {
|
||||
rec?.let { list ->
|
||||
(result_recommendations?.adapter as SearchAdapter?)?.updateList(list.filter { it.apiName == matchAgainst })
|
||||
(result_recommendations?.adapter as? SearchAdapter)?.updateList(list.filter { it.apiName == matchAgainst })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,30 +3,28 @@ package com.lagradost.cloudstream3.ui.result
|
|||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
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.R
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.mvvm.ResourceSome
|
||||
import com.lagradost.cloudstream3.mvvm.Some
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator
|
||||
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
||||
import com.lagradost.cloudstream3.ui.search.SearchAdapter
|
||||
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.setMaxViewPoolSize
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||
import kotlinx.android.synthetic.main.fragment_home.*
|
||||
import kotlinx.android.synthetic.main.fragment_result.*
|
||||
import kotlinx.android.synthetic.main.fragment_result_tv.*
|
||||
import kotlinx.android.synthetic.main.fragment_result_tv.result_episodes
|
||||
import kotlinx.android.synthetic.main.fragment_result_tv.result_episodes_text
|
||||
import kotlinx.android.synthetic.main.fragment_result_tv.result_play_movie
|
||||
import kotlinx.android.synthetic.main.fragment_result_tv.result_root
|
||||
|
||||
class ResultFragmentTv : ResultFragment() {
|
||||
override val resultLayout = R.layout.fragment_result_tv
|
||||
|
@ -85,13 +83,31 @@ class ResultFragmentTv : ResultFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun setTrailers(trailers: List<ExtractorLink>?) {
|
||||
context?.updateHasTrailers()
|
||||
if (!LoadResponse.isTrailersEnabled) return
|
||||
|
||||
result_play_trailer?.isGone = trailers.isNullOrEmpty()
|
||||
result_play_trailer?.setOnClickListener {
|
||||
if (trailers.isNullOrEmpty()) return@setOnClickListener
|
||||
activity.navigate(
|
||||
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
|
||||
ExtractorLinkGenerator(
|
||||
trailers,
|
||||
emptyList()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
|
||||
currentRecommendations = rec ?: emptyList()
|
||||
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 }
|
||||
(result_recommendations?.adapter as? SearchAdapter)?.updateList(rec?.filter { it.apiName == matchAgainst }
|
||||
?: emptyList())
|
||||
|
||||
rec?.map { it.apiName }?.distinct()?.let { apiNames ->
|
||||
|
@ -110,7 +126,7 @@ class ResultFragmentTv : ResultFragment() {
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
result_episodes?.layoutManager =
|
||||
//LinearListLayout(result_episodes ?: return, result_episodes?.context).apply {
|
||||
//LinearListLayout(result_episodes ?: return, result_episodes?.context).apply {
|
||||
LinearListLayout(result_episodes?.context).apply {
|
||||
setHorizontal()
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ 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.getId
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTime
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
|
@ -55,7 +56,6 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.lang.Math.abs
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
|
@ -314,6 +314,11 @@ data class ExtractedTrailerData(
|
|||
class ResultViewModel2 : ViewModel() {
|
||||
private var currentResponse: LoadResponse? = null
|
||||
|
||||
fun clear() {
|
||||
currentResponse = null
|
||||
_page.postValue(null)
|
||||
}
|
||||
|
||||
data class EpisodeIndexer(
|
||||
val dubStatus: DubStatus,
|
||||
val season: Int,
|
||||
|
@ -340,9 +345,9 @@ class ResultViewModel2 : ViewModel() {
|
|||
//private val currentHeaderName get() = currentResponse?.name
|
||||
|
||||
|
||||
private val _page: MutableLiveData<Resource<ResultData>> =
|
||||
MutableLiveData(Resource.Loading())
|
||||
val page: LiveData<Resource<ResultData>> = _page
|
||||
private val _page: MutableLiveData<Resource<ResultData>?> =
|
||||
MutableLiveData(null)
|
||||
val page: LiveData<Resource<ResultData>?> = _page
|
||||
|
||||
private val _episodes: MutableLiveData<ResourceSome<List<ResultEpisode>>> =
|
||||
MutableLiveData(ResourceSome.Loading())
|
||||
|
@ -398,7 +403,6 @@ 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
|
||||
|
||||
|
@ -406,6 +410,9 @@ class ResultViewModel2 : ViewModel() {
|
|||
MutableLiveData(Some.None)
|
||||
val resumeWatching: LiveData<Some<ResumeWatchingStatus>> = _resumeWatching
|
||||
|
||||
private val _episodeSynopsis: MutableLiveData<String?> = MutableLiveData(null)
|
||||
val episodeSynopsis: LiveData<String?> = _episodeSynopsis
|
||||
|
||||
companion object {
|
||||
const val TAG = "RVM2"
|
||||
private const val EPISODE_RANGE_SIZE = 20
|
||||
|
@ -418,7 +425,6 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
fun updateWatchStatus(currentResponse: LoadResponse, status: WatchType) {
|
||||
val currentId = currentResponse.getId()
|
||||
val resultPage = currentResponse
|
||||
|
||||
DataStoreHelper.setResultWatchState(currentId, status.internalId)
|
||||
val current = DataStoreHelper.getBookmarkedData(currentId)
|
||||
|
@ -429,12 +435,12 @@ class ResultViewModel2 : ViewModel() {
|
|||
currentId,
|
||||
current?.bookmarkedTime ?: currentTime,
|
||||
currentTime,
|
||||
resultPage.name,
|
||||
resultPage.url,
|
||||
resultPage.apiName,
|
||||
resultPage.type,
|
||||
resultPage.posterUrl,
|
||||
resultPage.year
|
||||
currentResponse.name,
|
||||
currentResponse.url,
|
||||
currentResponse.apiName,
|
||||
currentResponse.type,
|
||||
currentResponse.posterUrl,
|
||||
currentResponse.year
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -1113,6 +1119,10 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
)
|
||||
|
||||
fun releaseEpisodeSynopsis() {
|
||||
_episodeSynopsis.postValue(null)
|
||||
}
|
||||
|
||||
private suspend fun handleEpisodeClickEvent(activity: Activity?, click: EpisodeClickEvent) {
|
||||
when (click.action) {
|
||||
ACTION_SHOW_OPTIONS -> {
|
||||
|
@ -1146,10 +1156,20 @@ class ResultViewModel2 : ViewModel() {
|
|||
txt(R.string.episode_action_download_mirror) to ACTION_DOWNLOAD_MIRROR,
|
||||
txt(R.string.episode_action_download_subtitle) to ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR,
|
||||
txt(R.string.episode_action_reload_links) to ACTION_RELOAD_EPISODE,
|
||||
// txt(R.string.action_mark_as_watched) to ACTION_MARK_AS_WATCHED,
|
||||
)
|
||||
)
|
||||
|
||||
// Do not add mark as watched on movies
|
||||
if (!listOf(TvType.Movie, TvType.AnimeMovie).contains(click.data.tvType)) {
|
||||
val isWatched =
|
||||
DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched
|
||||
|
||||
val watchedText = if (isWatched) R.string.action_remove_from_watched
|
||||
else R.string.action_mark_as_watched
|
||||
|
||||
options.add(txt(watchedText) to ACTION_MARK_AS_WATCHED)
|
||||
}
|
||||
|
||||
postPopup(
|
||||
txt(
|
||||
activity?.getNameFull(
|
||||
|
@ -1182,6 +1202,10 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
}
|
||||
}
|
||||
ACTION_SHOW_DESCRIPTION -> {
|
||||
_episodeSynopsis.postValue(click.data.description)
|
||||
}
|
||||
|
||||
/* not implemented, not used
|
||||
ACTION_DOWNLOAD_EPISODE_SUBTITLE -> {
|
||||
loadLinks(click.data, isVisible = false, isCasting = false) { links ->
|
||||
|
@ -1378,8 +1402,17 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
}
|
||||
ACTION_MARK_AS_WATCHED -> {
|
||||
// TODO FIX
|
||||
// DataStoreHelper.setViewPos(click.data.id, 1, 1)
|
||||
val isWatched =
|
||||
DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched
|
||||
|
||||
if (isWatched) {
|
||||
DataStoreHelper.setVideoWatchState(click.data.id, VideoWatchState.None)
|
||||
} else {
|
||||
DataStoreHelper.setVideoWatchState(click.data.id, VideoWatchState.Watched)
|
||||
}
|
||||
|
||||
// Kinda dirty to reload all episodes :(
|
||||
reloadEpisodes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1411,12 +1444,18 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
val realRecommendations = ArrayList<SearchResponse>()
|
||||
// TODO: fix
|
||||
//val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name)
|
||||
// meta.recommendations?.forEach { rec ->
|
||||
// apiNames.forEach { name ->
|
||||
// realRecommendations.add(rec.copy(apiName = name))
|
||||
// }
|
||||
// }
|
||||
val apiNames = 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))
|
||||
}
|
||||
}
|
||||
|
||||
recommendations = recommendations?.union(realRecommendations)?.toList()
|
||||
?: realRecommendations
|
||||
|
@ -1529,7 +1568,13 @@ class ResultViewModel2 : ViewModel() {
|
|||
val end = minOf(list.size, start + length)
|
||||
list.subList(start, end).map {
|
||||
val posDur = getViewPos(it.id)
|
||||
it.copy(position = posDur?.position ?: 0, duration = posDur?.duration ?: 0)
|
||||
val watchState =
|
||||
DataStoreHelper.getVideoWatchState(it.id) ?: VideoWatchState.None
|
||||
it.copy(
|
||||
position = posDur?.position ?: 0,
|
||||
duration = posDur?.duration ?: 0,
|
||||
videoWatchState = watchState
|
||||
)
|
||||
}
|
||||
}
|
||||
?: emptyList()
|
||||
|
@ -1592,10 +1637,11 @@ class ResultViewModel2 : ViewModel() {
|
|||
if (ranges?.contains(range) != true) {
|
||||
// if the current ranges does not include the range then select the range with the closest matching start episode
|
||||
// this usually happends when dub has less episodes then sub -> the range does not exist
|
||||
ranges?.minByOrNull { abs(it.startEpisode - range.startEpisode) }?.let { r ->
|
||||
postEpisodeRange(indexer, r)
|
||||
return
|
||||
}
|
||||
ranges?.minByOrNull { kotlin.math.abs(it.startEpisode - range.startEpisode) }
|
||||
?.let { r ->
|
||||
postEpisodeRange(indexer, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val isMovie = currentResponse?.isMovie() == true
|
||||
|
@ -1926,7 +1972,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
// this takes the indexer most preferable by the user given the current sorting
|
||||
val min = ranges.keys.minByOrNull { index ->
|
||||
kotlin.math.abs(
|
||||
index.season - (preferStartSeason ?: 0)
|
||||
index.season - (preferStartSeason ?: 1)
|
||||
) + if (index.dubStatus == preferDubStatus) 0 else 100000
|
||||
}
|
||||
|
||||
|
@ -2076,6 +2122,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
showFillers: Boolean,
|
||||
dubStatus: DubStatus,
|
||||
autostart: AutoResume?,
|
||||
loadTrailers: Boolean = true,
|
||||
) =
|
||||
viewModelScope.launchSafe {
|
||||
_page.postValue(Resource.Loading(url))
|
||||
|
@ -2103,7 +2150,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
val validUrlResource = safeApiCall {
|
||||
SyncRedirector.redirect(
|
||||
url,
|
||||
api.mainUrl
|
||||
api
|
||||
)
|
||||
}
|
||||
// TODO: fix
|
||||
|
@ -2139,7 +2186,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
preferDubStatus = getDub(mainId) ?: preferDubStatus
|
||||
preferStartEpisode = getResultEpisode(mainId)
|
||||
preferStartSeason = getResultSeason(mainId)
|
||||
preferStartSeason = getResultSeason(mainId) ?: 1
|
||||
|
||||
setKey(
|
||||
DOWNLOAD_HEADER_CACHE,
|
||||
|
@ -2154,8 +2201,8 @@ class ResultViewModel2 : ViewModel() {
|
|||
System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
|
||||
loadTrailers(data.value)
|
||||
if (loadTrailers)
|
||||
loadTrailers(data.value)
|
||||
postSuccessful(
|
||||
data.value,
|
||||
updateEpisodes = true,
|
||||
|
|
|
@ -10,27 +10,37 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
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 */
|
||||
const val SEARCH_ACTION_LOAD = 0
|
||||
|
||||
/** Long press */
|
||||
const val SEARCH_ACTION_SHOW_METADATA = 1
|
||||
const val SEARCH_ACTION_PLAY_FILE = 2
|
||||
const val SEARCH_ACTION_FOCUSED = 4
|
||||
|
||||
class SearchClickCallback(val action: Int, val view: View, val position : Int, val card: SearchResponse)
|
||||
class SearchClickCallback(
|
||||
val action: Int,
|
||||
val view: View,
|
||||
val position: Int,
|
||||
val card: SearchResponse
|
||||
)
|
||||
|
||||
class SearchAdapter(
|
||||
private val cardList: MutableList<SearchResponse>,
|
||||
private val resView: AutofitRecyclerView,
|
||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
var hasNext : Boolean = false
|
||||
var hasNext: Boolean = false
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val layout = if(parent.context.IsBottomLayout()) R.layout.search_result_grid_expanded else R.layout.search_result_grid
|
||||
val layout =
|
||||
if (parent.context.IsBottomLayout()) R.layout.search_result_grid_expanded else R.layout.search_result_grid
|
||||
return CardViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(layout, parent, false),
|
||||
clickCallback,
|
||||
|
@ -71,7 +81,8 @@ class SearchAdapter(
|
|||
val cardView: ImageView = itemView.imageView
|
||||
|
||||
private val compactView = false//itemView.context.getGridIsCompact()
|
||||
private val coverHeight: Int = if (compactView) 80.toPx else (resView.itemWidth / 0.68).roundToInt()
|
||||
private val coverHeight: Int =
|
||||
if (compactView) 80.toPx else (resView.itemWidth / 0.68).roundToInt()
|
||||
|
||||
fun bind(card: SearchResponse, position: Int) {
|
||||
if (!compactView) {
|
||||
|
@ -88,7 +99,10 @@ class SearchAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
class SearchResponseDiffCallback(private val oldList: List<SearchResponse>, private val newList: List<SearchResponse>) :
|
||||
class SearchResponseDiffCallback(
|
||||
private val oldList: List<SearchResponse>,
|
||||
private val newList: List<SearchResponse>
|
||||
) :
|
||||
DiffUtil.Callback() {
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
oldList[oldItemPosition].name == newList[newItemPosition].name
|
||||
|
|
|
@ -44,6 +44,10 @@ import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageLis
|
|||
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.updateChips
|
||||
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.ownHide
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.ownShow
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
|
@ -84,6 +88,7 @@ class SearchFragment : Fragment() {
|
|||
}
|
||||
|
||||
private val searchViewModel: SearchViewModel by activityViewModels()
|
||||
private var bottomSheetDialog: BottomSheetDialog? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
@ -93,7 +98,12 @@ class SearchFragment : Fragment() {
|
|||
activity?.window?.setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||
)
|
||||
return inflater.inflate(R.layout.fragment_search, container, false)
|
||||
bottomSheetDialog?.ownShow()
|
||||
return inflater.inflate(
|
||||
if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search,
|
||||
container,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
private fun fixGrid() {
|
||||
|
@ -112,6 +122,7 @@ class SearchFragment : Fragment() {
|
|||
|
||||
override fun onDestroyView() {
|
||||
hideKeyboard()
|
||||
bottomSheetDialog?.ownHide()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
|
@ -387,7 +398,7 @@ class SearchFragment : Fragment() {
|
|||
)
|
||||
.setPositiveButton(R.string.sort_clear, dialogClickListener)
|
||||
.setNegativeButton(R.string.cancel, dialogClickListener)
|
||||
.show()
|
||||
.show().setDefaultFocus()
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
// ye you somehow fucked up formatting did you?
|
||||
|
@ -409,7 +420,7 @@ class SearchFragment : Fragment() {
|
|||
is Resource.Success -> {
|
||||
it.value.let { data ->
|
||||
if (data.isNotEmpty()) {
|
||||
(search_autofit_results?.adapter as SearchAdapter?)?.updateList(data)
|
||||
(search_autofit_results?.adapter as? SearchAdapter)?.updateList(data)
|
||||
}
|
||||
}
|
||||
searchExitIcon.alpha = 1f
|
||||
|
@ -468,7 +479,9 @@ class SearchFragment : Fragment() {
|
|||
ParentItemAdapter(mutableListOf(), { callback ->
|
||||
SearchHelper.handleSearchClickCallback(activity, callback)
|
||||
}, { item ->
|
||||
activity?.loadHomepageList(item)
|
||||
bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = {
|
||||
bottomSheetDialog = null
|
||||
})
|
||||
})
|
||||
|
||||
val historyAdapter = SearchHistoryAdaptor(mutableListOf()) { click ->
|
||||
|
|
|
@ -3,11 +3,13 @@ package com.lagradost.cloudstream3.ui.search
|
|||
import android.app.Activity
|
||||
import android.widget.Toast
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_PLAY_FILE
|
||||
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
||||
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
|
||||
import com.lagradost.cloudstream3.ui.result.START_ACTION_LOAD_EP
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||
|
@ -54,7 +56,15 @@ object SearchHelper {
|
|||
}
|
||||
}
|
||||
SEARCH_ACTION_SHOW_METADATA -> {
|
||||
showToast(activity, callback.card.name, Toast.LENGTH_SHORT)
|
||||
if(!isTvSettings()) { // we only want this on phone as UI is not done yet on tv
|
||||
(activity as? MainActivity?)?.apply {
|
||||
loadPopup(callback.card)
|
||||
} ?: kotlin.run {
|
||||
showToast(activity, callback.card.name, Toast.LENGTH_SHORT)
|
||||
}
|
||||
} else {
|
||||
showToast(activity, callback.card.name, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
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
|
||||
import android.widget.TextView
|
||||
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.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
|
@ -41,6 +43,7 @@ object SearchResultBuilder {
|
|||
nextFocusBehavior: Boolean? = null,
|
||||
nextFocusUp: Int? = null,
|
||||
nextFocusDown: Int? = null,
|
||||
colorCallback : ((Palette) -> Unit)? = null
|
||||
) {
|
||||
val cardView: ImageView = itemView.imageView
|
||||
val cardText: TextView? = itemView.imageText
|
||||
|
@ -100,7 +103,7 @@ object SearchResultBuilder {
|
|||
cardText?.isVisible = showTitle
|
||||
cardView.isVisible = true
|
||||
|
||||
if (!cardView.setImage(card.posterUrl, card.posterHeaders)) {
|
||||
if (!cardView.setImage(card.posterUrl, card.posterHeaders, colorCallback = colorCallback)) {
|
||||
cardView.setImageResource(R.drawable.default_cover)
|
||||
}
|
||||
|
||||
|
|
|
@ -55,33 +55,48 @@ fun getCurrentLocale(context: Context): String {
|
|||
// Emoji Character Encoding Data --> C/C++/Java Src
|
||||
// https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes leave blank for auto
|
||||
val appLanguages = arrayListOf(
|
||||
Triple("", "Spanish", "es"),
|
||||
Triple("", "English", "en"),
|
||||
Triple("", "Viet Nam", "vi"),
|
||||
Triple("", "Dutch", "nl"),
|
||||
Triple("", "French", "fr"),
|
||||
Triple("", "Greek", "el"),
|
||||
Triple("", "Swedish", "sv"),
|
||||
Triple("", "Tagalog", "tl"),
|
||||
Triple("", "Polish", "pl"),
|
||||
Triple("", "Hindi", "hi"),
|
||||
Triple("", "Malayalam", "ml"),
|
||||
Triple("", "Norsk", "no"),
|
||||
Triple("", "German", "de"),
|
||||
/* begin language list */
|
||||
Triple("", "Arabic", "ar"),
|
||||
Triple("", "Turkish", "tr"),
|
||||
Triple("", "Macedonian", "mk"),
|
||||
Triple("\uD83C\uDDF5\uD83C\uDDF9", "Portuguese", "pt"),
|
||||
Triple("\uD83C\uDDE7\uD83C\uDDF7", "Brazilian Portuguese", "bp"),
|
||||
Triple("", "Romanian", "ro"),
|
||||
Triple("", "Italian", "it"),
|
||||
Triple("", "Chinese Simplified", "zh"),
|
||||
Triple("\uD83C\uDDF9\uD83C\uDDFC", "Chinese Traditional", "zh_TW"),
|
||||
Triple("\uD83C\uDDEE\uD83C\uDDE9", "Indonesian", "in"),
|
||||
Triple("", "Czech", "cs"),
|
||||
Triple("", "Croatian", "hr"),
|
||||
Triple("", "Bulgarian", "bg"),
|
||||
Triple("", "Bengali", "bn"),
|
||||
Triple("\uD83C\uDDE7\uD83C\uDDF7", "Brazilian Portuguese", "bp"),
|
||||
Triple("", "Czech", "cs"),
|
||||
Triple("", "German", "de"),
|
||||
Triple("", "Greek", "el"),
|
||||
Triple("", "English", "en"),
|
||||
Triple("", "Esperanto", "eo"),
|
||||
Triple("", "Spanish", "es"),
|
||||
Triple("", "Farsi", "fa"),
|
||||
Triple("", "French", "fr"),
|
||||
Triple("", "Hindi", "hi"),
|
||||
Triple("", "Croatian", "hr"),
|
||||
Triple("", "Hungarian", "hu"),
|
||||
Triple("\uD83C\uDDEE\uD83C\uDDE9", "Indonesian", "in"),
|
||||
Triple("", "Italian", "it"),
|
||||
Triple("\uD83C\uDDEE\uD83C\uDDF1", "Hebrew", "iw"),
|
||||
Triple("", "Kannada", "kn"),
|
||||
Triple("", "Macedonian", "mk"),
|
||||
Triple("", "Malayalam", "ml"),
|
||||
Triple("", "Moldavian", "mo"),
|
||||
Triple("", "Dutch", "nl"),
|
||||
Triple("", "Norwegian Nynorsk", "nn"),
|
||||
Triple("", "Norwegian", "no"),
|
||||
Triple("", "Polish", "pl"),
|
||||
Triple("\uD83C\uDDF5\uD83C\uDDF9", "Portuguese", "pt"),
|
||||
Triple("", "Romanian", "ro"),
|
||||
Triple("", "Russian", "ru"),
|
||||
Triple("", "Slovak", "sk"),
|
||||
Triple("", "Somali", "so"),
|
||||
Triple("", "Swedish", "sv"),
|
||||
Triple("", "Tamil", "ta"),
|
||||
Triple("", "Tagalog", "tl"),
|
||||
Triple("", "Turkish", "tr"),
|
||||
Triple("", "Ukrainian", "uk"),
|
||||
Triple("", "Urdu", "ur"),
|
||||
Triple("", "Viet Nam", "vi"),
|
||||
Triple("", "Chinese Simplified", "zh"),
|
||||
Triple("\uD83C\uDDF9\uD83C\uDDFC", "Chinese Traditional", "zh-rTW"),
|
||||
/* end language list */
|
||||
).sortedBy { it.second } //ye, we go alphabetical, so ppl don't put their lang on top
|
||||
|
||||
class SettingsGeneral : PreferenceFragmentCompat() {
|
||||
|
|
|
@ -10,7 +10,7 @@ import android.widget.Toast
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.lagradost.cloudstream3.CommonActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
|
@ -21,6 +21,7 @@ import com.lagradost.cloudstream3.utils.BackupUtils.backup
|
|||
import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
|
||||
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
|
||||
|
@ -124,6 +125,32 @@ class SettingsUpdates : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
getPref(R.string.apk_installer_key)?.setOnPreferenceClickListener {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context)
|
||||
|
||||
val prefNames = resources.getStringArray(R.array.apk_installer_pref)
|
||||
val prefValues = resources.getIntArray(R.array.apk_installer_values)
|
||||
|
||||
val currentInstaller =
|
||||
settingsManager.getInt(getString(R.string.apk_installer_key), 0)
|
||||
|
||||
activity?.showBottomDialog(
|
||||
prefNames.toList(),
|
||||
prefValues.indexOf(currentInstaller),
|
||||
getString(R.string.apk_installer_settings),
|
||||
true,
|
||||
{}) {
|
||||
try {
|
||||
settingsManager.edit()
|
||||
.putInt(getString(R.string.apk_installer_key), prefValues[it])
|
||||
.apply()
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
getPref(R.string.manual_check_update_key)?.setOnPreferenceClickListener {
|
||||
ioSafe {
|
||||
if (activity?.runAutoUpdate(false) == false) {
|
||||
|
|
|
@ -26,6 +26,7 @@ import com.lagradost.cloudstream3.ui.result.setText
|
|||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.downloadAllPluginsDialog
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
|
@ -107,7 +108,7 @@ class ExtensionsFragment : Fragment() {
|
|||
)
|
||||
.setPositiveButton(R.string.delete, dialogClickListener)
|
||||
.setNegativeButton(R.string.cancel, dialogClickListener)
|
||||
.show()
|
||||
.show().setDefaultFocus()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -143,7 +143,7 @@ class PluginsFragment : Fragment() {
|
|||
}
|
||||
|
||||
observe(pluginViewModel.filteredPlugins) { (scrollToTop, list) ->
|
||||
(plugin_recycler_view?.adapter as? PluginAdapter?)?.updateList(list)
|
||||
(plugin_recycler_view?.adapter as? PluginAdapter)?.updateList(list)
|
||||
|
||||
if (scrollToTop)
|
||||
plugin_recycler_view?.scrollToPosition(0)
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.lifecycle.MutableLiveData
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.amap
|
||||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||
|
@ -102,11 +103,12 @@ class PluginsViewModel : ViewModel() {
|
|||
)
|
||||
}
|
||||
}.amap { (repo, metadata) ->
|
||||
PluginManager.downloadAndLoadPlugin(
|
||||
PluginManager.downloadPlugin(
|
||||
activity,
|
||||
metadata.url,
|
||||
metadata.internalName,
|
||||
repo
|
||||
repo,
|
||||
metadata.status != PROVIDER_STATUS_DOWN
|
||||
)
|
||||
}.main { list ->
|
||||
if (list.any { it }) {
|
||||
|
@ -151,12 +153,15 @@ class PluginsViewModel : ViewModel() {
|
|||
val (success, message) = if (file.exists()) {
|
||||
PluginManager.deletePlugin(file) to R.string.plugin_deleted
|
||||
} else {
|
||||
PluginManager.downloadAndLoadPlugin(
|
||||
val isEnabled = plugin.second.status != PROVIDER_STATUS_DOWN
|
||||
val message = if (isEnabled) R.string.plugin_loaded else R.string.plugin_downloaded
|
||||
PluginManager.downloadPlugin(
|
||||
activity,
|
||||
metadata.url,
|
||||
metadata.name,
|
||||
repo
|
||||
) to R.string.plugin_loaded
|
||||
repo,
|
||||
isEnabled
|
||||
) to message
|
||||
}
|
||||
|
||||
runOnMainThread {
|
||||
|
|
|
@ -67,7 +67,7 @@ class SetupFragmentLayout : Fragment() {
|
|||
crash_reporting_text?.text = getText(text)
|
||||
}
|
||||
|
||||
val enableCrashReporting = !settingsManager.getBoolean(ACRA.PREF_DISABLE_ACRA, false)
|
||||
val enableCrashReporting = !settingsManager.getBoolean(ACRA.PREF_DISABLE_ACRA, true)
|
||||
acra_switch.isChecked = enableCrashReporting
|
||||
crash_reporting_text.text =
|
||||
getText(
|
||||
|
|
|
@ -13,9 +13,7 @@ import android.media.tv.TvContract.Channels.COLUMN_INTERNAL_PROVIDER_ID
|
|||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.*
|
||||
import android.provider.MediaStore
|
||||
import android.text.Spanned
|
||||
import android.util.Log
|
||||
|
@ -30,16 +28,19 @@ import androidx.core.text.toSpanned
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.tvprovider.media.tv.*
|
||||
import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.google.android.gms.cast.framework.CastContext
|
||||
import com.google.android.gms.cast.framework.CastState
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
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.showToast
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent
|
||||
|
@ -50,6 +51,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStri
|
|||
import com.lagradost.cloudstream3.ui.WebviewFragment
|
||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel.Companion.downloadAll
|
||||
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
|
@ -65,6 +67,7 @@ import okhttp3.Cache
|
|||
import java.io.*
|
||||
import java.net.URL
|
||||
import java.net.URLDecoder
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
object AppUtils {
|
||||
fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) {
|
||||
|
@ -79,6 +82,19 @@ 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 BottomSheetDialog?.ownHide() {
|
||||
this?.hide()
|
||||
}
|
||||
|
||||
fun BottomSheetDialog?.ownShow() {
|
||||
// the reason for this is because show has a shitty animation we don't want
|
||||
this?.window?.setWindowAnimations(-1)
|
||||
this?.show()
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
this?.window?.setWindowAnimations(R.style.Animation_Design_BottomSheetDialog)
|
||||
}, 200)
|
||||
}
|
||||
|
||||
//fun Context.deleteFavorite(data: SearchResponse) {
|
||||
// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
// normalSafeApiCall {
|
||||
|
@ -151,6 +167,18 @@ object AppUtils {
|
|||
return builder.build()
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/67441735/13746422
|
||||
fun ViewPager2.reduceDragSensitivity(f: Int = 4) {
|
||||
val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView")
|
||||
recyclerViewField.isAccessible = true
|
||||
val recyclerView = recyclerViewField.get(this) as RecyclerView
|
||||
|
||||
val touchSlopField = RecyclerView::class.java.getDeclaredField("mTouchSlop")
|
||||
touchSlopField.isAccessible = true
|
||||
val touchSlop = touchSlopField.get(recyclerView) as Int
|
||||
touchSlopField.set(recyclerView, touchSlop * f) // "8" was obtained experimentally
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun getAllWatchNextPrograms(context: Context): Set<Long> {
|
||||
val COLUMN_WATCH_NEXT_ID_INDEX = 0
|
||||
|
@ -316,6 +344,46 @@ object AppUtils {
|
|||
}
|
||||
}
|
||||
|
||||
abstract class DiffAdapter<T>(
|
||||
open val items: MutableList<T>,
|
||||
val comparison: (first: T, second: T) -> Boolean = { first, second ->
|
||||
first.hashCode() == second.hashCode()
|
||||
}
|
||||
) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
|
||||
fun updateList(newList: List<T>) {
|
||||
val diffResult = DiffUtil.calculateDiff(
|
||||
GenericDiffCallback(this.items, newList)
|
||||
)
|
||||
|
||||
items.clear()
|
||||
items.addAll(newList)
|
||||
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
inner class GenericDiffCallback(
|
||||
private val oldList: List<T>,
|
||||
private val newList: List<T>
|
||||
) :
|
||||
DiffUtil.Callback() {
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
comparison(oldList[oldItemPosition], newList[newItemPosition])
|
||||
|
||||
override fun getOldListSize() = oldList.size
|
||||
|
||||
override fun getNewListSize() = newList.size
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
oldList[oldItemPosition] == newList[newItemPosition]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun Activity.downloadAllPluginsDialog(repositoryUrl: String, repositoryName: String) {
|
||||
runOnUiThread {
|
||||
val context = this
|
||||
|
@ -333,7 +401,7 @@ object AppUtils {
|
|||
|
||||
setNegativeButton(R.string.no) { _, _ -> }
|
||||
}
|
||||
builder.show()
|
||||
builder.show().setDefaultFocus()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -556,6 +624,17 @@ object AppUtils {
|
|||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the focus to the negative button when in TV and Emulator layout.
|
||||
**/
|
||||
fun AlertDialog.setDefaultFocus(buttonFocus: Int = DialogInterface.BUTTON_NEGATIVE) {
|
||||
if (!isTvSettings()) return
|
||||
this.getButton(buttonFocus).run {
|
||||
isFocusableInTouchMode = true
|
||||
requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from https://github.com/videolan/vlc-android/blob/master/application/vlc-android/src/org/videolan/vlc/util/FileUtils.kt
|
||||
@SuppressLint("Range")
|
||||
fun Context.getUri(data: Uri?): Uri? {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
|
@ -17,13 +18,11 @@ import com.lagradost.cloudstream3.mvvm.logError
|
|||
import com.lagradost.cloudstream3.plugins.PLUGINS_KEY
|
||||
import com.lagradost.cloudstream3.plugins.PLUGINS_KEY_LOCAL
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_CACHED_LIST
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_SHOULD_UPDATE_LIST
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_TOKEN_KEY
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_UNIXTIME_KEY
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_USER_KEY
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_CACHED_LIST
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_REFRESH_TOKEN_KEY
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_SHOULD_UPDATE_LIST
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_TOKEN_KEY
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_USER_KEY
|
||||
|
@ -51,12 +50,10 @@ object BackupUtils {
|
|||
// When sharing backup we do not want to transfer what is essentially the password
|
||||
ANILIST_TOKEN_KEY,
|
||||
ANILIST_CACHED_LIST,
|
||||
ANILIST_SHOULD_UPDATE_LIST,
|
||||
ANILIST_UNIXTIME_KEY,
|
||||
ANILIST_USER_KEY,
|
||||
MAL_TOKEN_KEY,
|
||||
MAL_REFRESH_TOKEN_KEY,
|
||||
MAL_SHOULD_UPDATE_LIST,
|
||||
MAL_CACHED_LIST,
|
||||
MAL_UNIXTIME_KEY,
|
||||
MAL_USER_KEY,
|
||||
|
@ -74,7 +71,7 @@ object BackupUtils {
|
|||
return !nonTransferableKeys.contains(this)
|
||||
}
|
||||
|
||||
var restoreFileSelector: ActivityResultLauncher<Array<String>>? = null
|
||||
private var restoreFileSelector: ActivityResultLauncher<Array<String>>? = null
|
||||
|
||||
// Kinda hack, but I couldn't think of a better way
|
||||
data class BackupVars(
|
||||
|
@ -91,6 +88,7 @@ object BackupUtils {
|
|||
@JsonProperty("settings") val settings: BackupVars
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun Context.getBackup(): BackupFile {
|
||||
val allData = getSharedPrefs().all.filter { it.key.isTransferable() }
|
||||
val allSettings = getDefaultSharedPrefs().all.filter { it.key.isTransferable() }
|
||||
|
@ -143,64 +141,66 @@ object BackupUtils {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
fun FragmentActivity.backup() {
|
||||
try {
|
||||
if (checkWrite()) {
|
||||
val subDir = getBasePath().first
|
||||
val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis()))
|
||||
val ext = "json"
|
||||
val displayName = "CS3_Backup_${date}"
|
||||
val backupFile = getBackup()
|
||||
|
||||
val steam =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && subDir?.isDownloadDir() == true) {
|
||||
val cr = this.contentResolver
|
||||
val contentUri =
|
||||
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI
|
||||
//val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||
|
||||
val newFile = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
||||
put(MediaStore.MediaColumns.TITLE, displayName)
|
||||
// While it a json file we store as txt because not
|
||||
// all file managers support mimetype json
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
|
||||
//put(MediaStore.MediaColumns.RELATIVE_PATH, folder)
|
||||
}
|
||||
|
||||
val newFileUri = cr.insert(
|
||||
contentUri,
|
||||
newFile
|
||||
) ?: throw IOException("Error creating file uri")
|
||||
cr.openOutputStream(newFileUri, "w")
|
||||
?: throw IOException("Error opening stream")
|
||||
} else {
|
||||
val fileName = "$displayName.$ext"
|
||||
val rFile = subDir?.findFile(fileName)
|
||||
if (rFile?.exists() == true) {
|
||||
rFile.delete()
|
||||
}
|
||||
val file =
|
||||
subDir?.createFile(fileName)
|
||||
?: throw IOException("Error creating file")
|
||||
if (!file.exists()) throw IOException("File does not exist")
|
||||
file.openOutputStream()
|
||||
}
|
||||
|
||||
val printStream = PrintWriter(steam)
|
||||
printStream.print(mapper.writeValueAsString(backupFile))
|
||||
printStream.close()
|
||||
|
||||
showToast(
|
||||
this,
|
||||
R.string.backup_success,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
} else {
|
||||
if (!checkWrite()) {
|
||||
showToast(this, getString(R.string.backup_failed), Toast.LENGTH_LONG)
|
||||
requestRW()
|
||||
return
|
||||
}
|
||||
|
||||
val subDir = getBasePath().first
|
||||
val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis()))
|
||||
val ext = "json"
|
||||
val displayName = "CS3_Backup_${date}"
|
||||
val backupFile = getBackup()
|
||||
|
||||
val steam = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
&& subDir?.isDownloadDir() == true
|
||||
) {
|
||||
val cr = this.contentResolver
|
||||
val contentUri =
|
||||
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI
|
||||
//val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||
|
||||
val newFile = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
||||
put(MediaStore.MediaColumns.TITLE, displayName)
|
||||
// While it a json file we store as txt because not
|
||||
// all file managers support mimetype json
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
|
||||
//put(MediaStore.MediaColumns.RELATIVE_PATH, folder)
|
||||
}
|
||||
|
||||
val newFileUri = cr.insert(
|
||||
contentUri,
|
||||
newFile
|
||||
) ?: throw IOException("Error creating file uri")
|
||||
cr.openOutputStream(newFileUri, "w")
|
||||
?: throw IOException("Error opening stream")
|
||||
} else {
|
||||
val fileName = "$displayName.$ext"
|
||||
val rFile = subDir?.findFile(fileName)
|
||||
if (rFile?.exists() == true) {
|
||||
rFile.delete()
|
||||
}
|
||||
val file =
|
||||
subDir?.createFile(fileName)
|
||||
?: throw IOException("Error creating file")
|
||||
if (!file.exists()) throw IOException("File does not exist")
|
||||
file.openOutputStream()
|
||||
}
|
||||
|
||||
val printStream = PrintWriter(steam)
|
||||
printStream.print(mapper.writeValueAsString(backupFile))
|
||||
printStream.close()
|
||||
|
||||
showToast(
|
||||
this,
|
||||
R.string.backup_success,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
try {
|
||||
|
@ -264,6 +264,7 @@ object BackupUtils {
|
|||
"application/json",
|
||||
"unknown/unknown",
|
||||
"content/unknown",
|
||||
"application/octet-stream",
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.APIHolder.capitalize
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
|
@ -10,9 +11,13 @@ import com.lagradost.cloudstream3.DubStatus
|
|||
import com.lagradost.cloudstream3.SearchQuality
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.result.VideoWatchState
|
||||
|
||||
const val VIDEO_POS_DUR = "video_pos_dur"
|
||||
const val VIDEO_WATCH_STATE = "video_watch_state"
|
||||
const val RESULT_WATCH_STATE = "result_watch_state"
|
||||
const val RESULT_WATCH_STATE_DATA = "result_watch_state_data"
|
||||
const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // changed due to id changes
|
||||
|
@ -49,7 +54,20 @@ object DataStoreHelper {
|
|||
@JsonProperty("year") val year: Int?,
|
||||
@JsonProperty("quality") override var quality: SearchQuality? = null,
|
||||
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
|
||||
) : SearchResponse
|
||||
) : SearchResponse {
|
||||
fun toLibraryItem(id: String): SyncAPI.LibraryItem {
|
||||
return SyncAPI.LibraryItem(
|
||||
name,
|
||||
url,
|
||||
id,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
apiName, type, posterUrl, posterHeaders, quality, this.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ResumeWatchingResult(
|
||||
@JsonProperty("name") override val name: String,
|
||||
|
@ -69,6 +87,9 @@ object DataStoreHelper {
|
|||
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
|
||||
) : SearchResponse
|
||||
|
||||
/**
|
||||
* A datastore wide account for future implementations of a multiple account system
|
||||
**/
|
||||
private var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION
|
||||
|
||||
fun getAllWatchStateIds(): List<Int>? {
|
||||
|
@ -175,6 +196,7 @@ object DataStoreHelper {
|
|||
fun setBookmarkedData(id: Int?, data: BookmarkedData) {
|
||||
if (id == null) return
|
||||
setKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString(), data)
|
||||
AccountManager.localListApi.requireLibraryRefresh = true
|
||||
}
|
||||
|
||||
fun getBookmarkedData(id: Int?): BookmarkedData? {
|
||||
|
@ -193,6 +215,22 @@ object DataStoreHelper {
|
|||
return getKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), null)
|
||||
}
|
||||
|
||||
fun getVideoWatchState(id: Int?): VideoWatchState? {
|
||||
if (id == null) return null
|
||||
return getKey("$currentAccount/$VIDEO_WATCH_STATE", id.toString(), null)
|
||||
}
|
||||
|
||||
fun setVideoWatchState(id: Int?, watchState: VideoWatchState) {
|
||||
if (id == null) return
|
||||
|
||||
// None == No key
|
||||
if (watchState == VideoWatchState.None) {
|
||||
removeKey("$currentAccount/$VIDEO_WATCH_STATE", id.toString())
|
||||
} else {
|
||||
setKey("$currentAccount/$VIDEO_WATCH_STATE", id.toString(), watchState)
|
||||
}
|
||||
}
|
||||
|
||||
fun getDub(id: Int): DubStatus? {
|
||||
return DubStatus.values()
|
||||
.getOrNull(getKey("$currentAccount/$RESULT_DUB", id.toString(), -1) ?: -1)
|
||||
|
|
|
@ -260,9 +260,11 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
|||
UpstreamExtractor(),
|
||||
|
||||
Tomatomatela(),
|
||||
TomatomatelalClub(),
|
||||
Cinestart(),
|
||||
OkRu(),
|
||||
OkRuHttps(),
|
||||
Okrulink(),
|
||||
|
||||
// dood extractors
|
||||
DoodCxExtractor(),
|
||||
|
@ -356,6 +358,10 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
|||
VidSrcExtractor2(),
|
||||
PlayLtXyz(),
|
||||
AStreamHub(),
|
||||
|
||||
Cda(),
|
||||
Dailymotion(),
|
||||
ByteShare(),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -15,12 +15,18 @@ import com.lagradost.cloudstream3.*
|
|||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import okio.BufferedSink
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import java.io.File
|
||||
import android.text.TextUtils
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
|
||||
|
||||
class InAppUpdater {
|
||||
|
@ -291,24 +297,52 @@ class InAppUpdater {
|
|||
val context = this
|
||||
builder.apply {
|
||||
setPositiveButton(R.string.update) { _, _ ->
|
||||
// Forcefully start any delayed installations
|
||||
if (ApkInstaller.delayedInstaller?.startInstallation() == true) return@setPositiveButton
|
||||
|
||||
showToast(context, R.string.download_started, Toast.LENGTH_LONG)
|
||||
val intent = PackageInstallerService.getIntent(
|
||||
context,
|
||||
update.updateURL
|
||||
)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
// ioSafe {
|
||||
// if (
|
||||
// !downloadUpdate(update.updateURL)
|
||||
// )
|
||||
// runOnUiThread {
|
||||
// showToast(
|
||||
// context,
|
||||
// R.string.download_failed,
|
||||
// Toast.LENGTH_LONG
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
// Check if the setting hasn't been changed
|
||||
if (settingsManager.getInt(
|
||||
getString(R.string.apk_installer_key),
|
||||
-1
|
||||
) == -1
|
||||
) {
|
||||
if (isMiUi()) // Set to legacy if using miui
|
||||
settingsManager.edit()
|
||||
.putInt(getString(R.string.apk_installer_key), 1)
|
||||
.apply()
|
||||
}
|
||||
|
||||
val currentInstaller =
|
||||
settingsManager.getInt(
|
||||
getString(R.string.apk_installer_key),
|
||||
0
|
||||
)
|
||||
|
||||
when (currentInstaller) {
|
||||
// New method
|
||||
0 -> {
|
||||
val intent = PackageInstallerService.getIntent(
|
||||
context,
|
||||
update.updateURL
|
||||
)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
// Legacy
|
||||
1 -> {
|
||||
ioSafe {
|
||||
if (!downloadUpdate(update.updateURL))
|
||||
runOnUiThread {
|
||||
showToast(
|
||||
context,
|
||||
R.string.download_failed,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
|
@ -322,7 +356,7 @@ class InAppUpdater {
|
|||
}
|
||||
}
|
||||
}
|
||||
builder.show()
|
||||
builder.show().setDefaultFocus()
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
|
@ -333,5 +367,20 @@ class InAppUpdater {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun isMiUi(): Boolean {
|
||||
return !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.name"))
|
||||
}
|
||||
|
||||
private fun getSystemProperty(propName: String): String? {
|
||||
return try {
|
||||
val p = Runtime.getRuntime().exec("getprop $propName")
|
||||
BufferedReader(InputStreamReader(p.inputStream), 1024).use {
|
||||
it.readLine()
|
||||
}
|
||||
} catch (ex: IOException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.pow
|
||||
|
||||
//author: https://github.com/daarkdemon
|
||||
class JsHunter(private val hunterJS: String) {
|
||||
|
||||
/**
|
||||
* Detects whether the javascript is H.U.N.T.E.R coded.
|
||||
*
|
||||
* @return true if it's H.U.N.T.E.R coded.
|
||||
*/
|
||||
fun detect(): Boolean {
|
||||
val p = Pattern.compile("eval\\(function\\(h,u,n,t,e,r\\)")
|
||||
val searchResults = p.matcher(hunterJS)
|
||||
return searchResults.find()
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpack the javascript
|
||||
*
|
||||
* @return the javascript unhunt or null.
|
||||
*/
|
||||
|
||||
fun dehunt(): String? {
|
||||
try {
|
||||
val p: Pattern =
|
||||
Pattern.compile(
|
||||
"""}\("([^"]+)",[^,]+,\s*"([^"]+)",\s*(\d+),\s*(\d+)""",
|
||||
Pattern.DOTALL
|
||||
)
|
||||
val searchResults: Matcher = p.matcher(hunterJS)
|
||||
if (searchResults.find() && searchResults.groupCount() == 4) {
|
||||
val h = searchResults.group(1)!!.toString()
|
||||
val n = searchResults.group(2)!!.toString()
|
||||
val t = searchResults.group(3)!!.toInt()
|
||||
val e = searchResults.group(4)!!.toInt()
|
||||
return hunter(h, n, t, e)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun duf(d: String, e: Int, f: Int = 10): Int {
|
||||
val str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/"
|
||||
val g = str.toList()
|
||||
val h = g.take(e)
|
||||
val i = g.take(f)
|
||||
val dList = d.reversed().toList()
|
||||
var j = 0.0
|
||||
for ((c, b) in dList.withIndex()) {
|
||||
if (b in h) {
|
||||
j += h.indexOf(b) * e.toDouble().pow(c)
|
||||
}
|
||||
}
|
||||
var k = ""
|
||||
while (j > 0) {
|
||||
k = i[(j % f).toInt()] + k
|
||||
j = (j - j % f) / f
|
||||
}
|
||||
return k.toIntOrNull() ?: 0
|
||||
}
|
||||
|
||||
private fun hunter(h: String, n: String, t: Int, e: Int): String {
|
||||
var result = ""
|
||||
var i = 0
|
||||
while (i < h.length) {
|
||||
var j = 0
|
||||
var s = ""
|
||||
while (h[i] != n[e]) {
|
||||
s += h[i]
|
||||
i++
|
||||
}
|
||||
while (j < n.length) {
|
||||
s = s.replace(n[j], j.digitToChar())
|
||||
j++
|
||||
}
|
||||
result += (duf(s, e) - t).toChar()
|
||||
i++
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -5,15 +5,42 @@ import android.content.BroadcastReceiver
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.IntentSender
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import java.io.InputStream
|
||||
|
||||
const val INSTALL_ACTION = "ApkInstaller.INSTALL_ACTION"
|
||||
|
||||
|
||||
class ApkInstaller(private val service: PackageInstallerService) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Used for postponed installations
|
||||
**/
|
||||
var delayedInstaller: DelayedInstaller? = null
|
||||
}
|
||||
|
||||
inner class DelayedInstaller(
|
||||
private val session: PackageInstaller.Session,
|
||||
private val intent: IntentSender
|
||||
) {
|
||||
fun startInstallation(): Boolean {
|
||||
return try {
|
||||
session.commit(intent)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}.also { delayedInstaller = null }
|
||||
}
|
||||
}
|
||||
|
||||
private val packageInstaller = service.packageManager.packageInstaller
|
||||
|
||||
enum class InstallProgressStatus {
|
||||
|
@ -76,7 +103,6 @@ class ApkInstaller(private val service: PackageInstallerService) {
|
|||
inputStream.close()
|
||||
}
|
||||
|
||||
installProgressStatus.invoke(InstallProgressStatus.Installing)
|
||||
|
||||
val intentSender = PendingIntent.getBroadcast(
|
||||
service,
|
||||
|
@ -85,7 +111,22 @@ class ApkInstaller(private val service: PackageInstallerService) {
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
|
||||
).intentSender
|
||||
|
||||
session.commit(intentSender)
|
||||
// Use delayed installations on android 13 and only if "allow from unknown sources" is enabled
|
||||
// if the app lacks installation permission it cannot ask for the permission when it's closed.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
||||
context.packageManager.canRequestPackageInstalls()
|
||||
) {
|
||||
// Save for later installation since it's more jarring to have the app exit abruptly
|
||||
delayedInstaller = DelayedInstaller(session, intentSender)
|
||||
main {
|
||||
// Use real toast since it should show even if app is exited
|
||||
Toast.makeText(context, R.string.delayed_update_notice, Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
} else {
|
||||
installProgressStatus.invoke(InstallProgressStatus.Installing)
|
||||
session.commit(intentSender)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
|
||||
|
|
|
@ -187,7 +187,7 @@ class PackageInstallerService : Service() {
|
|||
const val UPDATE_CHANNEL_ID = "cloudstream3.updates"
|
||||
const val UPDATE_CHANNEL_NAME = "App Updates"
|
||||
const val UPDATE_CHANNEL_DESCRIPTION = "App updates notification channel"
|
||||
const val UPDATE_NOTIFICATION_ID = -68454136
|
||||
const val UPDATE_NOTIFICATION_ID = -68454136 // Random unique
|
||||
|
||||
fun getIntent(
|
||||
context: Context,
|
||||
|
|
|
@ -99,7 +99,8 @@ object SingleSelectionHelper {
|
|||
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 applyHolder = dialog.apply_btt_holder//.findViewById<LinearLayout>(R.id.apply_btt_holder)
|
||||
val applyHolder =
|
||||
dialog.apply_btt_holder//.findViewById<LinearLayout>(R.id.apply_btt_holder)
|
||||
|
||||
applyHolder?.isVisible = realShowApply
|
||||
if (!realShowApply) {
|
||||
|
@ -249,6 +250,17 @@ object SingleSelectionHelper {
|
|||
)
|
||||
}
|
||||
|
||||
fun showBottomDialog(
|
||||
items: List<String>,
|
||||
selectedIndex: Int,
|
||||
name: String,
|
||||
showApply: Boolean,
|
||||
dismissCallback: () -> Unit,
|
||||
callback: (Int) -> Unit,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
/** Only for a low amount of items */
|
||||
fun Activity?.showBottomDialog(
|
||||
items: List<String>,
|
||||
|
|
|
@ -4,6 +4,7 @@ package com.lagradost.cloudstream3.utils
|
|||
|
||||
import android.util.Log
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.APIHolder.apis
|
||||
//import com.lagradost.cloudstream3.animeproviders.AniflixProvider
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
|
@ -78,17 +79,21 @@ object SyncUtil {
|
|||
return null
|
||||
}
|
||||
|
||||
suspend fun getUrlsFromId(id: String, type: String = "anilist") : List<String> {
|
||||
return arrayListOf()
|
||||
// val url =
|
||||
// "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/$type/anime/$id.json"
|
||||
// val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).parsed<SyncPage>()
|
||||
// val pages = response.pages ?: return emptyList()
|
||||
// val current = pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values).mapNotNull { it.url }.toMutableList()
|
||||
// if(type == "anilist") { // TODO MAKE BETTER
|
||||
// current.add("${AniflixProvider().mainUrl}/anime/$id")
|
||||
// }
|
||||
// return current
|
||||
suspend fun getUrlsFromId(id: String, type: String = "anilist"): List<String> {
|
||||
val url =
|
||||
"https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/$type/anime/$id.json"
|
||||
val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).parsed<SyncPage>()
|
||||
val pages = response.pages ?: return emptyList()
|
||||
val current =
|
||||
pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values)
|
||||
.mapNotNull { it.url }.toMutableList()
|
||||
|
||||
if (type == "anilist") { // TODO MAKE BETTER
|
||||
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
|
||||
current.add("${it.mainUrl}/anime/$id")
|
||||
}
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
data class SyncPage(
|
||||
|
|
|
@ -9,7 +9,9 @@ import android.content.Context
|
|||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
|
@ -28,15 +30,21 @@ import androidx.core.app.ActivityCompat
|
|||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.alpha
|
||||
import androidx.core.graphics.blue
|
||||
import androidx.core.graphics.drawable.toBitmapOrNull
|
||||
import androidx.core.graphics.green
|
||||
import androidx.core.graphics.red
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.palette.graphics.Palette
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||
|
@ -57,7 +65,10 @@ object UIHelper {
|
|||
this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
)
|
||||
== PackageManager.PERMISSION_GRANTED)
|
||||
== PackageManager.PERMISSION_GRANTED
|
||||
// Since Android 13, we can't request external storage permission,
|
||||
// so don't check it.
|
||||
|| Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
}
|
||||
|
||||
fun Activity.requestRW() {
|
||||
|
@ -102,7 +113,7 @@ object UIHelper {
|
|||
listView.requestLayout()
|
||||
}
|
||||
|
||||
fun Activity?.getSpanCount(): Int? {
|
||||
fun Context?.getSpanCount(): Int? {
|
||||
val compactView = false
|
||||
val spanCountLandscape = if (compactView) 2 else 6
|
||||
val spanCountPortrait = if (compactView) 1 else 3
|
||||
|
@ -155,12 +166,27 @@ object UIHelper {
|
|||
return color
|
||||
}
|
||||
|
||||
var createPaletteAsyncCache: HashMap<String, Palette> = hashMapOf()
|
||||
fun createPaletteAsync(url: String, bitmap: Bitmap, callback: (Palette) -> Unit) {
|
||||
createPaletteAsyncCache[url]?.let { palette ->
|
||||
callback.invoke(palette)
|
||||
return
|
||||
}
|
||||
Palette.from(bitmap).generate { paletteNull ->
|
||||
paletteNull?.let { palette ->
|
||||
createPaletteAsyncCache[url] = palette
|
||||
callback(palette)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ImageView?.setImage(
|
||||
url: String?,
|
||||
headers: Map<String, String>? = null,
|
||||
@DrawableRes
|
||||
errorImageDrawable: Int? = null,
|
||||
fadeIn: Boolean = true
|
||||
fadeIn: Boolean = true,
|
||||
colorCallback: ((Palette) -> Unit)? = null
|
||||
): Boolean {
|
||||
if (this == null || url.isNullOrBlank()) return false
|
||||
|
||||
|
@ -174,6 +200,33 @@ object UIHelper {
|
|||
else req
|
||||
}
|
||||
|
||||
if (colorCallback != null) {
|
||||
builder.listener(object : RequestListener<Drawable> {
|
||||
@SuppressLint("CheckResult")
|
||||
override fun onResourceReady(
|
||||
resource: Drawable?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
resource?.toBitmapOrNull()
|
||||
?.let { bitmap -> createPaletteAsync(url, bitmap, colorCallback) }
|
||||
return false
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
val res = if (errorImageDrawable != null)
|
||||
builder.error(errorImageDrawable).into(this)
|
||||
else
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?attr/colorPrimary" android:state_checked="true"/>
|
||||
<item android:color="?attr/colorPrimary" android:state_focused="true"/>
|
||||
<item android:color="?attr/colorPrimary" android:state_selected="true"/>
|
||||
<item android:color="?attr/grayTextColor" android:state_checked="false"/>
|
||||
</selector>
|
|
@ -0,0 +1,6 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,12l-2.5,-1.5L15,12L15,4h5v8z"/>
|
||||
</vector>
|
|
@ -1,5 +1,5 @@
|
|||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:tint="?attr/white" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M15.41,16.59L10.83,12l4.58,-4.59L14,6l-6,6 6,6 1.41,-1.41z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M13.3,17.3 L8.7,12.7Q8.55,12.55 8.488,12.375Q8.425,12.2 8.425,12Q8.425,11.8 8.488,11.625Q8.55,11.45 8.7,11.3L13.3,6.7Q13.575,6.425 14,6.425Q14.425,6.425 14.7,6.7Q14.975,6.975 14.975,7.4Q14.975,7.825 14.7,8.1L10.8,12L14.7,15.9Q14.975,16.175 14.975,16.6Q14.975,17.025 14.7,17.3Q14.425,17.575 14,17.575Q13.575,17.575 13.3,17.3Z"/>
|
||||
</vector>
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:tint="?attr/white" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M8.59,16.59L13.17,12 8.59,7.41 10,6l6,6 -6,6 -1.41,-1.41z"/>
|
||||
<vector android:autoMirrored="true"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/white"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M8.7,17.3Q8.425,17.025 8.425,16.6Q8.425,16.175 8.7,15.9L12.6,12L8.7,8.1Q8.425,7.825 8.425,7.4Q8.425,6.975 8.7,6.7Q8.975,6.425 9.4,6.425Q9.825,6.425 10.1,6.7L14.7,11.3Q14.85,11.45 14.913,11.625Q14.975,11.8 14.975,12Q14.975,12.2 14.913,12.375Q14.85,12.55 14.7,12.7L10.1,17.3Q9.825,17.575 9.4,17.575Q8.975,17.575 8.7,17.3Z" />
|
||||
</vector>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:tint="?attr/white" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/>
|
||||
</vector>
|
|
@ -1,5 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="?attr/white"
|
||||
<vector android:height="12dp" android:tint="?attr/white"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
android:width="12dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
|
||||
</vector>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM7.35,18.5C8.66,17.56 10.26,17 12,17s3.34,0.56 4.65,1.5C15.34,19.44 13.74,20 12,20S8.66,19.44 7.35,18.5zM18.14,17.12L18.14,17.12C16.45,15.8 14.32,15 12,15s-4.45,0.8 -6.14,2.12l0,0C4.7,15.73 4,13.95 4,12c0,-4.42 3.58,-8 8,-8s8,3.58 8,8C20,13.95 19.3,15.73 18.14,17.12z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,6c-1.93,0 -3.5,1.57 -3.5,3.5S10.07,13 12,13s3.5,-1.57 3.5,-3.5S13.93,6 12,6zM12,11c-0.83,0 -1.5,-0.67 -1.5,-1.5S11.17,8 12,8s1.5,0.67 1.5,1.5S12.83,11 12,11z"/>
|
||||
</vector>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/textColor"/>
|
||||
<corners android:radius="16dp" />
|
||||
</shape>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/ratingColorBg"/>
|
||||
<corners android:radius="@dimen/rounded_image_radius"/>
|
||||
<!-- <stroke android:color="@color/subColor" android:width="2dp"/>-->
|
||||
</shape>
|
|
@ -14,10 +14,11 @@
|
|||
|
||||
<com.google.android.material.navigationrail.NavigationRailView
|
||||
android:id="@+id/nav_rail_view"
|
||||
android:layout_width="62dp"
|
||||
android:layout_width="@dimen/navbar_width"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/primaryGrayBackground"
|
||||
app:itemIconTint="@color/item_select_color"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
|
||||
app:itemTextColor="@color/item_select_color"
|
||||
app:labelVisibilityMode="unlabeled"
|
||||
|
@ -34,9 +35,9 @@
|
|||
-->
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/nav_view"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="70dp"
|
||||
android:layout_width="0dp"
|
||||
app:labelVisibilityMode="labeled"
|
||||
app:labelVisibilityMode="unlabeled"
|
||||
android:background="?attr/primaryGrayBackground"
|
||||
|
||||
app:itemIconTint="@color/item_select_color"
|
||||
|
|
|
@ -1,64 +1,64 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/homeRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard|navigation"
|
||||
android:paddingTop="0dp">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/homeRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard|navigation"
|
||||
android:paddingTop="0dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/cast_mini_controller_holder"
|
||||
app:navGraph="@navigation/mobile_navigation"
|
||||
app:layout_constraintStart_toEndOf="@id/nav_rail_view"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/cast_mini_controller_holder"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:navGraph="@navigation/mobile_navigation" />
|
||||
|
||||
<com.google.android.material.navigationrail.NavigationRailView
|
||||
android:layout_width="62dp"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/nav_rail_view"
|
||||
android:background="?attr/primaryGrayBackground"
|
||||
app:itemTextColor="@color/item_select_color"
|
||||
app:itemIconTint="@color/item_select_color"
|
||||
android:id="@+id/nav_rail_view"
|
||||
android:layout_width="62dp"
|
||||
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/primaryGrayBackground"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
app:itemIconTint="@color/item_select_color"
|
||||
app:itemTextColor="@color/item_select_color"
|
||||
|
||||
app:menuGravity="center"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:labelVisibilityMode="unlabeled"
|
||||
app:menu="@menu/bottom_nav_menu">
|
||||
app:labelVisibilityMode="unlabeled"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:menu="@menu/bottom_nav_menu"
|
||||
app:menuGravity="center">
|
||||
|
||||
</com.google.android.material.navigationrail.NavigationRailView>
|
||||
|
||||
<LinearLayout
|
||||
app:layout_constraintStart_toEndOf="@+id/nav_rail_view"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:layout_height="100dp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/cast_mini_controller_holder">
|
||||
android:id="@+id/cast_mini_controller_holder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/nav_rail_view"
|
||||
tools:layout_height="100dp">
|
||||
<!--com.google.android.gms.cast.framework.media.widget.MiniControllerFragment-->
|
||||
<fragment
|
||||
app:customCastBackgroundColor="?attr/primaryGrayBackground"
|
||||
app:castControlButtons="@array/cast_mini_controller_control_buttons"
|
||||
android:id="@+id/cast_mini_controller"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
class="com.lagradost.cloudstream3.ui.MyMiniControllerFragment"
|
||||
tools:ignore="FragmentTagUsage" />
|
||||
android:id="@+id/cast_mini_controller"
|
||||
class="com.lagradost.cloudstream3.ui.MyMiniControllerFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:castControlButtons="@array/cast_mini_controller_control_buttons"
|
||||
app:customCastBackgroundColor="?attr/primaryGrayBackground"
|
||||
tools:ignore="FragmentTagUsage" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
|
@ -0,0 +1,197 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/resultview_preview_result"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="@dimen/rounded_image_radius">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/resultview_preview_poster"
|
||||
android:layout_width="88dp"
|
||||
android:layout_height="138dp"
|
||||
android:contentDescription="@string/poster_image"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/example_poster" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="138dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/resultview_preview_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="16sp"
|
||||
|
||||
android:textStyle="bold"
|
||||
tools:text="The Perfect Run">
|
||||
|
||||
</TextView>
|
||||
|
||||
<com.lagradost.cloudstream3.widget.FlowLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:itemSpacing="10dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/resultview_preview_meta_type"
|
||||
style="@style/ResultInfoText"
|
||||
tools:text="Movie" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/resultview_preview_meta_year"
|
||||
style="@style/ResultInfoText"
|
||||
tools:text="2022" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/resultview_preview_meta_rating"
|
||||
style="@style/ResultInfoText"
|
||||
tools:text="Rated: 8.5/10.0" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/resultview_preview_meta_status"
|
||||
style="@style/ResultInfoText"
|
||||
tools:text="Ongoing" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/resultview_preview_meta_duration"
|
||||
style="@style/ResultInfoText"
|
||||
tools:text="121min" />
|
||||
</com.lagradost.cloudstream3.widget.FlowLayout>
|
||||
|
||||
<!-- <TextView
|
||||
android:id="@+id/resultview_preview_year"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:textColor="?attr/grayTextColor"
|
||||
tools:text="2023" />-->
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/resultview_preview_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:ellipsize="end"
|
||||
android:textColor="?attr/textColor"
|
||||
tools:text="Ryan Quicksave Romano is an eccentric adventurer with a strange power: he can create a save-point in time and redo his life whenever he dies. Arriving in New Rome, the glitzy capital of sin of a rebuilding Europe, he finds the city torn between mega-corporations, sponsored heroes, superpowered criminals, and true monsters. It's a time of chaos, where potions can grant the power to rule the world and dangers lurk everywhere. " />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/background_shadow" />
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/resultview_preview_more_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:gravity="start|center_vertical"
|
||||
android:padding="12dp"
|
||||
android:text="@string/home_more_info"
|
||||
android:textColor="?attr/textColor"
|
||||
app:drawableRightCompat="@drawable/ic_baseline_arrow_forward_24"
|
||||
app:drawableTint="?attr/white" />
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/resultview_preview_loading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.facebook.shimmer.ShimmerFrameLayout
|
||||
android:id="@+id/resultview_preview_loading_shimmer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical"
|
||||
app:shimmer_auto_start="true"
|
||||
app:shimmer_base_alpha="0.2"
|
||||
app:shimmer_duration="@integer/loading_time"
|
||||
app:shimmer_highlight_alpha="0.3">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/result_padding"
|
||||
android:layout_marginTop="@dimen/result_padding"
|
||||
|
||||
android:layout_marginEnd="@dimen/result_padding"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<include layout="@layout/loading_poster" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp">
|
||||
|
||||
<include layout="@layout/loading_line" />
|
||||
|
||||
<include layout="@layout/loading_line_short" />
|
||||
|
||||
<include layout="@layout/loading_line" />
|
||||
|
||||
<include layout="@layout/loading_line" />
|
||||
|
||||
<include layout="@layout/loading_line" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="26dp"
|
||||
android:layout_margin="@dimen/result_padding"
|
||||
android:layout_marginBottom="@dimen/loading_margin"
|
||||
android:background="@color/grayShimmer"
|
||||
app:cardCornerRadius="@dimen/loading_radius"
|
||||
tools:ignore="ContentDescription" />
|
||||
</LinearLayout>
|
||||
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
|
|
@ -1,140 +1,158 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@null"
|
||||
android:orientation="vertical">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@null"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="60dp"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/sort_subtitles_holder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="50"
|
||||
android:orientation="vertical">
|
||||
android:id="@+id/sort_subtitles_holder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="50"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- android:id="@+id/subs_settings" android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
-->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_margin="10dp"
|
||||
android:background="@drawable/search_background"
|
||||
android:visibility="visible">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_margin="10dp"
|
||||
android:background="@drawable/search_background"
|
||||
android:visibility="visible">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="30dp">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="30dp">
|
||||
|
||||
<androidx.appcompat.widget.SearchView
|
||||
android:id="@+id/subtitles_search"
|
||||
app:iconifiedByDefault="false"
|
||||
app:queryBackground="@color/transparent"
|
||||
app:queryHint="@string/search_hint"
|
||||
android:id="@+id/subtitles_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
|
||||
app:searchIcon="@drawable/search_icon"
|
||||
android:layout_width="match_parent"
|
||||
android:imeOptions="actionSearch"
|
||||
android:inputType="text"
|
||||
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingStart="-10dp"
|
||||
app:iconifiedByDefault="false"
|
||||
|
||||
android:imeOptions="actionSearch"
|
||||
android:inputType="text"
|
||||
android:paddingStart="-10dp"
|
||||
tools:ignore="RtlSymmetry">
|
||||
app:queryBackground="@color/transparent"
|
||||
app:queryHint="@string/search_hint"
|
||||
app:searchIcon="@drawable/search_icon"
|
||||
tools:ignore="RtlSymmetry">
|
||||
|
||||
<androidx.core.widget.ContentLoadingProgressBar
|
||||
android:id="@+id/search_loading_bar"
|
||||
style="@style/Widget.AppCompat.ProgressBar"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="-70dp"
|
||||
android:foregroundTint="@color/white"
|
||||
android:visibility="visible"
|
||||
tools:visibility="visible"
|
||||
android:progressTint="@color/white">
|
||||
|
||||
</androidx.core.widget.ContentLoadingProgressBar>
|
||||
<!--app:queryHint="@string/search_hint"
|
||||
android:background="@color/grayBackground" @color/itemBackground
|
||||
app:searchHintIcon="@drawable/search_white"
|
||||
-->
|
||||
</androidx.appcompat.widget.SearchView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:gravity="end">
|
||||
|
||||
<androidx.core.widget.ContentLoadingProgressBar
|
||||
android:id="@+id/search_loading_bar"
|
||||
style="@style/Widget.AppCompat.ProgressBar"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:foregroundTint="@color/white"
|
||||
android:progressTint="@color/white"
|
||||
android:visibility="visible"
|
||||
tools:visibility="visible">
|
||||
|
||||
</androidx.core.widget.ContentLoadingProgressBar>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/year_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:text="@string/none"
|
||||
/>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/search_filter"
|
||||
app:tint="?attr/textColor"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
android:id="@+id/search_filter"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_margin="10dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/change_providers_img_des"
|
||||
android:nextFocusLeft="@id/main_search"
|
||||
android:nextFocusRight="@id/main_search"
|
||||
android:nextFocusUp="@id/nav_rail_view"
|
||||
android:nextFocusDown="@id/search_autofit_results"
|
||||
android:src="@drawable/ic_baseline_tune_24" />
|
||||
android:layout_margin="10dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/change_providers_img_des"
|
||||
android:nextFocusLeft="@id/main_search"
|
||||
android:nextFocusRight="@id/main_search"
|
||||
android:nextFocusUp="@id/nav_rail_view"
|
||||
android:nextFocusDown="@id/search_autofit_results"
|
||||
android:src="@drawable/ic_baseline_tune_24"
|
||||
app:tint="?attr/textColor" />
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/subtitle_adapter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/subtitle_adapter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
android:layout_rowWeight="1"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:nextFocusLeft="@id/sort_providers"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:listfooter="@layout/sort_bottom_footer_add_choice"
|
||||
tools:listitem="@layout/sort_bottom_single_choice" />
|
||||
android:layout_rowWeight="1"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:nextFocusLeft="@id/sort_providers"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:listfooter="@layout/sort_bottom_footer_add_choice"
|
||||
tools:listitem="@layout/sort_bottom_single_choice" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/apply_btt_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginTop="-60dp"
|
||||
android:gravity="bottom|end"
|
||||
android:orientation="horizontal">
|
||||
android:id="@+id/apply_btt_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginTop="-60dp"
|
||||
android:gravity="bottom|end"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/apply_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/sort_apply" />
|
||||
android:id="@+id/apply_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/sort_apply" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel_btt"
|
||||
style="@style/BlackButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/sort_cancel" />
|
||||
android:id="@+id/cancel_btt"
|
||||
style="@style/BlackButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/sort_cancel" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
android:id="@+id/download_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/primaryGrayBackground"
|
||||
android:orientation="vertical"
|
||||
tools:context=".ui.download.DownloadFragment">
|
||||
|
||||
|
@ -132,7 +131,8 @@
|
|||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/download_list"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
android:paddingBottom="100dp"
|
||||
android:clipToPadding="false"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
|
|
|
@ -153,435 +153,14 @@
|
|||
android:textColor="?attr/textColor" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/home_loaded"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/home_master_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:id="@+id/home_statusbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/primaryGrayBackground" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/home_settings_bar"
|
||||
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="70dp"
|
||||
android:background="?attr/primaryGrayBackground"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="10dp"
|
||||
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:paddingBottom="10dp">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/home_profile_picture_holder"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="10dp"
|
||||
app:cardCornerRadius="100dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/home_profile_picture"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
tools:ignore="ContentDescription" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="@drawable/search_background"
|
||||
android:visibility="visible">
|
||||
|
||||
<androidx.appcompat.widget.SearchView
|
||||
android:id="@+id/home_search2"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:iconifiedByDefault="false"
|
||||
android:paddingStart="-10dp"
|
||||
app:iconifiedByDefault="false"
|
||||
app:queryBackground="@color/transparent"
|
||||
app:queryHint="@string/search_hint"
|
||||
app:searchIcon="@drawable/search_icon"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/home_provider_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center_vertical"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
|
||||
tools:text="Hello World" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/home_provider_meta_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center_vertical"
|
||||
android:textColor="?attr/grayTextColor"
|
||||
android:textSize="14sp"
|
||||
tools:text="Hello World" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!--
|
||||
<ImageView
|
||||
android:nextFocusDown="@id/home_main_poster_recyclerview"
|
||||
android:nextFocusUp="@id/nav_rail_view"
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
|
||||
android:id="@+id/home_change_api"
|
||||
android:layout_margin="10dp"
|
||||
android:layout_gravity="center|end"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
|
||||
android:src="@drawable/ic_baseline_keyboard_arrow_down_24"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:contentDescription="@string/home_change_provider_img_des">
|
||||
|
||||
<requestFocus />
|
||||
</ImageView>-->
|
||||
</FrameLayout>
|
||||
<!--https://www.digitalocean.com/community/tutorials/android-viewpager-example-tutorial-->
|
||||
<FrameLayout
|
||||
android:id="@+id/home_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="500dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/home_preview_viewpager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
</androidx.viewpager2.widget.ViewPager2>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/home_preview_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0.8"
|
||||
android:scaleType="centerCrop"
|
||||
android:visibility="gone"
|
||||
tools:src="@drawable/example_poster" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/home_padding"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.SearchView
|
||||
android:id="@+id/home_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:editTextColor="@color/white"
|
||||
android:gravity="start"
|
||||
android:iconifiedByDefault="true"
|
||||
android:textColor="@color/white"
|
||||
android:textColorHint="@color/white"
|
||||
app:iconifiedByDefault="true"
|
||||
app:queryBackground="@color/transparent"
|
||||
app:queryHint="@string/search"
|
||||
app:closeIcon="@drawable/ic_baseline_close_24"
|
||||
|
||||
app:searchIcon="@drawable/search_icon"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
</LinearLayout>
|
||||
|
||||
<!--
|
||||
<TextView
|
||||
android:visibility="gone"
|
||||
android:id="@+id/test_search"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:layout_gravity="start"
|
||||
android:gravity="center"
|
||||
|
||||
android:textSize="20dp"
|
||||
android:layout_margin="20dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/search"
|
||||
android:textColor="@color/white"
|
||||
app:drawableLeftCompat="@drawable/search_icon"
|
||||
app:tint="@color/white" />
|
||||
-->
|
||||
|
||||
<LinearLayout
|
||||
|
||||
android:id="@+id/home_preview_title_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/home_preview_bookmark"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="25dp"
|
||||
android:layout_marginEnd="25dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:text="@string/none"
|
||||
android:textColor="?attr/white"
|
||||
app:drawableTint="?attr/white"
|
||||
app:drawableTopCompat="@drawable/ic_baseline_add_24"
|
||||
app:tint="?attr/white" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/home_preview_play"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
|
||||
android:text="@string/home_play"
|
||||
app:icon="@drawable/ic_baseline_play_arrow_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/home_preview_info"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="25dp"
|
||||
android:layout_marginEnd="25dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:text="@string/home_info"
|
||||
android:textColor="?attr/white"
|
||||
app:drawableTint="?attr/white"
|
||||
app:drawableTopCompat="@drawable/ic_outline_info_24"
|
||||
app:tint="?attr/white" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/home_fix_padding"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"></View>
|
||||
<!--
|
||||
All padding in home_watch_holder is determined in runtime
|
||||
This is because the home poster can be invisible which forces
|
||||
us to take the status bar space into account
|
||||
-->
|
||||
<LinearLayout
|
||||
android:id="@+id/home_watch_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/home_watch_child_more_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusDown="@id/home_watch_child_recyclerview"
|
||||
android:padding="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/home_watch_parent_item_title"
|
||||
style="@style/WatchHeaderText"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/continue_watching" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:contentDescription="@string/home_more_info"
|
||||
android:src="@drawable/ic_baseline_arrow_forward_24"
|
||||
app:tint="?attr/textColor" />
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/home_watch_child_recyclerview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="5dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/home_result_grid" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/home_bookmarked_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/home_bookmarked_child_more_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusUp="@id/home_watch_child_recyclerview"
|
||||
android:nextFocusForward="@id/home_bookmarked_child_recyclerview"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="5dp">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="50dp"
|
||||
android:fadingEdge="horizontal"
|
||||
android:requiresFadingEdge="horizontal">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:singleSelection="true">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_type_watching_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusRight="@id/home_plan_to_watch_btt"
|
||||
android:text="@string/type_watching" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_plan_to_watch_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:nextFocusLeft="@id/home_type_watching_btt"
|
||||
android:nextFocusRight="@id/home_type_on_hold_btt"
|
||||
android:text="@string/type_plan_to_watch" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_type_on_hold_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:nextFocusLeft="@id/home_plan_to_watch_btt"
|
||||
android:nextFocusRight="@id/home_type_dropped_btt"
|
||||
android:text="@string/type_on_hold" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_type_dropped_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:nextFocusLeft="@id/home_type_on_hold_btt"
|
||||
android:nextFocusRight="@id/home_type_completed_btt"
|
||||
android:text="@string/type_dropped" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_type_completed_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
android:layout_height="wrap_content"
|
||||
android:nextFocusLeft="@id/home_type_dropped_btt"
|
||||
android:text="@string/type_completed" />
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
</HorizontalScrollView>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:contentDescription="@string/home_more_info"
|
||||
android:src="@drawable/ic_baseline_arrow_forward_24"
|
||||
app:tint="?attr/textColor" />
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/home_bookmarked_child_recyclerview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:clipToPadding="false"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="5dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/home_result_grid" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/home_master_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
tools:listitem="@layout/homepage_parent" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
android:layout_height="wrap_content"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/homepage_parent" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/home_api_fab"
|
||||
|
|
|
@ -0,0 +1,287 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/home_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:id="@+id/home_none_padding"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/home_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="500dp">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/home_preview_viewpager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
</androidx.viewpager2.widget.ViewPager2>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/home_preview_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0.8"
|
||||
android:scaleType="centerCrop"
|
||||
android:visibility="gone"
|
||||
tools:src="@drawable/example_poster" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/home_padding"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.SearchView
|
||||
android:id="@+id/home_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:layout_gravity="start"
|
||||
android:editTextColor="@color/white"
|
||||
android:gravity="start"
|
||||
android:iconifiedByDefault="true"
|
||||
android:textColor="@color/white"
|
||||
android:textColorHint="@color/white"
|
||||
app:closeIcon="@drawable/ic_baseline_close_24"
|
||||
app:iconifiedByDefault="true"
|
||||
app:queryBackground="@color/transparent"
|
||||
app:queryHint="@string/search_hint"
|
||||
app:searchIcon="@drawable/search_icon"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
</LinearLayout>
|
||||
|
||||
<!--
|
||||
<TextView
|
||||
android:visibility="gone"
|
||||
android:id="@+id/test_search"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:layout_gravity="start"
|
||||
android:gravity="center"
|
||||
|
||||
android:textSize="20dp"
|
||||
android:layout_margin="20dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/search"
|
||||
android:textColor="@color/white"
|
||||
app:drawableLeftCompat="@drawable/search_icon"
|
||||
app:tint="@color/white" />
|
||||
-->
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/home_preview_title_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/home_preview_bookmark"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="25dp"
|
||||
android:layout_marginEnd="25dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:text="@string/none"
|
||||
android:textColor="?attr/white"
|
||||
app:drawableTint="?attr/white"
|
||||
app:drawableTopCompat="@drawable/ic_baseline_add_24"
|
||||
app:tint="?attr/white" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/home_preview_play"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
|
||||
android:text="@string/home_play"
|
||||
app:icon="@drawable/ic_baseline_play_arrow_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/home_preview_info"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="25dp"
|
||||
android:layout_marginEnd="25dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:text="@string/home_info"
|
||||
android:textColor="?attr/white"
|
||||
app:drawableTint="?attr/white"
|
||||
app:drawableTopCompat="@drawable/ic_outline_info_24"
|
||||
app:tint="?attr/white" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/home_watch_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/home_watch_parent_item_title"
|
||||
|
||||
style="@style/WatchHeaderText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:padding="12dp"
|
||||
android:text="@string/continue_watching"
|
||||
app:drawableRightCompat="@drawable/ic_baseline_arrow_forward_24"
|
||||
app:drawableTint="?attr/white"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:contentDescription="@string/home_more_info"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/home_watch_child_recyclerview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:orientation="horizontal"
|
||||
android:paddingEnd="5dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/home_result_grid" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/home_bookmarked_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/home_bookmark_parent_item_title"
|
||||
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="50dp"
|
||||
android:fadingEdge="horizontal"
|
||||
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusUp="@id/home_watch_child_recyclerview"
|
||||
android:nextFocusForward="@id/home_bookmarked_child_recyclerview"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingEnd="12dp"
|
||||
|
||||
android:paddingBottom="5dp"
|
||||
android:requiresFadingEdge="horizontal">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_type_watching_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusRight="@id/home_plan_to_watch_btt"
|
||||
android:text="@string/type_watching" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_plan_to_watch_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:nextFocusLeft="@id/home_type_watching_btt"
|
||||
android:nextFocusRight="@id/home_type_on_hold_btt"
|
||||
android:text="@string/type_plan_to_watch" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_type_on_hold_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:nextFocusLeft="@id/home_plan_to_watch_btt"
|
||||
android:nextFocusRight="@id/home_type_dropped_btt"
|
||||
android:text="@string/type_on_hold" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_type_dropped_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:nextFocusLeft="@id/home_type_on_hold_btt"
|
||||
android:nextFocusRight="@id/home_type_completed_btt"
|
||||
android:text="@string/type_dropped" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_type_completed_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
android:layout_height="wrap_content"
|
||||
android:nextFocusLeft="@id/home_type_dropped_btt"
|
||||
android:text="@string/type_completed" />
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
<ImageView
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_gravity="end"
|
||||
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:src="@drawable/ic_baseline_arrow_forward_24"
|
||||
app:drawableTint="?attr/white"
|
||||
android:contentDescription="@string/home_more_info"/>
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/home_bookmarked_child_recyclerview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:orientation="horizontal"
|
||||
android:paddingEnd="5dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/home_result_grid" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
|
@ -0,0 +1,270 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout android:orientation="vertical"
|
||||
android:id="@+id/home_header"
|
||||
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<View
|
||||
android:id="@+id/home_none_padding"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/home_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
android:id="@+id/home_preview_viewpager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="400dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
</androidx.viewpager2.widget.ViewPager2>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="10dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/home_preview_change_api"
|
||||
style="@style/BlackButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="top|start"
|
||||
android:layout_marginStart="@dimen/navbar_width"
|
||||
android:backgroundTint="@color/semiWhite"
|
||||
android:minWidth="150dp" />
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginStart="@dimen/navbar_width"
|
||||
android:gravity="bottom"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp">
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/home_preview_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textSize="25sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="The Perfect Run" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/home_preview_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="5"
|
||||
android:paddingBottom="5dp"
|
||||
android:textSize="15sp"
|
||||
tools:text="very nice tv series" />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/home_preview_tags"
|
||||
style="@style/ChipParent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp">
|
||||
|
||||
<View
|
||||
android:id="@+id/home_preview_hidden_prev_focus"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="1dp"
|
||||
android:focusable="false" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/home_preview_play_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_margin="0dp"
|
||||
android:minWidth="150dp"
|
||||
android:nextFocusLeft="@id/home_preview_hidden_prev_focus"
|
||||
android:nextFocusRight="@id/home_preview_info_btt"
|
||||
android:nextFocusUp="@id/home_preview_change_api"
|
||||
android:nextFocusDown="@id/home_watch_parent_item_title"
|
||||
android:text="@string/home_play"
|
||||
app:icon="@drawable/ic_baseline_play_arrow_24" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/home_preview_info_btt"
|
||||
style="@style/BlackButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:minWidth="150dp"
|
||||
|
||||
android:nextFocusLeft="@id/home_preview_play_btt"
|
||||
android:nextFocusRight="@id/home_preview_hidden_next_focus"
|
||||
android:nextFocusUp="@id/home_preview_change_api"
|
||||
android:nextFocusDown="@id/home_watch_parent_item_title"
|
||||
android:text="@string/home_info"
|
||||
app:icon="@drawable/ic_outline_info_24" />
|
||||
|
||||
<View
|
||||
android:id="@+id/home_preview_hidden_next_focus"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="1dp"
|
||||
android:focusable="false" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="10dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/home_preview_change_api2"
|
||||
style="@style/BlackButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="top|start"
|
||||
android:layout_marginStart="@dimen/navbar_width"
|
||||
android:backgroundTint="@color/semiWhite"
|
||||
android:minWidth="150dp" />
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/home_watch_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/home_watch_parent_item_title"
|
||||
|
||||
style="@style/WatchHeaderText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/navbar_width"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:padding="12dp"
|
||||
android:text="@string/continue_watching" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/home_watch_child_recyclerview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="@dimen/navbar_width"
|
||||
android:paddingEnd="5dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/home_result_grid" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/home_bookmarked_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fadingEdge="horizontal"
|
||||
|
||||
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusUp="@id/home_watch_child_recyclerview"
|
||||
android:nextFocusForward="@id/home_bookmarked_child_recyclerview"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingEnd="12dp"
|
||||
|
||||
android:paddingBottom="5dp"
|
||||
android:requiresFadingEdge="horizontal">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/navbar_width"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_type_watching_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusRight="@id/home_plan_to_watch_btt"
|
||||
android:text="@string/type_watching" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_plan_to_watch_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:nextFocusLeft="@id/home_type_watching_btt"
|
||||
android:nextFocusRight="@id/home_type_on_hold_btt"
|
||||
android:text="@string/type_plan_to_watch" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_type_on_hold_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:nextFocusLeft="@id/home_plan_to_watch_btt"
|
||||
android:nextFocusRight="@id/home_type_dropped_btt"
|
||||
android:text="@string/type_on_hold" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_type_dropped_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:nextFocusLeft="@id/home_type_on_hold_btt"
|
||||
android:nextFocusRight="@id/home_type_completed_btt"
|
||||
android:text="@string/type_dropped" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_type_completed_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
android:layout_height="wrap_content"
|
||||
android:nextFocusLeft="@id/home_type_dropped_btt"
|
||||
android:text="@string/type_completed" />
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
</HorizontalScrollView>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/home_bookmarked_child_recyclerview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="@dimen/navbar_width"
|
||||
android:paddingEnd="5dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/home_result_grid" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
|
@ -1,87 +1,88 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/home_root"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
android:id="@+id/home_root"
|
||||
tools:context=".ui.home.HomeFragment">
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.home.HomeFragment">
|
||||
|
||||
<FrameLayout
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone"
|
||||
android:id="@+id/home_loading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:id="@+id/home_loading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="@dimen/navbar_width"
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone">
|
||||
|
||||
<ProgressBar
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp" />
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<com.facebook.shimmer.ShimmerFrameLayout
|
||||
android:id="@+id/home_loading_shimmer"
|
||||
app:shimmer_base_alpha="0.2"
|
||||
app:shimmer_highlight_alpha="0.3"
|
||||
app:shimmer_duration="@integer/loading_time"
|
||||
app:shimmer_auto_start="true"
|
||||
android:paddingTop="40dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="15dp"
|
||||
android:orientation="vertical">
|
||||
android:id="@+id/home_loading_shimmer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="15dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="40dp"
|
||||
app:shimmer_auto_start="true"
|
||||
app:shimmer_base_alpha="0.2"
|
||||
app:shimmer_duration="@integer/loading_time"
|
||||
app:shimmer_highlight_alpha="0.3">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_margin="@dimen/loading_margin"
|
||||
android:layout_gravity="center"
|
||||
app:cardCornerRadius="@dimen/loading_radius"
|
||||
android:background="@color/grayShimmer"
|
||||
android:translationX="-164dp"
|
||||
android:layout_width="125dp"
|
||||
android:layout_height="200dp" />
|
||||
android:layout_width="125dp"
|
||||
android:layout_height="200dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="@dimen/loading_margin"
|
||||
android:background="@color/grayShimmer"
|
||||
android:translationX="-164dp"
|
||||
app:cardCornerRadius="@dimen/loading_radius" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_margin="@dimen/loading_margin"
|
||||
android:layout_gravity="center"
|
||||
app:cardCornerRadius="@dimen/loading_radius"
|
||||
android:background="@color/grayShimmer"
|
||||
android:layout_width="148dp"
|
||||
android:layout_height="234dp" />
|
||||
android:layout_width="148dp"
|
||||
android:layout_height="234dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="@dimen/loading_margin"
|
||||
android:background="@color/grayShimmer"
|
||||
app:cardCornerRadius="@dimen/loading_radius" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_margin="@dimen/loading_margin"
|
||||
android:layout_gravity="center"
|
||||
app:cardCornerRadius="@dimen/loading_radius"
|
||||
android:background="@color/grayShimmer"
|
||||
android:translationX="164dp"
|
||||
android:layout_width="125dp"
|
||||
android:layout_height="200dp" />
|
||||
android:layout_width="125dp"
|
||||
android:layout_height="200dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="@dimen/loading_margin"
|
||||
android:background="@color/grayShimmer"
|
||||
android:translationX="164dp"
|
||||
app:cardCornerRadius="@dimen/loading_radius" />
|
||||
</FrameLayout>
|
||||
|
||||
<include layout="@layout/loading_line_short_center" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="@dimen/result_padding"
|
||||
android:layout_marginStart="@dimen/result_padding"
|
||||
android:layout_marginEnd="@dimen/result_padding"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/result_padding"
|
||||
android:layout_marginTop="@dimen/result_padding"
|
||||
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_marginEnd="@dimen/result_padding"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include layout="@layout/loading_list" />
|
||||
|
||||
|
@ -93,355 +94,82 @@
|
|||
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/home_loading_statusbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="70dp">
|
||||
android:id="@+id/home_loading_statusbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="70dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/home_change_api_loading"
|
||||
android:layout_margin="10dp"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:id="@+id/home_change_api_loading"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
|
||||
android:src="@drawable/ic_baseline_keyboard_arrow_down_24"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:contentDescription="@string/home_change_provider_img_des" />
|
||||
android:layout_margin="10dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/home_change_provider_img_des"
|
||||
android:src="@drawable/ic_baseline_keyboard_arrow_down_24" />
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone"
|
||||
android:id="@+id/home_loading_error"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/home_loading_error"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="@dimen/navbar_width"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/home_reload_connectionerror"
|
||||
style="@style/WhiteButton"
|
||||
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_margin="5dp"
|
||||
android:minWidth="200dp"
|
||||
android:text="@string/reload_error"
|
||||
app:icon="@drawable/ic_baseline_autorenew_24" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:layout_gravity="center"
|
||||
style="@style/WhiteButton"
|
||||
android:id="@+id/home_reload_connection_open_in_browser"
|
||||
style="@style/BlackButton"
|
||||
|
||||
android:layout_margin="5dp"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
app:icon="@drawable/ic_baseline_autorenew_24"
|
||||
android:text="@string/reload_error"
|
||||
android:id="@+id/home_reload_connectionerror"
|
||||
android:layout_width="wrap_content"
|
||||
android:minWidth="200dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:layout_gravity="center"
|
||||
style="@style/BlackButton"
|
||||
|
||||
android:layout_margin="5dp"
|
||||
|
||||
app:icon="@drawable/ic_baseline_public_24"
|
||||
android:text="@string/result_open_in_browser"
|
||||
android:id="@+id/home_reload_connection_open_in_browser"
|
||||
android:layout_width="wrap_content"
|
||||
android:minWidth="200dp" />
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="5dp"
|
||||
android:minWidth="200dp"
|
||||
android:text="@string/result_open_in_browser"
|
||||
app:icon="@drawable/ic_baseline_public_24" />
|
||||
|
||||
<TextView
|
||||
android:layout_margin="5dp"
|
||||
android:gravity="center"
|
||||
android:layout_gravity="center"
|
||||
android:id="@+id/result_error_text"
|
||||
android:textColor="?attr/textColor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
android:id="@+id/result_error_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="5dp"
|
||||
android:gravity="center"
|
||||
android:textColor="?attr/textColor" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
tools:visibility="visible"
|
||||
android:visibility="gone"
|
||||
android:id="@+id/home_loaded"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<View
|
||||
android:background="?attr/primaryGrayBackground"
|
||||
android:id="@+id/home_statusbar"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/example_poster"
|
||||
android:id="@+id/home_blur_poster"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/shadow_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
android:background="@drawable/background_shadow" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_marginTop="100dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp">
|
||||
|
||||
<!--
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/home_profile_picture_holder"
|
||||
android:layout_marginEnd="20dp"
|
||||
app:cardCornerRadius="100dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_width="35dp"
|
||||
android:layout_height="35dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/home_profile_picture"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="ContentDescription" />
|
||||
</androidx.cardview.widget.CardView>-->
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:nextFocusDown="@id/home_main_poster_recyclerview"
|
||||
android:nextFocusUp="@id/nav_rail_view"
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:layout_gravity="center_vertical"
|
||||
|
||||
android:id="@+id/home_change_api"
|
||||
android:layout_margin="10dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:foreground="@drawable/outline_drawable"
|
||||
android:src="@drawable/ic_baseline_filter_list_24"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:contentDescription="@string/home_change_provider_img_des">
|
||||
|
||||
<requestFocus />
|
||||
</ImageView>
|
||||
|
||||
<TextView
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:textStyle="bold"
|
||||
android:textSize="25sp"
|
||||
tools:text="The Perfect Run"
|
||||
android:id="@+id/home_focus_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_marginStart="12dp"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:gravity="center_vertical"
|
||||
android:id="@+id/home_provider_name"
|
||||
android:textColor="?attr/grayTextColor"
|
||||
android:textSize="20sp"
|
||||
android:paddingEnd="10dp"
|
||||
tools:text="Hello World"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:gravity="center_vertical"
|
||||
android:id="@+id/home_provider_meta_info"
|
||||
android:textColor="?attr/grayTextColor"
|
||||
android:textSize="20sp"
|
||||
tools:text="Hello World"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:paddingHorizontal="5dp"
|
||||
android:clipToPadding="false"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
android:id="@+id/home_main_poster_recyclerview"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:listitem="@layout/home_result_grid" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/home_watch_holder"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/home_watch_parent_item_title"
|
||||
android:padding="12dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/WatchHeaderText"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:text="@string/continue_watching">
|
||||
</TextView>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:paddingHorizontal="5dp"
|
||||
android:clipToPadding="false"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
android:id="@+id/home_watch_child_recyclerview"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:listitem="@layout/home_result_grid" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/home_bookmarked_holder"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusUp="@id/home_watch_child_recyclerview"
|
||||
android:nextFocusForward="@id/home_bookmarked_child_recyclerview"
|
||||
|
||||
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:fadingEdge="horizontal"
|
||||
android:requiresFadingEdge="horizontal">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_type_watching_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusRight="@id/home_plan_to_watch_btt"
|
||||
android:text="@string/type_watching" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_plan_to_watch_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
android:nextFocusLeft="@id/home_type_watching_btt"
|
||||
android:nextFocusRight="@id/home_type_on_hold_btt"
|
||||
android:text="@string/type_plan_to_watch" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_type_on_hold_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
android:nextFocusLeft="@id/home_plan_to_watch_btt"
|
||||
android:nextFocusRight="@id/home_type_dropped_btt"
|
||||
android:text="@string/type_on_hold" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_type_dropped_btt"
|
||||
style="@style/ChipFilled"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
android:nextFocusLeft="@id/home_type_on_hold_btt"
|
||||
android:nextFocusRight="@id/home_type_completed_btt"
|
||||
android:text="@string/type_dropped" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/home_type_completed_btt"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
style="@style/ChipFilled"
|
||||
android:nextFocusLeft="@id/home_type_dropped_btt"
|
||||
android:text="@string/type_completed" />
|
||||
</com.google.android.material.chip.ChipGroup> </HorizontalScrollView>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:paddingHorizontal="5dp"
|
||||
android:clipToPadding="false"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
android:id="@+id/home_bookmarked_child_recyclerview"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:listitem="@layout/home_result_grid" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:id="@+id/home_master_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:listitem="@layout/homepage_parent_tv" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/home_master_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/homepage_parent_tv" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:id="@+id/home_api_fab"
|
||||
app:icon="@drawable/ic_baseline_filter_list_24"
|
||||
style="@style/ExtendedFloatingActionButton"
|
||||
tools:ignore="ContentDescription" />
|
||||
android:id="@+id/home_api_fab"
|
||||
style="@style/ExtendedFloatingActionButton"
|
||||
android:visibility="gone"
|
||||
app:icon="@drawable/ic_baseline_filter_list_24"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:visibility="visible" />
|
||||
</FrameLayout>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue