Merge branch 'master' into feature/remote-sync

This commit is contained in:
CranberrySoup 2023-10-31 21:12:12 +00:00 committed by GitHub
commit 3bd7b95350
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
188 changed files with 4833 additions and 1439 deletions

View file

@ -61,16 +61,18 @@ android {
}
}
compileSdk = 33
compileSdk = 34
buildToolsVersion = "34.0.0"
defaultConfig {
applicationId = "com.lagradost.cloudstream3"
minSdk = 21
targetSdk = 33
// https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading
targetSdk = 33 // android 14 is fucked
versionCode = 62
versionName = "4.2.0"
versionName = "4.2.1"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
@ -175,28 +177,30 @@ repositories {
dependencies {
implementation("com.google.android.mediahome:video:1.0.0")
implementation("androidx.test.ext:junit-ktx:1.1.5")
testImplementation("org.json:json:20180813")
testImplementation("org.json:json:20230618")
implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.appcompat:appcompat:1.6.1") // need target 32 for 1.5.0
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
// dont change this to 1.6.0 it looks ugly af
implementation("com.google.android.material:material:1.5.0")
implementation("com.google.android.material:material:1.10.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.navigation:navigation-fragment-ktx:2.6.0")
implementation("androidx.navigation:navigation-ui-ktx:2.6.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.4")
implementation("androidx.navigation:navigation-ui-ktx:2.7.4")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test:core")
//implementation("io.karn:khttp-android:0.1.2") //okhttp instead
// implementation("io.karn:khttp-android:0.1.2") //okhttp instead
// implementation("org.jsoup:jsoup:1.13.1")
// DONT UPDATE, WILL CRASH ANDROID TV ????
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
implementation("androidx.preference:preference-ktx:1.2.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("com.github.bumptech.glide:glide:4.13.1")
kapt("com.github.bumptech.glide:compiler:4.13.1")
@ -220,28 +224,26 @@ dependencies {
// Custom ffmpeg extension for audio codecs
implementation("com.github.recloudstream:media-ffmpeg:1.1.0")
//implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
// Bug reports
implementation("ch.acra:acra-core:5.11.0")
implementation("ch.acra:acra-toast:5.11.0")
implementation("ch.acra:acra-core:5.11.2")
implementation("ch.acra:acra-toast:5.11.2")
compileOnly("com.google.auto.service:auto-service-annotations:1.0")
compileOnly("com.google.auto.service:auto-service-annotations:1.1.1")
//either for java sources:
annotationProcessor("com.google.auto.service:auto-service:1.0")
annotationProcessor("com.google.auto.service:auto-service:1.1.1")
//or for kotlin sources (requires kapt gradle plugin):
kapt("com.google.auto.service:auto-service:1.0")
kapt("com.google.auto.service:auto-service:1.1.1")
// subtitle color picker
implementation("com.jaredrummler:colorpicker:1.1.0")
//run JS
// run JS
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
// available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
implementation("org.mozilla:rhino:1.7.13")
// TorrentStream
//implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
// implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
// Downloading
implementation("androidx.work:work-runtime:2.8.1")
@ -250,18 +252,18 @@ dependencies {
// Networking
// implementation("com.squareup.okhttp3:okhttp:4.9.2")
// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1")
implementation("com.github.Blatzar:NiceHttp:0.4.3")
implementation("com.github.Blatzar:NiceHttp:0.4.4") // http library
// To fix SSL fuckery on android 9
implementation("org.conscrypt:conscrypt-android:2.2.1")
implementation("org.conscrypt:conscrypt-android:2.5.2")
// Util to skip the URI file fuckery 🙏
implementation("com.github.LagradOst:SafeFile:0.0.5")
// API because cba maintaining it myself
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.10.0")
implementation("com.github.discord:OverlappingPanels:0.1.5")
// debugImplementation because LeakCanary should only run in debug builds.
//debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
// for shimmer when loading
implementation("com.facebook.shimmer:shimmer:0.5.0")
@ -273,7 +275,7 @@ dependencies {
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipeExtractor/commits/dev
// this should be updated frequently to avoid trailer fu*kery
implementation("com.github.teamnewpipe:NewPipeExtractor:1f08d28")
implementation("com.github.teamnewpipe:NewPipeExtractor:917554a")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
// Library/extensions searching with Levenshtein distance
@ -298,6 +300,8 @@ dependencies {
group = "org.apache.httpcomponents",
)
}
// seekbar https://github.com/rubensousa/PreviewSeekBar
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0")
}

View file

@ -9,6 +9,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
import com.lagradost.cloudstream3.databinding.FragmentLibraryTvBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding
import com.lagradost.cloudstream3.databinding.FragmentResultBinding
@ -17,6 +19,7 @@ import com.lagradost.cloudstream3.databinding.FragmentSearchBinding
import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
import com.lagradost.cloudstream3.databinding.HomepageParentEmulatorBinding
import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutTvBinding
@ -117,9 +120,12 @@ class ExampleInstrumentedTest {
// testAllLayouts<HomeScrollViewBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
// testAllLayouts<HomeScrollViewTvBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
testAllLayouts<HomepageParentEmulatorBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
testAllLayouts<FragmentLibraryTvBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
testAllLayouts<FragmentLibraryBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
}
}
}

View file

@ -15,6 +15,8 @@
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!-- Required for getting arbitrary Aniyomi packages -->
<!-- Fixes android tv fuckery -->
<uses-feature
@ -61,7 +63,9 @@
android:exported="true"
android:resizeableActivity="true"
android:screenOrientation="userLandscape"
android:supportsPictureInPicture="true">
android:supportsPictureInPicture="true"
android:taskAffinity="com.lagradost.cloudstream3.downloadedplayer"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -92,12 +96,6 @@
android:launchMode="singleTask"
android:resizeableActivity="true"
android:supportsPictureInPicture="true">
<intent-filter android:exported="true">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<!-- cloudstreamplayer://encodedUrl?name=Dune -->
<intent-filter>
@ -172,6 +170,22 @@
</intent-filter>
</activity>
<activity
android:name=".ui.account.AccountSelectActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
android:exported="true"
android:resizeableActivity="true">
<intent-filter android:exported="true">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ui.EasterEggMonke"
android:exported="true" />
@ -186,6 +200,7 @@
</receiver>
<service
android:foregroundServiceType="dataSync"
android:name=".services.VideoDownloadService"
android:enabled="true"
android:exported="false" />
@ -195,6 +210,7 @@
android:exported="false" />
<service
android:foregroundServiceType="dataSync"
android:name=".utils.PackageInstallerService"
android:exported="false" />

View file

@ -65,6 +65,11 @@ object CommonActivity {
_activity = WeakReference(value)
}
@MainThread
fun setActivityInstance(newActivity: Activity?) {
activity = newActivity
}
@MainThread
fun Activity?.getCastSession(): CastSession? {
return (this as MainActivity?)?.mSessionManager?.currentCastSession
@ -203,23 +208,25 @@ object CommonActivity {
setLocale(this, localeCode)
}
fun init(act: ComponentActivity?) {
if (act == null) return
activity = act
fun init(act: Activity) {
setActivityInstance(act)
val componentActivity = activity as? ComponentActivity ?: return
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
//https://developer.android.com/guide/topics/ui/picture-in-picture
canShowPipMode =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT
act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
componentActivity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
componentActivity.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
act.updateLocale()
act.updateTv()
componentActivity.updateLocale()
componentActivity.updateTv()
NewPipe.init(DownloaderTestImpl.getInstance())
for (resumeApp in resumeApps) {
resumeApp.launcher =
act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
@ -236,11 +243,11 @@ object CommonActivity {
// Ask for notification permissions on Android 13
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(
act,
componentActivity,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
val requestPermissionLauncher = act.registerForActivityResult(
val requestPermissionLauncher = componentActivity.registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
Log.d(TAG, "Notification permission: $isGranted")
@ -295,12 +302,15 @@ object CommonActivity {
val currentOverlayTheme =
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
"Normal" -> R.style.OverlayPrimaryColorNormal
"DandelionYellow" -> R.style.OverlayPrimaryColorDandelionYellow
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
"Orange" -> R.style.OverlayPrimaryColorOrange
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
"Maroon" -> R.style.OverlayPrimaryColorMaroon
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
"Grey" -> R.style.OverlayPrimaryColorGrey
"White" -> R.style.OverlayPrimaryColorWhite
"CoolBlue" -> R.style.OverlayPrimaryColorCoolBlue
"Brown" -> R.style.OverlayPrimaryColorBrown
"Purple" -> R.style.OverlayPrimaryColorPurple
"Green" -> R.style.OverlayPrimaryColorGreen

View file

@ -1246,6 +1246,18 @@ interface LoadResponse {
return this.syncData[aniListIdPrefix]
}
fun LoadResponse.getImdbId(): String? {
return normalSafeApiCall {
SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Imdb)
}
}
fun LoadResponse.getTMDbId(): String? {
return normalSafeApiCall {
SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Tmdb)
}
}
fun LoadResponse.addMalId(id: Int?) {
this.syncData[malIdPrefix] = (id ?: return).toString()
this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString())
@ -1453,6 +1465,15 @@ interface EpisodeResponse {
var nextAiring: NextAiring?
var seasonNames: List<SeasonData>?
fun getLatestEpisodes(): Map<DubStatus, Int?>
/** Count all episodes in all previous seasons up until this episode to get a total count.
* Example:
* Season 1: 10 episodes.
* Season 2: 6 episodes.
*
* getTotalEpisodeIndex(episode = 3, season = 2) -> 10 + 3 = 13
* */
fun getTotalEpisodeIndex(episode: Int, season: Int): Int
}
@JvmName("addSeasonNamesString")
@ -1532,6 +1553,12 @@ data class AnimeLoadResponse(
.takeUnless { it == Int.MIN_VALUE }
}.toMap()
}
override fun getTotalEpisodeIndex(episode: Int, season: Int): Int {
return this.episodes.maxOf { (_, episodes) ->
episodes.count { ((it.season ?: Int.MIN_VALUE) < season) && it.season != 0 }
} + episode
}
}
/**
@ -1740,6 +1767,12 @@ data class TvSeriesLoadResponse(
.takeUnless { it == Int.MIN_VALUE }
return mapOf(DubStatus.None to max)
}
override fun getTotalEpisodeIndex(episode: Int, season: Int): Int {
return episodes.count {
(it.season ?: Int.MIN_VALUE) < season && it.season != 0
} + episode
}
}
suspend fun MainAPI.newTvSeriesLoadResponse(

View file

@ -67,6 +67,7 @@ import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
import com.lagradost.cloudstream3.CommonActivity.screenHeight
import com.lagradost.cloudstream3.CommonActivity.setActivityInstance
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale
import com.lagradost.cloudstream3.databinding.ActivityMainBinding
@ -283,6 +284,7 @@ var app = Requests(responseParser = object : ResponseParser {
class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
companion object {
const val TAG = "MAINACT"
const val ANIMATED_OUTLINE : Boolean = false
var lastError: String? = null
/**
@ -544,13 +546,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
navRailView.isVisible = isNavVisible && landscape
// Hide library on TV since it is not supported yet :(
val isTrueTv = isTrueTvSettings()
navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
//val isTrueTv = isTrueTvSettings()
//navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
//navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
// Hide downloads on TV
navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv
navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv
//navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv
//navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv
}
}
@ -592,6 +594,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
override fun onResume() {
super.onResume()
afterPluginsLoadedEvent += ::onAllPluginsLoaded
setActivityInstance(this)
try {
if (isCastApiAvailable()) {
//mCastSession = mSessionManager.currentCastSession
@ -661,7 +664,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val isAtHome =
navController?.currentDestination?.matchDestination(R.id.navigation_home) == true
if (isAtHome && isTrueTvSettings()) {
if (isAtHome && isTvSettings()) {
showConfirmExitDialog()
} else {
super.onBackPressed()
@ -1072,7 +1075,22 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
private fun centerView(view : View?) {
if(view == null) return
try {
Log.v(TAG, "centerView: $view")
val r = Rect(0, 0, 0, 0)
view.getDrawingRect(r)
val x = r.centerX()
val y = r.centerY()
val dx = r.width() / 2 //screenWidth / 2
val dy = screenHeight / 2
val r2 = Rect(x - dx, y - dy, x + dx, y + dy)
view.requestRectangleOnScreen(r2, false)
// TvFocus.current =TvFocus.current.copy(y=y.toFloat())
} catch (_: Throwable) {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
app.initClient(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
@ -1112,7 +1130,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (appVer != lastAppAutoBackup) {
setKey("VERSION_NAME", BuildConfig.VERSION_NAME)
normalSafeApiCall {
backup()
backup(this)
}
normalSafeApiCall {
// Recompile oat on new version
@ -1126,38 +1144,27 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (isTvSettings()) {
val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false)
setContentView(newLocalBinding.root)
TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
// println("refocus $oldFocus -> $newFocus")
try {
val r = Rect(0, 0, 0, 0)
newFocus.getDrawingRect(r)
val x = r.centerX()
val y = r.centerY()
val dx = 0 //screenWidth / 2
val dy = screenHeight / 2
val r2 = Rect(x - dx, y - dy, x + dx, y + dy)
newFocus.requestRectangleOnScreen(r2, false)
// TvFocus.current =TvFocus.current.copy(y=y.toFloat())
} catch (_: Throwable) {
}
TvFocus.updateFocusView(newFocus)
/*var focus = newFocus
while(focus != null) {
if(focus is ScrollingView && focus.canScrollVertically()) {
focus.scrollBy()
}
when(focus.parent) {
is View -> focus = newFocus
else -> break
}
}*/
if(isTrueTvSettings() && ANIMATED_OUTLINE) {
TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
}
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
TvFocus.updateFocusView(newFocus)
}
} else {
newLocalBinding.focusOutline.isVisible = false
}
newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
if(isTrueTvSettings()) {
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
centerView(newFocus)
}
}
ActivityMainBinding.bind(newLocalBinding.root) // this may crash
} else {
val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false)
@ -1303,7 +1310,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
this@MainActivity.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
viewModel.updateWatchStatus(WatchType.values()[it])
viewModel.updateWatchStatus(WatchType.values()[it], this@MainActivity)
}
}

View file

@ -29,7 +29,7 @@ open class Chillx : ExtractorApi() {
override val requiresReferer = true
companion object {
private const val KEY = "m4H6D9%0\$N&F6rQ&"
private const val KEY = "eN0^>\$^#M08uFv%c"
}
override suspend fun getUrl(

View file

@ -7,21 +7,12 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
class SpeedoStream2 : SpeedoStream() {
override val mainUrl = "https://speedostream.mom"
}
open class Minoplres : ExtractorApi() {
class SpeedoStream1 : SpeedoStream() {
override val mainUrl = "https://speedostream.pm"
}
open class SpeedoStream : ExtractorApi() {
override val name = "SpeedoStream"
override val mainUrl = "https://speedostream.bond"
override val name = "Minoplres" // formerly SpeedoStream
override val requiresReferer = true
// .bond, .pm, .mom redirect to .bond
private val hostUrl = "https://speedostream.bond"
override val mainUrl = "https://minoplres.xyz" // formerly speedostream.bond
private val hostUrl = "https://minoplres.xyz"
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<ExtractorLink>()

View file

@ -1,73 +0,0 @@
package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
import com.lagradost.cloudstream3.utils.SyncUtil
// wont be implemented
class MultiAnimeProvider : MainAPI() {
override var name = "MultiAnime"
override var lang = "en"
override val usesWebView = true
override val supportedTypes = setOf(TvType.Anime)
private val syncApi: SyncAPI = aniListApi
private val syncUtilType by lazy {
when (syncApi) {
is AniListApi -> "anilist"
is MALApi -> "myanimelist"
else -> throw ErrorLoadingException("Invalid Api")
}
}
private val validApis
get() =
synchronized(APIHolder.apis) {
APIHolder.apis.filter {
it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains(
TvType.Anime
)
}
}
private fun filterName(name: String): String {
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
}
override suspend fun search(query: String): List<SearchResponse>? {
return syncApi.search(query)?.map {
AnimeSearchResponse(it.name, it.url, this.name, TvType.Anime, it.posterUrl)
}
}
override suspend fun load(url: String): LoadResponse? {
return syncApi.getResult(url)?.let { res ->
val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).amap { url ->
validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url)
}.filterNotNull()
val type =
if (data.any { it.type == TvType.AnimeMovie }) TvType.AnimeMovie else TvType.Anime
newAnimeLoadResponse(
res.title ?: throw ErrorLoadingException("No Title found"),
url,
type
) {
posterUrl = res.posterUrl
plot = res.synopsis
tags = res.genres
rating = res.publicScore
addTrailer(res.trailers)
addAniListId(res.id.toIntOrNull())
recommendations = res.recommendations
}
}
}
}

View file

@ -477,6 +477,14 @@ object PluginManager {
Log.i(TAG, "Loading plugin: $data")
return try {
/* in case of android 14 then
try {
File(filePath).setReadOnly()
} catch (t : Throwable) {
Log.e(TAG, "Failed to set dex as readonly")
logError(t)
}*/
val loader = PathClassLoader(filePath, context.classLoader)
var manifest: Plugin.Manifest
loader.getResourceAsStream("manifest.json").use { stream ->

View file

@ -0,0 +1,96 @@
package com.lagradost.cloudstream3.services
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel
import com.lagradost.cloudstream3.utils.BackupUtils
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import java.util.concurrent.TimeUnit
const val BACKUP_CHANNEL_ID = "cloudstream3.backups"
const val BACKUP_WORK_NAME = "work_backup"
const val BACKUP_CHANNEL_NAME = "Backups"
const val BACKUP_CHANNEL_DESCRIPTION = "Notifications for background backups"
const val BACKUP_NOTIFICATION_ID = 938712898 // Random unique
class BackupWorkManager(val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
companion object {
fun enqueuePeriodicWork(context: Context?, intervalHours: Long) {
if (context == null) return
if (intervalHours == 0L) {
WorkManager.getInstance(context).cancelUniqueWork(BACKUP_WORK_NAME)
return
}
val constraints = Constraints.Builder()
.setRequiresStorageNotLow(true)
.build()
val periodicSyncDataWork =
PeriodicWorkRequest.Builder(
BackupWorkManager::class.java,
intervalHours,
TimeUnit.HOURS
)
.addTag(BACKUP_WORK_NAME)
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
BACKUP_WORK_NAME,
ExistingPeriodicWorkPolicy.UPDATE,
periodicSyncDataWork
)
// Uncomment below for testing
// val oneTimeBackupWork =
// OneTimeWorkRequest.Builder(BackupWorkManager::class.java)
// .addTag(BACKUP_WORK_NAME)
// .setConstraints(constraints)
// .build()
//
// WorkManager.getInstance(context).enqueue(oneTimeBackupWork)
}
}
private val backupNotificationBuilder =
NotificationCompat.Builder(context, BACKUP_CHANNEL_ID)
.setColorized(true)
.setOnlyAlertOnce(true)
.setSilent(true)
.setAutoCancel(true)
.setContentTitle(context.getString(R.string.pref_category_backup))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
.setSmallIcon(R.drawable.ic_cloudstream_monochrome_big)
override suspend fun doWork(): Result {
context.createNotificationChannel(
BACKUP_CHANNEL_ID,
BACKUP_CHANNEL_NAME,
BACKUP_CHANNEL_DESCRIPTION
)
setForeground(
ForegroundInfo(
BACKUP_NOTIFICATION_ID,
backupNotificationBuilder.build()
)
)
BackupUtils.backup(context)
return Result.success()
}
}

View file

@ -8,7 +8,9 @@ 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.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
@ -69,29 +71,52 @@ class LocalList : SyncAPI {
}?.distinctBy { it.first } ?: return null
val list = ioWork {
watchStatusIds.groupBy {
it.second.stringRes
}.mapValues { group ->
val isTrueTv = isTrueTvSettings()
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
// None is not something to display
it.stringRes to emptyList<SyncAPI.LibraryItem>()
} + mapOf(
R.string.favorites_list_name to emptyList()
) + if (!isTrueTv) {
mapOf(
R.string.subscription_list_name to emptyList()
)
} else {
emptyMap()
}
val watchStatusMap = watchStatusIds.groupBy { it.second.stringRes }.mapValues { group ->
group.value.mapNotNull {
getBookmarkedData(it.first)?.toLibraryItem(it.first.toString())
}
} + mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull {
}
val favoritesMap = mapOf(R.string.favorites_list_name to getAllFavorites().mapNotNull {
it.toLibraryItem()
})
// Don't show subscriptions on TV
val result = if (isTrueTv) {
baseMap + watchStatusMap + favoritesMap
} else {
val subscriptionsMap = mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull {
it.toLibraryItem()
})
baseMap + watchStatusMap + subscriptionsMap + favoritesMap
}
result
}
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
// None is not something to display
it.stringRes to emptyList<SyncAPI.LibraryItem>()
} + mapOf(R.string.subscription_list_name to emptyList())
return SyncAPI.LibraryMetadata(
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
list.map { SyncAPI.LibraryList(txt(it.key), it.value) },
setOf(
ListSorting.AlphabeticalA,
ListSorting.AlphabeticalZ,
// ListSorting.UpdatedNew,
// ListSorting.UpdatedOld,
ListSorting.UpdatedNew,
ListSorting.UpdatedOld,
// ListSorting.RatingHigh,
// ListSorting.RatingLow,
)

View file

@ -203,7 +203,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
}
/** Read the id string to get all other ids */
private fun readIdFromString(idString: String?): Map<SyncServices, String> {
fun readIdFromString(idString: String?): Map<SyncServices, String> {
return tryParseJson(idString) ?: return emptyMap()
}
@ -376,6 +376,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
private var status: Int? = null,
private var addEpisodes: Pair<List<MediaObject.Season>?, List<MediaObject.Season.Episode>?>? = null,
private var removeEpisodes: Pair<List<MediaObject.Season>?, List<MediaObject.Season.Episode>?>? = null,
// Required for knowing if the status should be overwritten
private var onList: Boolean = false
) {
fun interceptor(interceptor: Interceptor) = apply { this.interceptor = interceptor }
fun apiUrl(url: String) = apply { this.url = url }
@ -387,6 +389,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
}
fun status(newStatus: Int?, oldStatus: Int?) = apply {
onList = oldStatus != null
// Only set status if its new
if (newStatus != oldStatus) {
this.status = newStatus
@ -412,6 +415,11 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
// Do not add episodes if there is no change
if (newEpisodes > (oldEpisodes ?: 0)) {
this.addEpisodes = getEpisodes(allEpisodes.take(newEpisodes))
// Set to watching if episodes are added and there is no current status
if (!onList) {
status = SimklListStatusType.Watching.value
}
}
if ((oldEpisodes ?: 0) > newEpisodes) {
this.removeEpisodes = getEpisodes(allEpisodes.drop(newEpisodes))
@ -431,6 +439,28 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
interceptor = interceptor
).isSuccessful
} else {
val statusResponse = status?.let { setStatus ->
val newStatus =
SimklListStatusType.values()
.firstOrNull { it.value == setStatus }?.originalName
?: SimklListStatusType.Watching.originalName!!
app.post(
"${this.url}/sync/add-to-list",
json = StatusRequest(
shows = listOf(
StatusMediaObject(
null,
null,
ids,
newStatus,
)
), movies = emptyList()
),
interceptor = interceptor
).isSuccessful
} ?: true
val episodeRemovalResponse = removeEpisodes?.let { (seasons, episodes) ->
app.post(
"${this.url}/sync/history/remove",
@ -472,28 +502,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
true
}
val statusResponse = status?.let { setStatus ->
val newStatus =
SimklListStatusType.values()
.firstOrNull { it.value == setStatus }?.originalName
?: SimklListStatusType.Watching.originalName!!
app.post(
"${this.url}/sync/add-to-list",
json = StatusRequest(
shows = listOf(
StatusMediaObject(
null,
null,
ids,
newStatus,
)
), movies = emptyList()
),
interceptor = interceptor
).isSuccessful
} ?: true
statusResponse && episodeRemovalResponse && historyResponse
}
}
@ -1051,4 +1059,4 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
return true
}
}
}

View file

@ -55,7 +55,6 @@ class WhoIsWatchingAdapter(
editCallBack = editCallBack,
)
override fun onBindViewHolder(holder: WhoIsWatchingHolder, position: Int) =
holder.bind(currentList.getOrNull(position))
@ -70,10 +69,15 @@ class WhoIsWatchingAdapter(
fun bind(card: DataStoreHelper.Account?) {
when (binding) {
is WhoIsWatchingAccountBinding -> binding.apply {
if(card == null) return@apply
if (card == null) return@apply
outline.isVisible = card.keyIndex == DataStoreHelper.selectedKeyIndex
profileText.text = card.name
profileImageBackground.setImage(card.image)
// Handle the lock indicator
val isLocked = card.lockPin != null
lockIcon.isVisible = isLocked
root.setOnClickListener {
selectCallBack(card)
}

View file

@ -0,0 +1,64 @@
package com.lagradost.cloudstream3.ui.account
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.databinding.AccountListItemBinding
import com.lagradost.cloudstream3.ui.result.setImage
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.DataStoreHelper
class AccountAdapter(
private val accounts: List<DataStoreHelper.Account>,
private val onItemClick: (DataStoreHelper.Account) -> Unit
) : RecyclerView.Adapter<AccountAdapter.AccountViewHolder>() {
inner class AccountViewHolder(private val binding: AccountListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(account: DataStoreHelper.Account) {
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex
binding.accountName.text = account.name
binding.accountImage.setImage(account.image)
binding.lockIcon.isVisible = account.lockPin != null
binding.outline.isVisible = isLastUsedAccount
if (isTvSettings()) {
binding.root.isFocusableInTouchMode = true
if (isLastUsedAccount) {
binding.root.requestFocus()
}
}
binding.root.setOnClickListener {
onItemClick(account)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder {
val binding = AccountListItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
if (isTvSettings()) {
val layoutParams = binding.root.layoutParams as RecyclerView.LayoutParams
val marginInDp = 5 // Set the margin to 5dp
val marginInPixels = (marginInDp * parent.resources.displayMetrics.density).toInt()
layoutParams.setMargins(marginInPixels, marginInPixels, marginInPixels, marginInPixels)
binding.root.layoutParams = layoutParams
}
return AccountViewHolder(binding)
}
override fun onBindViewHolder(holder: AccountViewHolder, position: Int) {
holder.bind(accounts[position])
}
override fun getItemCount(): Int {
return accounts.size
}
}

View file

@ -0,0 +1,115 @@
package com.lagradost.cloudstream3.ui.account
import android.content.Context
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.LockPinDialogBinding
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
object AccountDialog {
// TODO add account creation dialog to allow creating accounts directly from AccountSelectActivity
fun showPinInputDialog(
context: Context,
currentPin: String?,
editAccount: Boolean,
callback: (String?) -> Unit
) {
fun TextView.visibleWithText(@StringRes textRes: Int) {
visibility = View.VISIBLE
setText(textRes)
}
fun View.isVisible() = visibility == View.VISIBLE
val binding = LockPinDialogBinding.inflate(LayoutInflater.from(context))
val isPinSet = currentPin != null
val isNewPin = editAccount && !isPinSet
val isEditPin = editAccount && isPinSet
val titleRes = if (isEditPin) R.string.enter_current_pin else R.string.enter_pin
val dialog = AlertDialog.Builder(context, R.style.AlertDialogCustom)
.setView(binding.root)
.setTitle(titleRes)
.setNegativeButton(R.string.cancel) { _, _ ->
callback.invoke(null)
}
.setOnCancelListener {
callback.invoke(null)
}
.setOnDismissListener {
if (binding.pinEditTextError.isVisible()) {
callback.invoke(null)
}
}
.create()
var isPinValid = false
binding.pinEditText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val enteredPin = s.toString()
val isEnteredPinValid = enteredPin.length == 4
if (isEnteredPinValid) {
if (isPinSet) {
if (enteredPin != currentPin) {
binding.pinEditTextError.visibleWithText(R.string.pin_error_incorrect)
binding.pinEditText.text = null
isPinValid = false
} else {
binding.pinEditTextError.visibility = View.GONE
isPinValid = true
callback.invoke(enteredPin)
dialog.dismissSafe()
}
} else {
binding.pinEditTextError.visibility = View.GONE
isPinValid = true
}
} else if (isNewPin) {
binding.pinEditTextError.visibleWithText(R.string.pin_error_length)
isPinValid = false
}
}
override fun afterTextChanged(s: Editable?) {}
})
// Detect IME_ACTION_DONE
binding.pinEditText.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE && isPinValid) {
val enteredPin = binding.pinEditText.text.toString()
callback.invoke(enteredPin)
dialog.dismissSafe()
}
true
}
// We don't want to accidentally have the dialog dismiss when clicking outside of it.
// That is what the cancel button is for.
dialog.setCanceledOnTouchOutside(false)
dialog.show()
// Auto focus on PIN input and show keyboard
binding.pinEditText.requestFocus()
binding.pinEditText.postDelayed({
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.pinEditText, InputMethodManager.SHOW_IMPLICIT)
}, 200)
}
}

View file

@ -0,0 +1,92 @@
package com.lagradost.cloudstream3.ui.account
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.CommonActivity.loadThemes
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.ActivityAccountSelectBinding
import com.lagradost.cloudstream3.databinding.ActivityAccountSelectTvBinding
import com.lagradost.cloudstream3.ui.account.AccountDialog.showPinInputDialog
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAccounts
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
class AccountSelectActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val accounts = getAccounts(this@AccountSelectActivity)
// Don't show account selection if there is only
// one account that exists
if (accounts.count() <= 1) {
navigateToMainActivity()
return
}
CommonActivity.init(this)
loadThemes(this)
window.navigationBarColor = colorFromAttribute(R.attr.primaryBlackBackground)
val binding = if (isTvSettings()) {
ActivityAccountSelectTvBinding.inflate(layoutInflater)
} else ActivityAccountSelectBinding.inflate(layoutInflater)
setContentView(binding.root)
val recyclerView: RecyclerView = binding.root.findViewById(R.id.account_recycler_view)
val adapter = AccountAdapter(accounts) { selectedAccount ->
// Handle the selected account
onAccountSelected(selectedAccount)
}
recyclerView.adapter = adapter
recyclerView.layoutManager = if (isTvSettings()) {
LinearLayoutManager(this)
} else GridLayoutManager(this, 2)
}
private fun onAccountSelected(selectedAccount: DataStoreHelper.Account) {
if (selectedAccount.lockPin != null) {
// The selected account has a PIN set, prompt the user to enter the PIN
showPinInputDialog(this@AccountSelectActivity, selectedAccount.lockPin, false) { pin ->
if (pin == null) return@showPinInputDialog
// Pin is correct, proceed to main activity
setAccount(selectedAccount)
navigateToMainActivity()
}
} else {
// No PIN set for the selected account, proceed to main activity
setAccount(selectedAccount)
navigateToMainActivity()
}
}
private fun setAccount(account: DataStoreHelper.Account) {
// Don't reload if it is the same account
if (DataStoreHelper.selectedKeyIndex == account.keyIndex) {
return
}
DataStoreHelper.selectedKeyIndex = account.keyIndex
MainActivity.bookmarksUpdatedEvent(true)
MainActivity.reloadHomeEvent(true)
}
private fun navigateToMainActivity() {
val mainIntent = Intent(this, MainActivity::class.java)
startActivity(mainIntent)
finish() // Finish the account selection activity
}
}

View file

@ -27,7 +27,6 @@ import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity.Companion.afterBackupRestoreEvent
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
@ -55,8 +54,6 @@ 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.currentHomePage
import com.lagradost.cloudstream3.utils.Event
@ -67,9 +64,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import java.util.*
const val HOME_BOOKMARK_VALUE_LIST = "home_bookmarked_last_list"
const val HOME_PREF_HOMEPAGE = "home_pref_homepage"
class HomeFragment : Fragment() {
companion object {
val configEvent = Event<Int>()
@ -371,10 +365,7 @@ class HomeFragment : Fragment() {
var currentApiName = selectedApiName
var currentValidApis: MutableList<MainAPI> = mutableListOf()
val preSelectedTypes = this.getKey<List<String>>(HOME_PREF_HOMEPAGE)
?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } }
?.toMutableList()
?: mutableListOf(TvType.Movie, TvType.TvSeries)
val preSelectedTypes = DataStoreHelper.homePreference.toMutableList()
binding.cancelBtt.setOnClickListener {
dialog.dismissSafe()
@ -402,7 +393,7 @@ class HomeFragment : Fragment() {
}
fun updateList() {
this.setKey(HOME_PREF_HOMEPAGE, preSelectedTypes)
DataStoreHelper.homePreference = preSelectedTypes
arrayAdapter.clear()
currentValidApis = validAPIs.filter { api ->

View file

@ -15,7 +15,8 @@ import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
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.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
class LoadClickCallback(
@ -34,11 +35,13 @@ open class ParentItemAdapter(
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val root = LayoutInflater.from(parent.context).inflate(
if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent,
parent,
false
)
val layoutResId = when {
isTrueTvSettings() -> R.layout.homepage_parent_tv
parent.context.isEmulatorSettings() -> R.layout.homepage_parent_emulator
else -> R.layout.homepage_parent
}
val root = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
val binding = HomepageParentBinding.bind(root)
@ -234,7 +237,7 @@ open class ParentItemAdapter(
})
//(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
if (!isTvSettings()) {
if (!isTrueTvSettings()) {
title.setOnClickListener {
moreInfoClickCallback.invoke(expand)
}

View file

@ -35,6 +35,7 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
@ -81,6 +82,28 @@ class HomeParentItemAdapterPreview(
parent,
false
) else FragmentHomeHeadBinding.inflate(inflater, parent, false)
if (binding is FragmentHomeHeadTvBinding && parent.context.isEmulatorSettings()) {
binding.homeBookmarkParentItemMoreInfo.isVisible = true
val marginInDp = 50
val density = binding.horizontalScrollChips.context.resources.displayMetrics.density
val marginInPixels = (marginInDp * density).toInt()
val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams
params.marginEnd = marginInPixels
binding.horizontalScrollChips.layoutParams = params
binding.homeWatchParentItemTitle.setCompoundDrawablesWithIntrinsicBounds(
null,
null,
ContextCompat.getDrawable(
parent.context,
R.drawable.ic_baseline_arrow_forward_24
),
null
)
}
HeaderViewHolder(
binding,
viewModel,
@ -355,21 +378,25 @@ class HomeParentItemAdapterPreview(
showApply = false,
{}) {
val newValue = WatchType.values()[it]
homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds(
null,
ContextCompat.getDrawable(
homePreviewBookmark.context,
newValue.iconRes
),
null,
null
)
homePreviewBookmark.setText(newValue.stringRes)
ResultViewModel2.updateWatchStatus(
item,
newValue
)
ResultViewModel2().updateWatchStatus(
newValue,
fab.context,
item
) { statusChanged: Boolean ->
if (!statusChanged) return@updateWatchStatus
homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds(
null,
ContextCompat.getDrawable(
homePreviewBookmark.context,
newValue.iconRes
),
null,
null
)
homePreviewBookmark.setText(newValue.stringRes)
}
}
}
}
@ -553,12 +580,19 @@ class HomeParentItemAdapterPreview(
resumeHolder.isVisible = resumeWatching.isNotEmpty()
resumeAdapter.updateList(resumeWatching)
if (binding is FragmentHomeHeadBinding) {
binding.homeWatchParentItemTitle.setOnClickListener {
if (
binding is FragmentHomeHeadBinding ||
binding is FragmentHomeHeadTvBinding &&
binding.root.context.isEmulatorSettings()
) {
val title = (binding as? FragmentHomeHeadBinding)?.homeWatchParentItemTitle
?: (binding as? FragmentHomeHeadTvBinding)?.homeWatchParentItemTitle
title?.setOnClickListener {
viewModel.popup(
HomeViewModel.ExpandableHomepageList(
HomePageList(
binding.homeWatchParentItemTitle.text.toString(),
title.text.toString(),
resumeWatching,
false
), 1, false
@ -576,8 +610,15 @@ class HomeParentItemAdapterPreview(
bookmarkHolder.isVisible = visible
bookmarkAdapter.updateList(list)
if (binding is FragmentHomeHeadBinding) {
binding.homeBookmarkParentItemTitle.setOnClickListener {
if (
binding is FragmentHomeHeadBinding ||
binding is FragmentHomeHeadTvBinding &&
binding.root.context.isEmulatorSettings()
) {
val title = (binding as? FragmentHomeHeadBinding)?.homeBookmarkParentItemTitle
?: (binding as? FragmentHomeHeadTvBinding)?.homeBookmarkParentItemTitle
title?.setOnClickListener {
val items = toggleList.map { it.first }.filter { it.isChecked }
if (items.isEmpty()) return@setOnClickListener // we don't want to show an empty dialog
val textSum = items

View file

@ -12,7 +12,6 @@ import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.LoadResponse
@ -41,7 +40,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllResumeStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
@ -103,11 +101,6 @@ class HomeViewModel : ViewModel() {
loadStoredData()
}
fun deleteBookmarks() {
deleteAllBookmarkedData()
loadStoredData()
}
var repo: APIRepository? = null
private val _apiName = MutableLiveData<String>()
@ -170,10 +163,7 @@ class HomeViewModel : ViewModel() {
currentWatchTypes.remove(WatchType.NONE)
if (currentWatchTypes.size <= 0) {
setKey(
HOME_BOOKMARK_VALUE_LIST,
intArrayOf()
)
DataStoreHelper.homeBookmarkedList = intArrayOf()
_availableWatchStatusTypes.postValue(setOf<WatchType>() to setOf())
_bookmarks.postValue(Pair(false, ArrayList()))
return@launchSafe
@ -181,16 +171,14 @@ class HomeViewModel : ViewModel() {
val watchPrefNotNull = preferredWatchStatus ?: EnumSet.of(currentWatchTypes.first())
//if (currentWatchTypes.any { watchPrefNotNull.contains(it) }) watchPrefNotNull else listOf(currentWatchTypes.first())
setKey(
HOME_BOOKMARK_VALUE_LIST,
watchPrefNotNull.map { it.internalId }.toIntArray()
)
DataStoreHelper.homeBookmarkedList = watchPrefNotNull.map { it.internalId }.toIntArray()
_availableWatchStatusTypes.postValue(
Pair(
watchPrefNotNull,
currentWatchTypes,
watchPrefNotNull to
currentWatchTypes,
)
)
val list = withContext(Dispatchers.IO) {
watchStatusIds.filter { watchPrefNotNull.contains(it.second) }
@ -463,7 +451,7 @@ class HomeViewModel : ViewModel() {
fun loadStoredData() {
val list = EnumSet.noneOf(WatchType::class.java)
getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let {
DataStoreHelper.homeBookmarkedList.map { WatchType.fromInternalId(it) }.let {
list.addAll(it)
}
loadStoredData(list)

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.ui.library
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
@ -8,14 +9,25 @@ import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.fragment.app.Fragment
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.FOCUS_AFTER_DESCENDANTS
import android.view.ViewGroup.FOCUS_BLOCK_DESCENDANTS
import android.view.animation.AlphaAnimation
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView
import androidx.core.view.allViews
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.viewpager2.widget.ViewPager2
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.allProviders
@ -23,21 +35,26 @@ 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.MainActivity
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.syncproviders.BackupAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
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.ui.settings.SettingsFragment
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.DataStoreHelper.currentAccount
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
@ -48,7 +65,7 @@ const val LIBRARY_FOLDER = "library_folder"
enum class LibraryOpenerType(@StringRes val stringRes: Int) {
Default(R.string.default_subtitles), // TODO FIX AFTER MERGE
Default(R.string.action_default),
Provider(R.string.none),
Browser(R.string.browser),
Search(R.string.search),
@ -83,9 +100,21 @@ class LibraryFragment : Fragment() {
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
MainActivity.afterBackupRestoreEvent += ::onNewSyncData
val localBinding = FragmentLibraryBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root
val layout =
if (SettingsFragment.isTvSettings()) R.layout.fragment_library_tv else R.layout.fragment_library
val root = inflater.inflate(layout, container, false)
binding = try {
FragmentLibraryBinding.bind(root)
} catch (t: Throwable) {
CommonActivity.showToast(
txt(R.string.unable_to_inflate, t.message ?: ""),
Toast.LENGTH_LONG
)
logError(t)
null
}
return root
//return inflater.inflate(R.layout.fragment_library, container, false)
}
@ -103,26 +132,25 @@ class LibraryFragment : Fragment() {
super.onSaveInstanceState(outState)
}
@SuppressLint("ResourceType", "CutPasteId")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fixPaddingStatusbar(binding?.searchStatusBarPadding)
binding?.sortFab?.setOnClickListener {
val methods = libraryViewModel.sortingMethods.map {
txt(it.stringRes).asString(view.context)
}
binding?.sortFab?.setOnClickListener(sortChangeClickListener)
binding?.librarySort?.setOnClickListener(sortChangeClickListener)
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)
})
binding?.libraryRoot?.findViewById<TextView>(R.id.search_src_text)?.apply {
tag = "tv_no_focus_tag"
}
// Set the color for the search exit icon to the correct theme text color
val searchExitIcon = binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
val searchExitIconColor = TypedValue()
activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true)
searchExitIcon?.setColorFilter(searchExitIconColor.data)
binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
libraryViewModel.sort(ListSorting.Query, query)
@ -186,7 +214,7 @@ class LibraryFragment : Fragment() {
val items = baseOptions.map { txt(it.stringRes).asString(this) } + availableProviders
val savedSelection = getKey<LibraryOpener>(LIBRARY_FOLDER, key)
val savedSelection = getKey<LibraryOpener>("$currentAccount/$LIBRARY_FOLDER", key)
val selectedIndex =
when {
savedSelection == null -> 0
@ -221,7 +249,7 @@ class LibraryFragment : Fragment() {
}
setKey(
LIBRARY_FOLDER,
"$currentAccount/$LIBRARY_FOLDER",
key,
savedData,
)
@ -234,6 +262,7 @@ class LibraryFragment : Fragment() {
}
binding?.viewpager?.setPageTransformer(LibraryScrollTransformer())
binding?.viewpager?.adapter =
binding?.viewpager?.adapter ?: ViewpagerAdapter(
mutableListOf(),
@ -268,8 +297,11 @@ class LibraryFragment : Fragment() {
// 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 {
getKey<LibraryOpener>("$currentAccount/$LIBRARY_FOLDER", syncName.name)
val savedSelection = getKey<LibraryOpener>(
"$currentAccount/$LIBRARY_FOLDER",
syncId
).takeIf {
it?.openType != LibraryOpenerType.Default
} ?: savedListSelection
@ -357,11 +389,18 @@ class LibraryFragment : Fragment() {
}
(viewpager.adapter as? ViewpagerAdapter)?.pages = pages
//fix focus on the viewpager itself
(viewpager.getChildAt(0) as RecyclerView).apply {
tag = "tv_no_focus_tag"
//isFocusable = false
}
// Using notifyItemRangeChanged keeps the animations when sorting
viewpager.adapter?.notifyItemRangeChanged(
0,
viewpager.adapter?.itemCount ?: 0
)
binding?.viewpager?.setCurrentItem(libraryViewModel.currentPage, false)
// Only stop loading after 300ms to hide the fade effect the viewpager produces when updating
// Without this there would be a flashing effect:
@ -398,6 +437,9 @@ class LibraryFragment : Fragment() {
viewpager,
) { tab, position ->
tab.text = pages.getOrNull(position)?.title?.asStringNull(context)
tab.view.tag = "tv_no_focus_tag"
tab.view.nextFocusDownId = R.id.search_result_root
tab.view.setOnClickListener {
val currentItem =
binding?.viewpager?.currentItem ?: return@setOnClickListener
@ -420,8 +462,25 @@ class LibraryFragment : Fragment() {
}
}
}
}
binding?.viewpager?.registerOnPageChangeCallback(object :
ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
val all = binding?.viewpager?.allViews?.toList()
?.filterIsInstance<AutofitRecyclerView>()
all?.forEach { view ->
view.isVisible = view.tag == position
view.isFocusable = view.tag == position
if (view.tag == position)
view.descendantFocusability = FOCUS_AFTER_DESCENDANTS
else
view.descendantFocusability = FOCUS_BLOCK_DESCENDANTS
}
super.onPageSelected(position)
}
})
}
override fun onConfigurationChanged(newConfig: Configuration) {
(binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind()
super.onConfigurationChanged(newConfig)
@ -440,6 +499,21 @@ class LibraryFragment : Fragment() {
private fun onNewSyncData(unused: Unit) {
Log.d(BackupAPI.LOG_KEY, "will reload pages")
libraryViewModel.reloadPages(true)
private val sortChangeClickListener = View.OnClickListener { view ->
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)
})
}
}

View file

@ -6,11 +6,14 @@ 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.MainActivity
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 com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
enum class ListSorting(@StringRes val stringRes: Int) {
Query(R.string.none),
@ -28,6 +31,8 @@ class LibraryViewModel : ViewModel() {
private val _pages: MutableLiveData<Resource<List<SyncAPI.Page>>> = MutableLiveData(null)
val pages: LiveData<Resource<List<SyncAPI.Page>>> = _pages
var currentPage: Int = 0
private val _currentApiName: MutableLiveData<String> = MutableLiveData("")
val currentApiName: LiveData<String> = _currentApiName
@ -35,12 +40,12 @@ class LibraryViewModel : ViewModel() {
get() = SyncApis.filter { it.hasAccount() }
var currentSyncApi = availableSyncApis.let { allApis ->
val lastSelection = getKey<String>(LAST_SYNC_API_KEY)
val lastSelection = getKey<String>("$currentAccount/$LAST_SYNC_API_KEY")
availableSyncApis.firstOrNull { it.name == lastSelection } ?: allApis.firstOrNull()
}
private set(value) {
field = value
setKey(LAST_SYNC_API_KEY, field?.name)
setKey("$currentAccount/$LAST_SYNC_API_KEY", field?.name)
}
val availableApiNames: List<String>
@ -58,13 +63,21 @@ class LibraryViewModel : ViewModel() {
reloadPages(true)
}
fun sort(method: ListSorting, query: String? = null) {
val currentList = pages.value ?: return
fun sort(method: ListSorting, query: String? = null) = ioSafe {
val value = _pages.value ?: return@ioSafe
if (value is Resource.Success) {
sort(method, query, value.value)
}
}
private fun sort(method: ListSorting, query: String? = null, items: List<SyncAPI.Page>) {
currentSortingMethod = method
(currentList as? Resource.Success)?.value?.forEachIndexed { _, page ->
DataStoreHelper.librarySortingMode = method.ordinal
items.forEach { page ->
page.sort(method, query)
}
_pages.postValue(currentList)
_pages.postValue(Resource.Success(items))
}
fun reloadPages(forceReload: Boolean) {
@ -85,8 +98,6 @@ class LibraryViewModel : ViewModel() {
val library = (libraryResource as? Resource.Success)?.value ?: return@let
sortingMethods = library.supportedListSorting.toList()
currentSortingMethod = null
repo.requireLibraryRefresh = false
val pages = library.allLibraryLists.map {
@ -96,8 +107,24 @@ class LibraryViewModel : ViewModel() {
)
}
_pages.postValue(Resource.Success(pages))
val desiredSortingMethod =
ListSorting.values().getOrNull(DataStoreHelper.librarySortingMode)
if (desiredSortingMethod != null && library.supportedListSorting.contains(desiredSortingMethod)) {
sort(desiredSortingMethod, null, pages)
} else {
// null query = no sorting
sort(ListSorting.Query, null, pages)
}
}
}
}
init {
MainActivity.reloadHomeEvent += ::reloadPages
}
override fun onCleared() {
MainActivity.reloadHomeEvent -= ::reloadPages
super.onCleared()
}
}

View file

@ -25,7 +25,7 @@ class ViewpagerAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is PageViewHolder -> {
holder.bind(pages[position], unbound.remove(position))
holder.bind(pages[position], position, unbound.remove(position))
}
}
}
@ -43,7 +43,8 @@ class ViewpagerAdapter(
inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(page: SyncAPI.Page, rebind: Boolean) {
fun bind(page: SyncAPI.Page, position: Int, rebind: Boolean) {
binding.pageRecyclerview.tag = position
binding.pageRecyclerview.apply {
spanCount =
this@PageViewHolder.itemView.context.getSpanCount() ?: 3

View file

@ -18,10 +18,9 @@ import android.widget.ProgressBar
import android.widget.Toast
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import androidx.media3.common.PlaybackException
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession
@ -30,12 +29,15 @@ import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerView
import androidx.media3.ui.SubtitleView
import androidx.media3.ui.TimeBar
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import androidx.preference.PreferenceManager
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.github.rubensousa.previewseekbar.PreviewBar
import com.github.rubensousa.previewseekbar.media3.PreviewTimeBar
import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode
import com.lagradost.cloudstream3.CommonActivity.isInPIPMode
import com.lagradost.cloudstream3.CommonActivity.keyEventListener
import com.lagradost.cloudstream3.CommonActivity.playerEventListener
import com.lagradost.cloudstream3.CommonActivity.screenWidth
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
@ -45,6 +47,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.EpisodeSkip
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
@ -77,12 +80,12 @@ abstract class AbstractPlayerFragment(
var isBuffering = true
protected open var hasPipModeSupport = true
var playerPausePlayHolderHolder : FrameLayout? = null
var playerPausePlay : ImageView? = null
var playerBuffering : ProgressBar? = null
var playerView : PlayerView? = null
var piphide : FrameLayout? = null
var subtitleHolder : FrameLayout? = null
var playerPausePlayHolderHolder: FrameLayout? = null
var playerPausePlay: ImageView? = null
var playerBuffering: ProgressBar? = null
var playerView: PlayerView? = null
var piphide: FrameLayout? = null
var subtitleHolder: FrameLayout? = null
@LayoutRes
protected open var layout: Int = R.layout.fragment_player
@ -95,11 +98,11 @@ abstract class AbstractPlayerFragment(
throw NotImplementedError()
}
open fun playerPositionChanged(position: Long, duration : Long) {
open fun playerPositionChanged(position: Long, duration: Long) {
throw NotImplementedError()
}
open fun playerDimensionsLoaded(width: Int, height : Int) {
open fun playerDimensionsLoaded(width: Int, height: Int) {
throw NotImplementedError()
}
@ -135,8 +138,10 @@ abstract class AbstractPlayerFragment(
}
}
private fun updateIsPlaying(wasPlaying : CSPlayerLoading,
isPlaying : CSPlayerLoading) {
private fun updateIsPlaying(
wasPlaying: CSPlayerLoading,
isPlaying: CSPlayerLoading
) {
val isPlayingRightNow = CSPlayerLoading.IsPlaying == isPlaying
val isPausedRightNow = CSPlayerLoading.IsPaused == isPlaying
@ -184,7 +189,11 @@ abstract class AbstractPlayerFragment(
canEnterPipMode = isPlayingRightNow && hasPipModeSupport
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity?.let { act ->
PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow, player.getAspectRatio())
PlayerPipHelper.updatePIPModeActions(
act,
isPlayingRightNow,
player.getAspectRatio()
)
}
}
}
@ -373,49 +382,61 @@ abstract class AbstractPlayerFragment(
/** This receives the events from the player, if you want to append functionality you do it here,
* do note that this only receives events for UI changes,
* and returning early WONT stop it from changing in eg the player time or pause status */
open fun mainCallback(event : PlayerEvent) {
open fun mainCallback(event: PlayerEvent) {
Log.i(TAG, "Handle event: $event")
when(event) {
when (event) {
is ResizedEvent -> {
playerDimensionsLoaded(event.width, event.height)
}
is PlayerAttachedEvent -> {
playerUpdated(event.player)
}
is SubtitlesUpdatedEvent -> {
subtitlesChanged()
}
is TimestampSkippedEvent -> {
onTimestampSkipped(event.timestamp)
}
is TimestampInvokedEvent -> {
onTimestamp(event.timestamp)
}
is TracksChangedEvent -> {
onTracksInfoChanged()
}
is EmbeddedSubtitlesFetchedEvent -> {
embeddedSubtitlesFetched(event.tracks)
}
is ErrorEvent -> {
playerError(event.error)
}
is RequestAudioFocusEvent -> {
requestAudioFocus()
}
is EpisodeSeekEvent -> {
when(event.offset) {
when (event.offset) {
-1 -> prevEpisode()
1 -> nextEpisode()
else -> {}
}
}
is StatusEvent -> {
updateIsPlaying(wasPlaying = event.wasPlaying, isPlaying = event.isPlaying)
}
is PositionEvent -> {
playerPositionChanged(position = event.toMs, duration = event.durationMs)
}
is VideoEndedEvent -> {
context?.let { ctx ->
// Only play next episode if autoplay is on (default)
@ -432,6 +453,7 @@ abstract class AbstractPlayerFragment(
}
}
}
is PauseEvent -> Unit
is PlayEvent -> Unit
}
@ -439,7 +461,7 @@ abstract class AbstractPlayerFragment(
@SuppressLint("SetTextI18n", "UnsafeOptInUsageError")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
resizeMode = getKey(RESIZE_MODE_KEY) ?: 0
resizeMode = DataStoreHelper.resizeMode
resize(resizeMode, false)
player.releaseCallbacks()
@ -454,22 +476,73 @@ abstract class AbstractPlayerFragment(
)
if (player is CS3IPlayer) {
// preview bar
val progressBar: PreviewTimeBar? = playerView?.findViewById(R.id.exo_progress)
val previewImageView: ImageView? = playerView?.findViewById(R.id.previewImageView)
val previewFrameLayout: FrameLayout? = playerView?.findViewById(R.id.previewFrameLayout)
if (progressBar != null && previewImageView != null && previewFrameLayout != null) {
var resume = false
progressBar.addOnScrubListener(object : PreviewBar.OnScrubListener {
override fun onScrubStart(previewBar: PreviewBar?) {
val hasPreview = player.hasPreview()
progressBar.isPreviewEnabled = hasPreview
resume = player.getIsPlaying()
if (resume) player.handleEvent(
CSPlayerEvent.Pause,
PlayerEventSource.Player
)
}
override fun onScrubMove(
previewBar: PreviewBar?,
progress: Int,
fromUser: Boolean
) {
}
override fun onScrubStop(previewBar: PreviewBar?) {
if (resume) player.handleEvent(CSPlayerEvent.Play, PlayerEventSource.Player)
}
})
progressBar.attachPreviewView(previewFrameLayout)
progressBar.setPreviewLoader { currentPosition, max ->
val bitmap = player.getPreview(currentPosition.toFloat().div(max.toFloat()))
previewImageView.isGone = bitmap == null
previewImageView.setImageBitmap(bitmap)
}
}
subView = playerView?.findViewById(R.id.exo_subtitles)
subStyle = SubtitlesFragment.getCurrentSavedStyle()
player.initSubtitles(subView, subtitleHolder, subStyle)
(player.imageGenerator as? PreviewGenerator)?.params = ImageParams.new16by9(screenWidth)
/*previewImageView?.doOnLayout {
(player.imageGenerator as? PreviewGenerator)?.params = ImageParams(
it.measuredWidth,
it.measuredHeight
)
}*/
/** this might seam a bit fucky and that is because it is, the seek event is captured twice, once by the player
* and once by the UI even if it should only be registered once by the UI */
playerView?.findViewById<DefaultTimeBar>(R.id.exo_progress)?.addListener(object : TimeBar.OnScrubListener {
override fun onScrubStart(timeBar: TimeBar, position: Long) = Unit
override fun onScrubMove(timeBar: TimeBar, position: Long) = Unit
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
if (canceled) return
val playerDuration = player.getDuration() ?: return
val playerPosition = player.getPosition() ?: return
mainCallback(PositionEvent(source = PlayerEventSource.UI, durationMs = playerDuration, fromMs = playerPosition, toMs = position))
}
})
playerView?.findViewById<DefaultTimeBar>(R.id.exo_progress)
?.addListener(object : TimeBar.OnScrubListener {
override fun onScrubStart(timeBar: TimeBar, position: Long) = Unit
override fun onScrubMove(timeBar: TimeBar, position: Long) = Unit
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
if (canceled) return
val playerDuration = player.getDuration() ?: return
val playerPosition = player.getPosition() ?: return
mainCallback(
PositionEvent(
source = PlayerEventSource.UI,
durationMs = playerDuration,
fromMs = playerPosition,
toMs = position
)
)
}
})
SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged
@ -535,7 +608,7 @@ abstract class AbstractPlayerFragment(
@SuppressLint("UnsafeOptInUsageError")
fun resize(resize: PlayerResize, showToast: Boolean) {
setKey(RESIZE_MODE_KEY, resize.ordinal)
DataStoreHelper.resizeMode = resize.ordinal
val type = when (resize) {
PlayerResize.Fill -> AspectRatioFrameLayout.RESIZE_MODE_FILL
PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT

View file

@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.os.Handler
import android.os.Looper
@ -56,6 +57,7 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
import com.lagradost.cloudstream3.utils.DrmExtractorLink
import com.lagradost.cloudstream3.utils.EpisodeSkip
import com.lagradost.cloudstream3.utils.ExtractorLink
@ -88,7 +90,9 @@ class CS3IPlayer : IPlayer {
private var exoPlayer: ExoPlayer? = null
set(value) {
// If the old value is not null then the player has not been properly released.
debugAssert({ field != null && value != null }, { "Previous player instance should be released!" })
debugAssert(
{ field != null && value != null },
{ "Previous player instance should be released!" })
field = value
}
@ -96,6 +100,8 @@ class CS3IPlayer : IPlayer {
var simpleCacheSize = 0L
var videoBufferMs = 0L
val imageGenerator = IPreviewGenerator.new()
private val seekActionTime = 30000L
private var ignoreSSL: Boolean = true
@ -182,6 +188,14 @@ class CS3IPlayer : IPlayer {
subtitleHelper.initSubtitles(subView, subHolder, style)
}
override fun getPreview(fraction: Float): Bitmap? {
return imageGenerator.getPreviewImage(fraction)
}
override fun hasPreview(): Boolean {
return imageGenerator.hasPreview()
}
override fun loadPlayer(
context: Context,
sameEpisode: Boolean,
@ -190,7 +204,8 @@ class CS3IPlayer : IPlayer {
startPosition: Long?,
subtitles: Set<SubtitleData>,
subtitle: SubtitleData?,
autoPlay: Boolean?
autoPlay: Boolean?,
preview: Boolean,
) {
Log.i(TAG, "loadPlayer")
if (sameEpisode) {
@ -209,11 +224,30 @@ class CS3IPlayer : IPlayer {
// release the current exoplayer and cache
releasePlayer()
if (link != null) {
// only video support atm
(imageGenerator as? PreviewGenerator)?.let { gen ->
if (preview) {
gen.load(link, sameEpisode)
} else {
gen.clear(sameEpisode)
}
}
loadOnlinePlayer(context, link)
} else if (data != null) {
(imageGenerator as? PreviewGenerator)?.let { gen ->
if (preview) {
gen.load(context, data, sameEpisode)
} else {
gen.clear(sameEpisode)
}
}
loadOfflinePlayer(context, data)
} else {
throw IllegalArgumentException("Requires link or uri")
}
}
override fun setActiveSubtitles(subtitles: Set<SubtitleData>) {
@ -494,6 +528,7 @@ class CS3IPlayer : IPlayer {
}
override fun release() {
imageGenerator.release()
releasePlayer()
}
@ -508,12 +543,15 @@ class CS3IPlayer : IPlayer {
**/
var preferredAudioTrackLanguage: String? = null
get() {
return field ?: getKey(PREFERRED_AUDIO_LANGUAGE_KEY, field)?.also {
return field ?: getKey(
"$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY",
field
)?.also {
field = it
}
}
set(value) {
setKey(PREFERRED_AUDIO_LANGUAGE_KEY, value)
setKey("$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY", value)
field = value
}
@ -871,8 +909,20 @@ class CS3IPlayer : IPlayer {
CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source)
CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source)
CSPlayerEvent.NextEpisode -> event(EpisodeSeekEvent(offset = 1, source = source))
CSPlayerEvent.PrevEpisode -> event(EpisodeSeekEvent(offset = -1, source = source))
CSPlayerEvent.NextEpisode -> event(
EpisodeSeekEvent(
offset = 1,
source = source
)
)
CSPlayerEvent.PrevEpisode -> event(
EpisodeSeekEvent(
offset = -1,
source = source
)
)
CSPlayerEvent.SkipCurrentChapter -> {
//val dur = this@CS3IPlayer.getDuration() ?: return@apply
getCurrentTimestamp()?.let { lastTimeStamp ->

View file

@ -110,4 +110,9 @@ class DownloadedPlayerActivity : AppCompatActivity() {
return
}
}
override fun onResume() {
super.onResume()
CommonActivity.setActivityInstance(this)
}
}

View file

@ -49,6 +49,7 @@ import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
@ -356,7 +357,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
private fun setPlayBackSpeed(speed: Float) {
try {
setKey(PLAYBACK_SPEED_KEY, speed)
DataStoreHelper.playBackSpeed = speed
playerBinding?.playerSpeedBtt?.text =
getString(R.string.player_speed_text_format).format(speed)
.replace(".0x", "x")
@ -1194,7 +1195,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// init variables
setPlayBackSpeed(getKey(PLAYBACK_SPEED_KEY) ?: 1.0f)
setPlayBackSpeed(DataStoreHelper.playBackSpeed)
savedInstanceState?.getLong(SUBTITLE_DELAY_BUNDLE_KEY)?.let {
subtitleDelay = it
}

View file

@ -182,6 +182,7 @@ class GeneratorPlayer : FullScreenPlayer() {
(if (sameEpisode) currentSelectedSubtitles else null) ?: getAutoSelectSubtitle(
currentSubs, settings = true, downloads = true
),
preview = isFullScreenPlayer
)
}
@ -1025,7 +1026,7 @@ class GeneratorPlayer : FullScreenPlayer() {
ctx.getString(R.string.episode_sync_enabled_key), true
)
) maxEpisodeSet = meta.episode
sync.modifyMaxEpisode(meta.episode)
sync.modifyMaxEpisode(meta.totalEpisodeIndex ?: meta.episode)
}
}

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.ui.player
import android.content.Context
import android.graphics.Bitmap
import android.util.Rational
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.utils.EpisodeSkip
@ -198,17 +199,8 @@ data class CurrentTracks(
class InvalidFileException(msg: String) : Exception(msg)
//http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
const val STATE_RESUME_WINDOW = "resumeWindow"
const val STATE_RESUME_POSITION = "resumePosition"
const val STATE_PLAYER_FULLSCREEN = "playerFullscreen"
const val STATE_PLAYER_PLAYING = "playerOnPlay"
const val ACTION_MEDIA_CONTROL = "media_control"
const val EXTRA_CONTROL_TYPE = "control_type"
const val PLAYBACK_SPEED = "playback_speed"
const val RESIZE_MODE_KEY = "resize_mode" // Last used resize mode
const val PLAYBACK_SPEED_KEY = "playback_speed" // Last used playback speed
const val PREFERRED_SUBS_KEY = "preferred_subtitles" // Last used resize mode
//const val PLAYBACK_FASTFORWARD = "playback_fastforward" // Last used resize mode
/** Abstract Exoplayer logic, can be expanded to other players */
interface IPlayer {
@ -246,11 +238,15 @@ interface IPlayer {
startPosition: Long? = null,
subtitles: Set<SubtitleData>,
subtitle: SubtitleData?,
autoPlay: Boolean? = true
autoPlay: Boolean? = true,
preview : Boolean = true,
)
fun reloadPlayer(context: Context)
fun getPreview(fraction : Float) : Bitmap?
fun hasPreview() : Boolean
fun setActiveSubtitles(subtitles: Set<SubtitleData>)
fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean // returns true if the player requires a reload, null for nothing
fun getCurrentPreferredSubtitle(): SubtitleData?

View file

@ -0,0 +1,541 @@
package com.lagradost.cloudstream3.ui.player
import android.content.Context
import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.annotation.WorkerThread
import androidx.core.graphics.scale
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.ExtractorUri
import com.lagradost.cloudstream3.utils.M3u8Helper
import com.lagradost.cloudstream3.utils.M3u8Helper2
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import kotlin.math.absoluteValue
import kotlin.math.ceil
import kotlin.math.log2
const val MAX_LOD = 6
const val MIN_LOD = 3
data class ImageParams(
val width: Int,
val height: Int,
) {
companion object {
val DEFAULT = ImageParams(200, 320)
fun new16by9(width: Int): ImageParams {
if (width < 100) {
return DEFAULT
}
return ImageParams(
width / 4,
(width * 9) / (4 * 16)
)
}
}
init {
assert(width > 0 && height > 0)
}
}
interface IPreviewGenerator {
fun hasPreview(): Boolean
fun getPreviewImage(fraction: Float): Bitmap?
fun release()
var params: ImageParams
var durationMs: Long
var loadedImages: Int
companion object {
fun new(): IPreviewGenerator {
/** because TV has low ram + not show we disable this for now */
return if (SettingsFragment.isTrueTvSettings()) {
empty()
} else {
PreviewGenerator()
}
}
fun empty(): IPreviewGenerator {
return NoPreviewGenerator()
}
}
}
private fun rescale(image: Bitmap, params: ImageParams): Bitmap {
if (image.width <= params.width && image.height <= params.height) return image
val new = image.scale(params.width, params.height)
// throw away the old image
if (new != image) {
image.recycle()
}
return new
}
/** rescale to not take up as much memory */
private fun MediaMetadataRetriever.image(timeUs: Long, params: ImageParams): Bitmap? {
/*if (timeUs <= 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try {
val primary = this.primaryImage
if (primary != null) {
return rescale(primary, params)
}
} catch (t: Throwable) {
logError(t)
}
}*/
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
this.getScaledFrameAtTime(
timeUs,
MediaMetadataRetriever.OPTION_CLOSEST_SYNC,
params.width,
params.height
)
} else {
return rescale(this.getFrameAtTime(timeUs) ?: return null, params)
}
}
/** PreviewGenerator that hides the implementation details of the sub generators that is used, used for source switch cache */
class PreviewGenerator : IPreviewGenerator {
/** the most up to date generator, will always mirror the actual source in the player */
private var currentGenerator: IPreviewGenerator = NoPreviewGenerator()
/** the longest generated preview of the same episode */
private var lastGenerator: IPreviewGenerator = NoPreviewGenerator()
/** always NoPreviewGenerator, used as a cache for nothing */
private val dummy: IPreviewGenerator = NoPreviewGenerator()
/** if the current generator is the same as the last by checking time */
private fun isSameLength(): Boolean =
currentGenerator.durationMs.minus(lastGenerator.durationMs).absoluteValue < 10_000L
/** use the backup if the current generator is init or if they have the same length */
private val backupGenerator: IPreviewGenerator
get() {
if (currentGenerator.durationMs == 0L || isSameLength()) {
return lastGenerator
}
return dummy
}
override fun hasPreview(): Boolean {
return currentGenerator.hasPreview() || backupGenerator.hasPreview()
}
override fun getPreviewImage(fraction: Float): Bitmap? {
return try {
currentGenerator.getPreviewImage(fraction) ?: backupGenerator.getPreviewImage(fraction)
} catch (t: Throwable) {
logError(t)
null
}
}
override fun release() {
lastGenerator.release()
currentGenerator.release()
lastGenerator = NoPreviewGenerator()
currentGenerator = NoPreviewGenerator()
}
override var params: ImageParams = ImageParams.DEFAULT
set(value) {
field = value
lastGenerator.params = value
backupGenerator.params = value
currentGenerator.params = value
}
override var durationMs: Long
get() = currentGenerator.durationMs
set(_) {}
override var loadedImages: Int
get() = currentGenerator.loadedImages
set(_) {}
fun clear(keepCache: Boolean) {
if (keepCache) {
if (!isSameLength() || currentGenerator.loadedImages >= lastGenerator.loadedImages || lastGenerator.durationMs == 0L) {
// the current generator is better than the last generator, therefore keep the current
// or the lengths are not the same, therefore favoring the more recent selection
// if they are the same we favor the current generator
lastGenerator.release()
lastGenerator = currentGenerator
} else {
// otherwise just keep the last generator and throw away the current generator
currentGenerator.release()
}
} else {
// we switched the episode, therefore keep nothing
lastGenerator.release()
lastGenerator = NoPreviewGenerator()
currentGenerator.release()
// we assume that we set currentGenerator right after this, so currentGenerator != NoPreviewGenerator
}
}
fun load(link: ExtractorLink, keepCache: Boolean) {
clear(keepCache)
when (link.type) {
ExtractorLinkType.M3U8 -> {
currentGenerator = M3u8PreviewGenerator(params).apply {
load(url = link.url, headers = link.getAllHeaders())
}
}
ExtractorLinkType.VIDEO -> {
currentGenerator = Mp4PreviewGenerator(params).apply {
load(url = link.url, headers = link.getAllHeaders())
}
}
else -> {
Log.i("PreviewImg", "unsupported format for $link")
}
}
}
fun load(context: Context, link: ExtractorUri, keepCache: Boolean) {
clear(keepCache)
currentGenerator = Mp4PreviewGenerator(params).apply {
load(keepCache = keepCache, context = context, uri = link.uri)
}
}
}
@Suppress("UNUSED_PARAMETER")
private class NoPreviewGenerator : IPreviewGenerator {
override fun hasPreview(): Boolean = false
override fun getPreviewImage(fraction: Float): Bitmap? = null
override fun release() = Unit
override var params: ImageParams
get() = ImageParams(0, 0)
set(value) {}
override var durationMs: Long = 0L
override var loadedImages: Int = 0
}
private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewGenerator {
// generated images 1:1 to idx of hsl
private var images: Array<Bitmap?> = arrayOf()
private val TAG = "PreviewImgM3u8"
// prefixSum[i] = sum(hsl.ts[0..i].time)
// where [0] = 0, [1] = hsl.ts[0].time aka time at start of segment, do [b] - [a] for range a,b
private var prefixSum: Array<Double> = arrayOf()
// how many images has been generated
override var loadedImages: Int = 0
// how many images we can generate in total, == hsl.size ?: 0
private var totalImages: Int = 0
override fun hasPreview(): Boolean {
return totalImages > 0 && loadedImages >= minOf(totalImages, 4)
}
override fun getPreviewImage(fraction: Float): Bitmap? {
var bestIdx = -1
var bestDiff = Double.MAX_VALUE
synchronized(images) {
// just find the best one in a for loop, we don't care about bin searching rn
for (i in 0..images.size) {
val diff = prefixSum[i].minus(fraction).absoluteValue
if (diff > bestDiff) {
break
}
if (images[i] != null) {
bestIdx = i
bestDiff = diff
}
}
return images.getOrNull(bestIdx)
}
/*
val targetIndex = prefixSum.binarySearch(target)
var ret = images[targetIndex]
if (ret != null) {
return ret
}
for (i in 0..images.size) {
ret = images.getOrNull(i+targetIndex) ?:
}*/
}
private fun clear() {
synchronized(images) {
currentJob?.cancel()
// for (i in images.indices) {
// images[i]?.recycle()
// }
images = arrayOf()
prefixSum = arrayOf()
loadedImages = 0
totalImages = 0
}
}
override fun release() {
clear()
images = arrayOf()
}
override var durationMs: Long = 0L
private var currentJob: Job? = null
fun load(url: String, headers: Map<String, String>) {
clear()
currentJob?.cancel()
currentJob = ioSafe {
withContext(Dispatchers.IO) {
Log.i(TAG, "Loading with url = $url headers = $headers")
//tmpFile =
// File.createTempFile("video", ".ts", context.cacheDir).apply {
// deleteOnExit()
// }
val retriever = MediaMetadataRetriever()
val hsl = M3u8Helper2.hslLazy(
listOf(
M3u8Helper.M3u8Stream(
streamUrl = url,
headers = headers
)
),
selectBest = false
)
// no support for encryption atm
if (hsl.isEncrypted) {
Log.i(TAG, "m3u8 is encrypted")
totalImages = 0
return@withContext
}
// total duration of the entire m3u8 in seconds
val duration = hsl.allTsLinks.sumOf { it.time ?: 0.0 }
durationMs = (duration * 1000.0).toLong()
val durationInv = 1.0 / duration
// if the total duration is less then 10s then something is very wrong or
// too short playback to matter
if (duration <= 10.0) {
totalImages = 0
return@withContext
}
totalImages = hsl.allTsLinks.size
// we cant init directly as it is no guarantee of in order
prefixSum = Array(hsl.allTsLinks.size + 1) { 0.0 }
var runningSum = 0.0
for (i in hsl.allTsLinks.indices) {
runningSum += (hsl.allTsLinks[i].time ?: 0.0)
prefixSum[i + 1] = runningSum * durationInv
}
synchronized(images) {
images = Array(hsl.size) { null }
loadedImages = 0
}
val maxLod = ceil(log2(duration)).toInt().coerceIn(MIN_LOD, MAX_LOD)
val count = hsl.allTsLinks.size
for (l in 1..maxLod) {
val items = (1 shl (l - 1))
for (i in 0 until items) {
val index = (count.div(1 shl l) + (i * count) / items).coerceIn(0, hsl.size)
if (synchronized(images) { images[index] } != null) {
continue
}
Log.i(TAG, "Generating preview for $index")
val ts = hsl.allTsLinks[index]
try {
retriever.setDataSource(ts.url, hsl.headers)
if (!isActive) {
return@withContext
}
val img = retriever.image(0, params)
if (!isActive) {
return@withContext
}
if (img == null || img.width <= 1 || img.height <= 1) continue
synchronized(images) {
images[index] = img
loadedImages += 1
}
} catch (t: Throwable) {
logError(t)
continue
}
/*
val buffer = hsl.resolveLinkSafe(index) ?: continue
tmpFile?.writeBytes(buffer)
val buff = FileOutputStream(tmpFile)
retriever.setDataSource(buff.fd)
val frame = retriever.getFrameAtTime(0L)*/
}
}
}
}
}
}
private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGenerator {
// lod = level of detail where the number indicates how many ones there is
// 2^(lod-1) = images
private var loadedLod = 0
override var loadedImages = 0
private var images = Array<Bitmap?>((1 shl MAX_LOD) - 1) {
null
}
override fun hasPreview(): Boolean {
synchronized(images) {
return loadedLod >= MIN_LOD
}
}
val TAG = "PreviewImgMp4"
override fun getPreviewImage(fraction: Float): Bitmap? {
synchronized(images) {
if (loadedLod < MIN_LOD) {
Log.i(TAG, "Requesting preview for $fraction but $loadedLod < $MIN_LOD")
return null
}
Log.i(TAG, "Requesting preview for $fraction")
var bestIdx = 0
var bestDiff = 0.5f.minus(fraction).absoluteValue
// this should be done mathematically, but for now we just loop all images
for (l in 1..loadedLod + 1) {
val items = (1 shl (l - 1))
for (i in 0 until items) {
val idx = items - 1 + i
if (idx > loadedImages) {
break
}
if (images[idx] == null) {
continue
}
val currentFraction =
(1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat()))
val diff = currentFraction.minus(fraction).absoluteValue
if (diff < bestDiff) {
bestDiff = diff
bestIdx = idx
}
}
}
Log.i(TAG, "Best diff found at ${bestDiff * 100}% diff (${bestIdx})")
return images[bestIdx]
}
}
// also check out https://github.com/wseemann/FFmpegMediaMetadataRetriever
private val retriever: MediaMetadataRetriever = MediaMetadataRetriever()
private fun clear(keepCache: Boolean) {
if (keepCache) return
synchronized(images) {
loadedLod = 0
loadedImages = 0
// for (i in images.indices) {
// images[i]?.recycle()
// images[i] = null
//}
images.fill(null)
}
}
private var currentJob: Job? = null
fun load(url: String, headers: Map<String, String>) {
currentJob?.cancel()
currentJob = ioSafe {
Log.i(TAG, "Loading with url = $url headers = $headers")
clear(true)
retriever.setDataSource(url, headers)
start(this)
}
}
fun load(keepCache: Boolean, context: Context, uri: Uri) {
currentJob?.cancel()
currentJob = ioSafe {
Log.i(TAG, "Loading with uri = $uri")
clear(keepCache)
retriever.setDataSource(context, uri)
start(this)
}
}
override fun release() {
currentJob?.cancel()
clear(false)
}
override var durationMs: Long = 0L
@Throws
@WorkerThread
private fun start(scope: CoroutineScope) {
Log.i(TAG, "Started loading preview")
val durationMs =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong()
?: throw IllegalArgumentException("Bad video duration")
this.durationMs = durationMs
val durationUs = (durationMs * 1000L).toFloat()
//val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: throw IllegalArgumentException("Bad video width")
//val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: throw IllegalArgumentException("Bad video height")
// log2 # 10s durations in the video ~= how many segments we have
val maxLod = ceil(log2((durationMs / 10_000).toFloat())).toInt().coerceIn(MIN_LOD, MAX_LOD)
for (l in 1..maxLod) {
val items = (1 shl (l - 1))
for (i in 0 until items) {
val idx = items - 1 + i // as sum(prev) = cur-1
// frame = 100 / 2^lod + i * 100 / 2^(lod-1) = duration % where lod is one indexed
val fraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat()))
Log.i(TAG, "Generating preview for ${fraction * 100}%")
val frame = durationUs * fraction
val img = retriever.image(frame.toLong(), params);
if (!scope.isActive) return
if (img == null || img.width <= 1 || img.height <= 1) continue
synchronized(images) {
images[idx] = img
loadedImages = maxOf(loadedImages, idx)
}
}
synchronized(images) {
loadedLod = maxOf(loadedLod, l)
}
}
}
}

View file

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -215,10 +216,16 @@ class QuickSearchFragment : Fragment() {
binding?.quickSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
//val searchMagIcon =
// binding.quickSearch.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
// binding?.quickSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
//searchMagIcon?.scaleX = 0.65f
//searchMagIcon?.scaleY = 0.65f
// searchMagIcon?.scaleX = 0.65f
// searchMagIcon?.scaleY = 0.65f
// Set the color for the search exit icon to the correct theme text color
val searchExitIconColor = TypedValue()
activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true)
searchExitIcon?.setColorFilter(searchExitIconColor.data)
binding?.quickSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {

View file

@ -47,7 +47,9 @@ data class ResultEpisode(
/**
* Conveys if the episode itself is marked as watched
**/
val videoWatchState: VideoWatchState
val videoWatchState: VideoWatchState,
/** Sum of all previous season episode counts + episode */
val totalEpisodeIndex: Int? = null,
)
fun ResultEpisode.getRealPosition(): Long {
@ -82,6 +84,7 @@ fun buildResultEpisode(
isFiller: Boolean? = null,
tvType: TvType,
parentId: Int,
totalEpisodeIndex: Int? = null,
): ResultEpisode {
val posDur = getViewPos(id)
val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None
@ -103,7 +106,8 @@ fun buildResultEpisode(
isFiller,
tvType,
parentId,
videoWatchState
videoWatchState,
totalEpisodeIndex
)
}

View file

@ -17,7 +17,6 @@ import android.view.animation.DecelerateInterpolator
import android.widget.AbsListView
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
@ -66,6 +65,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
@ -151,7 +151,8 @@ open class ResultFragmentPhone : FullScreenPlayer() {
startPosition = 0L,
subtitles = emptySet(),
subtitle = null,
autoPlay = false
autoPlay = false,
preview = false
)
true
} ?: run {
@ -429,20 +430,36 @@ open class ResultFragmentPhone : FullScreenPlayer() {
}
})
resultSubscribe.setOnClickListener {
val isSubscribed =
viewModel.toggleSubscriptionStatus() ?: return@setOnClickListener
viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? ->
if (newStatus == null) return@toggleSubscriptionStatus
val message = if (isSubscribed) {
// Kinda icky to have this here, but it works.
SubscriptionWorkManager.enqueuePeriodicWork(context)
R.string.subscription_new
} else {
R.string.subscription_deleted
val message = if (newStatus) {
// Kinda icky to have this here, but it works.
SubscriptionWorkManager.enqueuePeriodicWork(context)
R.string.subscription_new
} else {
R.string.subscription_deleted
}
val name = (viewModel.page.value as? Resource.Success)?.value?.title
?: txt(R.string.no_data).asStringNull(context) ?: ""
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
}
}
resultFavorite.setOnClickListener {
viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? ->
if (newStatus == null) return@toggleFavoriteStatus
val name = (viewModel.page.value as? Resource.Success)?.value?.title
?: txt(R.string.no_data).asStringNull(context) ?: ""
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
val message = if (newStatus) {
R.string.favorite_added
} else {
R.string.favorite_removed
}
val name = (viewModel.page.value as? Resource.Success)?.value?.title
?: txt(R.string.no_data).asStringNull(context) ?: ""
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
}
}
mediaRouteButton.apply {
val chromecastSupport = api?.hasChromecastSupport == true
@ -563,6 +580,19 @@ open class ResultFragmentPhone : FullScreenPlayer() {
binding?.resultSubscribe?.setImageResource(drawable)
}
observeNullable(viewModel.favoriteStatus) { isFavorite ->
binding?.resultFavorite?.isVisible = isFavorite != null
if (isFavorite == null) return@observeNullable
val drawable = if (isFavorite) {
R.drawable.ic_baseline_favorite_24
} else {
R.drawable.ic_baseline_favorite_border_24
}
binding?.resultFavorite?.setImageResource(drawable)
}
observe(viewModel.trailers) { trailers ->
setTrailers(trailers.flatMap { it.mirros }) // I dont care about subtitles yet!
}
@ -653,14 +683,13 @@ open class ResultFragmentPhone : FullScreenPlayer() {
resultPoster.setImage(d.posterImage)
resultPosterBackground.setImage(d.posterBackgroundImage)
resultDescription.setTextHtml(d.plotText)
resultDescription.setOnClickListener { view ->
// todo bottom 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()
resultDescription.setOnClickListener {
activity?.let { activity ->
activity.showBottomDialogText(
d.titleText.asString(activity),
d.plotText.asString(activity).html(),
{}
)
}
}
@ -851,16 +880,11 @@ open class ResultFragmentPhone : FullScreenPlayer() {
setRecommendations(recommendations, null)
}
observe(viewModel.episodeSynopsis) { description ->
// TODO bottom dialog
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()
activity?.let { activity ->
activity.showBottomDialogText(
activity.getString(R.string.synopsis),
description.html()
) { viewModel.releaseEpisodeSynopsis() }
}
}
context?.let { ctx ->
@ -938,7 +962,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
fab.context.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
viewModel.updateWatchStatus(WatchType.values()[it])
viewModel.updateWatchStatus(WatchType.values()[it], context)
}
}
}

View file

@ -8,6 +8,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
import androidx.core.view.isVisible
@ -17,6 +18,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
@ -26,6 +28,7 @@ import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.mvvm.observeNullable
import com.lagradost.cloudstream3.services.SubscriptionWorkManager
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup
import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator
@ -35,6 +38,7 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
@ -216,11 +220,9 @@ class ResultFragmentTv : Fragment() {
episodesShadow.fade(show)
episodeHolderTv.fade(show)
if (episodesShadow.isRtl()) {
episodesShadow.scaleX = -1.0f
episodesShadow.scaleY = -1.0f
episodesShadowBackground.scaleX = -1f
} else {
episodesShadow.scaleX = 1.0f
episodesShadow.scaleY = 1.0f
episodesShadowBackground.scaleX = 1f
}
}
}
@ -247,7 +249,7 @@ class ResultFragmentTv : Fragment() {
binding?.apply {
//episodesShadow.rotationX = 180.0f//if(episodesShadow.isRtl()) 180.0f else 0.0f
val leftListener: View.OnFocusChangeListener =
View.OnFocusChangeListener { _, hasFocus ->
if (!hasFocus) return@OnFocusChangeListener
@ -267,6 +269,7 @@ class ResultFragmentTv : Fragment() {
resultEpisodesShow.onFocusChangeListener = rightListener
resultDescription.onFocusChangeListener = leftListener
resultBookmarkButton.onFocusChangeListener = leftListener
resultFavoriteButton.onFocusChangeListener = leftListener
resultEpisodesShow.setOnClickListener {
// toggle, to make it more touch accessable just in case someone thinks that a
// tv layout is better but is using a touch device
@ -285,7 +288,9 @@ class ResultFragmentTv : Fragment() {
resultPlaySeries,
resultResumeSeries,
resultPlayTrailer,
resultBookmarkButton
resultBookmarkButton,
resultFavoriteButton,
resultSubscribeButton
)
for (requestView in views) {
if (!requestView.isVisible) continue
@ -426,6 +431,8 @@ class ResultFragmentTv : Fragment() {
val aboveCast = listOf(
binding?.resultEpisodesShow,
binding?.resultBookmarkButton,
binding?.resultFavoriteButton,
binding?.resultSubscribeButton,
).firstOrNull {
it?.isVisible == true
}
@ -528,7 +535,83 @@ class ResultFragmentTv : Fragment() {
view.context.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
viewModel.updateWatchStatus(WatchType.values()[it])
viewModel.updateWatchStatus(WatchType.values()[it], context)
}
}
}
}
observeNullable(viewModel.favoriteStatus) { isFavorite ->
binding?.resultFavoriteButton?.apply {
isVisible = isFavorite != null
if (isFavorite == null) return@observeNullable
val drawable = if (isFavorite) {
R.drawable.ic_baseline_favorite_24
} else {
R.drawable.ic_baseline_favorite_border_24
}
val text = if (isFavorite) {
R.string.action_remove_from_favorites
} else {
R.string.action_add_to_favorites
}
setIconResource(drawable)
setText(text)
setOnClickListener {
viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? ->
if (newStatus == null) return@toggleFavoriteStatus
val message = if (newStatus) {
R.string.favorite_added
} else {
R.string.favorite_removed
}
val name = (viewModel.page.value as? Resource.Success)?.value?.title
?: txt(R.string.no_data).asStringNull(context) ?: ""
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
}
}
}
}
observeNullable(viewModel.subscribeStatus) { isSubscribed ->
binding?.resultSubscribeButton?.apply {
isVisible = isSubscribed != null && context.isEmulatorSettings()
if (isSubscribed == null) return@observeNullable
val drawable = if (isSubscribed) {
R.drawable.ic_baseline_notifications_active_24
} else {
R.drawable.baseline_notifications_none_24
}
val text = if (isSubscribed) {
R.string.action_unsubscribe
} else {
R.string.action_subscribe
}
setIconResource(drawable)
setText(text)
setOnClickListener {
viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? ->
if (newStatus == null) return@toggleSubscriptionStatus
val message = if (newStatus) {
// Kinda icky to have this here, but it works.
SubscriptionWorkManager.enqueuePeriodicWork(context)
R.string.subscription_new
} else {
R.string.subscription_deleted
}
val name = (viewModel.page.value as? Resource.Success)?.value?.title
?: txt(R.string.no_data).asStringNull(context) ?: ""
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
}
}
}
@ -537,6 +620,7 @@ class ResultFragmentTv : Fragment() {
observeNullable(viewModel.movie) { data ->
binding?.apply {
resultPlayMovie.isVisible = data is Resource.Success
resultPlaySeries.isVisible = data == null
seriesHolder.isVisible = data == null
resultEpisodesShow.isVisible = data == null
@ -766,12 +850,14 @@ class ResultFragmentTv : Fragment() {
R.drawable.profile_bg_red,
R.drawable.profile_bg_teal
).random()
//Change poster crop area to 20% from Top
backgroundPoster.cropYCenterOffsetPct = 0.20F
backgroundPoster.setImage(
d.posterBackgroundImage ?: UiImage.Drawable(error),
radius = 0,
errorImageDrawable = error
)
resultComingSoon.isVisible = d.comingSoon
resultDataHolder.isGone = d.comingSoon
UIHelper.populateChips(resultTag, d.tags)

View file

@ -7,6 +7,8 @@ import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import androidx.lifecycle.LiveData
@ -31,6 +33,7 @@ import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
@ -45,19 +48,37 @@ import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled
import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.CastHelper.startCast
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub
import com.lagradost.cloudstream3.utils.DataStoreHelper.getFavoritesData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.getSubscribedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getVideoWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.DataStoreHelper.removeFavoritesData
import com.lagradost.cloudstream3.utils.DataStoreHelper.removeSubscribedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub
import com.lagradost.cloudstream3.utils.DataStoreHelper.setFavoritesData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.setSubscribedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import kotlinx.coroutines.*
import java.io.File
@ -110,6 +131,18 @@ data class ResultData(
val plotHeaderText: UiText,
)
data class CheckDuplicateData(
val name: String,
val year: Int?,
val syncData: Map<String, String>?
)
enum class LibraryListType {
BOOKMARKS,
FAVORITES,
SUBSCRIPTIONS
}
fun txt(status: DubStatus?): UiText? {
return txt(
when (status) {
@ -425,6 +458,9 @@ class ResultViewModel2 : ViewModel() {
private val _subscribeStatus: MutableLiveData<Boolean?> = MutableLiveData(null)
val subscribeStatus: LiveData<Boolean?> = _subscribeStatus
private val _favoriteStatus: MutableLiveData<Boolean?> = MutableLiveData(null)
val favoriteStatus: LiveData<Boolean?> = _favoriteStatus
companion object {
const val TAG = "RVM2"
//private const val EPISODE_RANGE_SIZE = 20
@ -435,33 +471,6 @@ class ResultViewModel2 : ViewModel() {
return this?.firstOrNull { it.season == season }
}
fun updateWatchStatus(currentResponse: LoadResponse, status: WatchType) {
val currentId = currentResponse.getId()
val currentWatchType = getResultWatchState(currentId)
DataStoreHelper.setResultWatchState(currentId, status.internalId)
val current = DataStoreHelper.getBookmarkedData(currentId)
val currentTime = System.currentTimeMillis()
DataStoreHelper.setBookmarkedData(
currentId,
DataStoreHelper.BookmarkedData(
currentId,
current?.bookmarkedTime ?: currentTime,
currentTime,
currentResponse.name,
currentResponse.url,
currentResponse.apiName,
currentResponse.type,
currentResponse.posterUrl,
currentResponse.year
)
)
if (currentWatchType != status) {
MainActivity.bookmarksUpdatedEvent(true)
}
}
private fun filterName(name: String?): String? {
if (name == null) return null
Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let {
@ -816,9 +825,77 @@ class ResultViewModel2 : ViewModel() {
val selectPopup: LiveData<SelectPopup?> = _selectPopup
fun updateWatchStatus(status: WatchType) {
updateWatchStatus(currentResponse ?: return, status)
_watchStatus.postValue(status)
fun updateWatchStatus(
status: WatchType,
context: Context?,
loadResponse: LoadResponse? = null,
statusChangedCallback: ((statusChanged: Boolean) -> Unit)? = null
) {
val response = loadResponse ?: currentResponse ?: return
val currentId = response.getId()
val currentStatus = getResultWatchState(currentId)
// If the current status is "NONE" and the new status is not "NONE",
// fetch the bookmarked data to check for duplicates, otherwise set this
// to an empty list, so that we don't show the duplicate warning dialog,
// but we still want to update the current bookmark and refresh the data anyway.
val bookmarkedData = if (currentStatus == WatchType.NONE && status != WatchType.NONE) {
getAllBookmarkedData()
} else emptyList()
checkAndWarnDuplicates(
context,
LibraryListType.BOOKMARKS,
CheckDuplicateData(
name = response.name,
year = response.year,
syncData = response.syncData,
),
bookmarkedData
) { shouldContinue: Boolean, duplicateIds: List<Int?> ->
if (!shouldContinue) return@checkAndWarnDuplicates
if (duplicateIds.isNotEmpty()) {
duplicateIds.forEach { duplicateId ->
deleteBookmarkedData(duplicateId)
}
}
setResultWatchState(currentId, status.internalId)
// We don't need to store if WatchType.NONE.
// The key is removed in setResultWatchState, we don't want to
// re-add it again here if it was just removed.
if (status != WatchType.NONE) {
val current = getBookmarkedData(currentId)
setBookmarkedData(
currentId,
DataStoreHelper.BookmarkedData(
current?.bookmarkedTime ?: unixTimeMS,
currentId,
unixTimeMS,
response.name,
response.url,
response.apiName,
response.type,
response.posterUrl,
response.year,
response.syncData
)
)
}
if (currentStatus != status) {
MainActivity.bookmarksUpdatedEvent(true)
}
_watchStatus.postValue(status)
statusChangedCallback?.invoke(true)
}
}
private fun startChromecast(
@ -833,39 +910,255 @@ class ResultViewModel2 : ViewModel() {
}
/**
* @return true if the new status is Subscribed, false if not. Null if not possible to subscribe.
**/
fun toggleSubscriptionStatus(): Boolean? {
val isSubscribed = _subscribeStatus.value ?: return null
val response = currentResponse ?: return null
if (response !is EpisodeResponse) return null
* Toggles the subscription status of an item.
*
* @param context The context to use for operations.
* @param statusChangedCallback A callback that is invoked when the subscription status changes.
* It provides the new subscription status (true if subscribed, false if unsubscribed, null if action was canceled).
*/
fun toggleSubscriptionStatus(
context: Context?,
statusChangedCallback: ((newStatus: Boolean?) -> Unit)? = null
) {
val isSubscribed = _subscribeStatus.value ?: return
val response = currentResponse ?: return
if (response !is EpisodeResponse) return
val currentId = response.getId()
if (isSubscribed) {
DataStoreHelper.removeSubscribedData(currentId)
removeSubscribedData(currentId)
statusChangedCallback?.invoke(false)
_subscribeStatus.postValue(false)
} else {
val current = DataStoreHelper.getSubscribedData(currentId)
checkAndWarnDuplicates(
context,
LibraryListType.SUBSCRIPTIONS,
CheckDuplicateData(
name = response.name,
year = response.year,
syncData = response.syncData,
),
getAllSubscriptions(),
) { shouldContinue: Boolean, duplicateIds: List<Int?> ->
if (!shouldContinue) {
statusChangedCallback?.invoke(null)
return@checkAndWarnDuplicates
}
DataStoreHelper.setSubscribedData(
currentId,
DataStoreHelper.SubscribedData(
if (duplicateIds.isNotEmpty()) {
duplicateIds.forEach { duplicateId ->
removeSubscribedData(duplicateId)
}
}
val current = getSubscribedData(currentId)
setSubscribedData(
currentId,
current?.bookmarkedTime ?: unixTimeMS,
unixTimeMS,
response.getLatestEpisodes(),
response.name,
response.url,
response.apiName,
response.type,
response.posterUrl,
response.year
DataStoreHelper.SubscribedData(
current?.subscribedTime ?: unixTimeMS,
response.getLatestEpisodes(),
currentId,
unixTimeMS,
response.name,
response.url,
response.apiName,
response.type,
response.posterUrl,
response.year,
response.syncData
)
)
)
_subscribeStatus.postValue(true)
statusChangedCallback?.invoke(true)
}
}
}
/**
* Toggles the favorite status of an item.
*
* @param context The context to use.
* @param statusChangedCallback A callback that is invoked when the favorite status changes.
* It provides the new favorite status (true if added to favorites, false if removed, null if action was canceled).
*/
fun toggleFavoriteStatus(
context: Context?,
statusChangedCallback: ((newStatus: Boolean?) -> Unit)? = null
) {
val isFavorite = _favoriteStatus.value ?: return
val response = currentResponse ?: return
val currentId = response.getId()
if (isFavorite) {
removeFavoritesData(currentId)
statusChangedCallback?.invoke(false)
_favoriteStatus.postValue(false)
} else {
checkAndWarnDuplicates(
context,
LibraryListType.FAVORITES,
CheckDuplicateData(
name = response.name,
year = response.year,
syncData = response.syncData,
),
getAllFavorites(),
) { shouldContinue: Boolean, duplicateIds: List<Int?> ->
if (!shouldContinue) {
statusChangedCallback?.invoke(null)
return@checkAndWarnDuplicates
}
if (duplicateIds.isNotEmpty()) {
duplicateIds.forEach { duplicateId ->
removeFavoritesData(duplicateId)
}
}
val current = getFavoritesData(currentId)
setFavoritesData(
currentId,
DataStoreHelper.FavoritesData(
current?.favoritesTime ?: unixTimeMS,
currentId,
unixTimeMS,
response.name,
response.url,
response.apiName,
response.type,
response.posterUrl,
response.year,
response.syncData
)
)
_favoriteStatus.postValue(true)
statusChangedCallback?.invoke(true)
}
}
}
@MainThread
private fun checkAndWarnDuplicates(
context: Context?,
listType: LibraryListType,
checkDuplicateData: CheckDuplicateData,
data: List<DataStoreHelper.LibrarySearchResponse>,
checkDuplicatesCallback: (shouldContinue: Boolean, duplicateIds: List<Int?>) -> Unit
) {
val whitespaceRegex = "\\s+".toRegex()
fun normalizeString(input: String): String {
/**
* Trim the input string and replace consecutive spaces with a single space.
* This covers some edge-cases where the title does not match exactly across providers,
* and one provider has the title with an extra whitespace. This is minor enough that
* it should still match in this case.
*/
return input.trim().replace(whitespaceRegex, " ")
}
_subscribeStatus.postValue(!isSubscribed)
return !isSubscribed
val syncData = checkDuplicateData.syncData
val imdbId = getImdbIdFromSyncData(syncData)
val tmdbId = getTMDbIdFromSyncData(syncData)
val malId = syncData?.get(AccountManager.malApi.idPrefix)
val aniListId = syncData?.get(AccountManager.aniListApi.idPrefix)
val normalizedName = normalizeString(checkDuplicateData.name)
val year = checkDuplicateData.year
val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse ->
val librarySyncData = it.syncData
val checks = listOf(
{ imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId },
{ tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId },
{ malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId },
{ aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId },
{ normalizedName == normalizeString(it.name) && year == it.year }
)
checks.any { it() }
}
if (duplicateEntries.isEmpty() || context == null) {
checkDuplicatesCallback.invoke(true, emptyList())
return
}
val replaceMessage = if (duplicateEntries.size > 1) {
R.string.duplicate_replace_all
} else R.string.duplicate_replace
val message = if (duplicateEntries.size == 1) {
val list = when (listType) {
LibraryListType.BOOKMARKS -> getResultWatchState(duplicateEntries[0].id ?: 0).stringRes
LibraryListType.FAVORITES -> R.string.favorites_list_name
LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name
}
context.getString(R.string.duplicate_message_single,
"${normalizeString(duplicateEntries[0].name)} (${context.getString(list)}) — ${duplicateEntries[0].apiName}"
)
} else {
val bulletPoints = duplicateEntries.joinToString("\n") {
val list = when (listType) {
LibraryListType.BOOKMARKS -> getResultWatchState(it.id ?: 0).stringRes
LibraryListType.FAVORITES -> R.string.favorites_list_name
LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name
}
"${it.apiName}: ${normalizeString(it.name)} (${context.getString(list)})"
}
context.getString(R.string.duplicate_message_multiple, bulletPoints)
}
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
val dialogClickListener =
DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
checkDuplicatesCallback.invoke(true, emptyList())
}
DialogInterface.BUTTON_NEGATIVE -> {
checkDuplicatesCallback.invoke(false, emptyList())
}
DialogInterface.BUTTON_NEUTRAL -> {
checkDuplicatesCallback.invoke(true, duplicateEntries.map { it.id })
}
}
}
builder.setTitle(R.string.duplicate_title)
.setMessage(message)
.setPositiveButton(R.string.duplicate_add, dialogClickListener)
.setNegativeButton(R.string.duplicate_cancel, dialogClickListener)
.setNeutralButton(replaceMessage, dialogClickListener)
.show().setDefaultFocus()
}
private fun getImdbIdFromSyncData(syncData: Map<String, String>?): String? {
return normalSafeApiCall {
SimklApi.readIdFromString(
syncData?.get(AccountManager.simklApi.idPrefix)
)[SimklApi.Companion.SyncServices.Imdb]
}
}
private fun getTMDbIdFromSyncData(syncData: Map<String, String>?): String? {
return normalSafeApiCall {
SimklApi.readIdFromString(
syncData?.get(AccountManager.simklApi.idPrefix)
)[SimklApi.Companion.SyncServices.Tmdb]
}
}
private fun startChromecast(
@ -1219,7 +1512,7 @@ class ResultViewModel2 : ViewModel() {
// 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
getVideoWatchState(click.data.id) == VideoWatchState.Watched
val watchedText = if (isWatched) R.string.action_remove_from_watched
else R.string.action_mark_as_watched
@ -1468,12 +1761,12 @@ class ResultViewModel2 : ViewModel() {
ACTION_MARK_AS_WATCHED -> {
val isWatched =
DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched
getVideoWatchState(click.data.id) == VideoWatchState.Watched
if (isWatched) {
DataStoreHelper.setVideoWatchState(click.data.id, VideoWatchState.None)
setVideoWatchState(click.data.id, VideoWatchState.None)
} else {
DataStoreHelper.setVideoWatchState(click.data.id, VideoWatchState.Watched)
setVideoWatchState(click.data.id, VideoWatchState.Watched)
}
// Kinda dirty to reload all episodes :(
@ -1682,7 +1975,7 @@ class ResultViewModel2 : ViewModel() {
list.subList(start, end).map {
val posDur = getViewPos(it.id)
val watchState =
DataStoreHelper.getVideoWatchState(it.id) ?: VideoWatchState.None
getVideoWatchState(it.id) ?: VideoWatchState.None
it.copy(
position = posDur?.position ?: 0,
duration = posDur?.duration ?: 0,
@ -1743,13 +2036,19 @@ class ResultViewModel2 : ViewModel() {
private fun postSubscription(loadResponse: LoadResponse) {
if (loadResponse.isEpisodeBased()) {
val id = loadResponse.getId()
val data = DataStoreHelper.getSubscribedData(id)
DataStoreHelper.updateSubscribedData(id, data, loadResponse as? EpisodeResponse)
val data = getSubscribedData(id)
updateSubscribedData(id, data, loadResponse as? EpisodeResponse)
val isSubscribed = data != null
_subscribeStatus.postValue(isSubscribed)
}
}
private fun postFavorites(loadResponse: LoadResponse) {
val id = loadResponse.getId()
val isFavorite = getFavoritesData(id) != null
_favoriteStatus.postValue(isFavorite)
}
private fun postEpisodeRange(indexer: EpisodeIndexer?, range: EpisodeRange?) {
if (range == null || indexer == null) {
return
@ -1887,6 +2186,7 @@ class ResultViewModel2 : ViewModel() {
currentResponse = loadResponse
postPage(loadResponse, apiRepository)
postSubscription(loadResponse)
postFavorites(loadResponse)
if (updateEpisodes)
postEpisodes(loadResponse, updateFillers)
}
@ -1915,6 +2215,10 @@ class ResultViewModel2 : ViewModel() {
val id =
mainId + episode + idIndex * 1_000_000 + (i.season?.times(10_000)
?: 0)
val totalIndex =
i.season?.let { season -> loadResponse.getTotalEpisodeIndex(episode, season) }
if (!existingEpisodes.contains(id)) {
existingEpisodes.add(id)
val seasonData = loadResponse.seasonNames.getSeason(i.season)
@ -1934,7 +2238,8 @@ class ResultViewModel2 : ViewModel() {
i.description,
fillers.getOrDefault(episode, false),
loadResponse.type,
mainId
mainId,
totalIndex
)
val season = eps.seasonIndex ?: 0
@ -1963,6 +2268,9 @@ class ResultViewModel2 : ViewModel() {
val seasonData =
loadResponse.seasonNames.getSeason(episode.season)
val totalIndex =
episode.season?.let { season -> loadResponse.getTotalEpisodeIndex(episodeIndex, season) }
val ep =
buildResultEpisode(
loadResponse.name,
@ -1979,7 +2287,8 @@ class ResultViewModel2 : ViewModel() {
episode.description,
null,
loadResponse.type,
mainId
mainId,
totalIndex
)
val season = ep.seasonIndex ?: 0
@ -2010,7 +2319,8 @@ class ResultViewModel2 : ViewModel() {
null,
null,
loadResponse.type,
mainId
mainId,
null
)
)
}
@ -2032,7 +2342,8 @@ class ResultViewModel2 : ViewModel() {
null,
null,
loadResponse.type,
mainId
mainId,
null
)
)
}
@ -2054,7 +2365,8 @@ class ResultViewModel2 : ViewModel() {
null,
null,
loadResponse.type,
mainId
mainId,
null
)
)
}
@ -2115,13 +2427,13 @@ class ResultViewModel2 : ViewModel() {
postResume()
}
fun postResume() {
private fun postResume() {
_resumeWatching.postValue(resume())
}
private fun resume(): ResumeWatchingStatus? {
val correctId = currentId ?: return null
val resume = DataStoreHelper.getLastWatched(correctId)
val resume = getLastWatched(correctId)
val resumeParentId = resume?.parentId
if (resumeParentId != correctId) return null // is null or smth went wrong with getLastWatched
val resumeId = resume.episodeId ?: return null// invalid episode id

View file

@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui.search
import android.content.DialogInterface
import android.content.res.Configuration
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -11,6 +12,7 @@ import android.widget.AbsListView
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
@ -22,17 +24,22 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.APIHolder.getApiSettings
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.AllLanguagesName
import com.lagradost.cloudstream3.AnimeSearchResponse
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.databinding.FragmentSearchBinding
import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding
import com.lagradost.cloudstream3.mvvm.Resource
@ -53,8 +60,8 @@ 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
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
@ -62,9 +69,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import java.util.concurrent.locks.ReentrantLock
const val SEARCH_PREF_TAGS = "search_pref_tags"
const val SEARCH_PREF_PROVIDERS = "search_pref_providers"
class SearchFragment : Fragment() {
companion object {
fun List<SearchResponse>.filterSearchResponse(): List<SearchResponse> {
@ -193,7 +197,7 @@ class SearchFragment : Fragment() {
validAPIs.flatMap { api -> api.supportedTypes }.distinct()
) { list ->
if (selectedSearchTypes.toSet() != list.toSet()) {
setKey(SEARCH_PREF_TAGS, selectedSearchTypes)
DataStoreHelper.searchPreferenceTags = list
selectedSearchTypes.clear()
selectedSearchTypes.addAll(list)
search(binding?.mainSearch?.query?.toString())
@ -219,7 +223,7 @@ class SearchFragment : Fragment() {
SearchHelper.handleSearchClickCallback(callback)
}
searchRoot.findViewById<TextView>(R.id.search_src_text)?.tag = "tv_no_focus_tag"
searchAutofitResults.adapter = adapter
searchLoadingBar.alpha = 0f
}
@ -228,17 +232,17 @@ class SearchFragment : Fragment() {
val searchExitIcon =
binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
// val searchMagIcon =
// main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
//searchMagIcon.scaleX = 0.65f
//searchMagIcon.scaleY = 0.65f
// binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
// searchMagIcon.scaleX = 0.65f
// searchMagIcon.scaleY = 0.65f
context?.let { ctx ->
val validAPIs = ctx.filterProviderByPreferredMedia()
selectedApis = ctx.getKey(
SEARCH_PREF_PROVIDERS,
defVal = validAPIs.map { it.name }
)!!.toMutableSet()
}
// Set the color for the search exit icon to the correct theme text color
val searchExitIconColor = TypedValue()
activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true)
searchExitIcon?.setColorFilter(searchExitIconColor.data)
selectedApis = DataStoreHelper.searchPreferenceProviders.toMutableSet()
binding?.searchFilter?.setOnClickListener { searchView ->
searchView?.context?.let { ctx ->
@ -286,7 +290,7 @@ class SearchFragment : Fragment() {
}
fun updateList(types: List<TvType>) {
setKey(SEARCH_PREF_TAGS, types.map { it.name })
DataStoreHelper.searchPreferenceTags = types
arrayAdapter.clear()
currentValidApis = validAPIs.filter { api ->
@ -311,12 +315,7 @@ class SearchFragment : Fragment() {
arrayAdapter.notifyDataSetChanged()
}
val selectedSearchTypes = getKey<List<String>>(SEARCH_PREF_TAGS)
?.mapNotNull { listName ->
TvType.values().firstOrNull { it.name == listName }
}
?.toMutableList()
?: mutableListOf(TvType.Movie, TvType.TvSeries)
val selectedSearchTypes = DataStoreHelper.searchPreferenceTags
bindChips(
binding.tvtypesChipsScroll.tvtypesChips,
@ -342,7 +341,7 @@ class SearchFragment : Fragment() {
}
dialog.setOnDismissListener {
context?.setKey(SEARCH_PREF_PROVIDERS, currentSelectedApis.toList())
DataStoreHelper.searchPreferenceProviders = currentSelectedApis.toList()
selectedApis = currentSelectedApis
}
updateList(selectedSearchTypes.toList())
@ -353,10 +352,7 @@ class SearchFragment : Fragment() {
val settingsManager = context?.let { PreferenceManager.getDefaultSharedPreferences(it) }
val isAdvancedSearch = settingsManager?.getBoolean("advanced_search", true) ?: true
selectedSearchTypes = context?.getKey<List<String>>(SEARCH_PREF_TAGS)
?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } }
?.toMutableList()
?: mutableListOf(TvType.Movie, TvType.TvSeries)
selectedSearchTypes = DataStoreHelper.searchPreferenceTags.toMutableList()
if (isTrueTvSettings()) {
binding?.searchFilter?.isFocusable = true
@ -398,7 +394,7 @@ class SearchFragment : Fragment() {
DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
removeKeys(SEARCH_HISTORY_KEY)
removeKeys("$currentAccount/$SEARCH_HISTORY_KEY")
searchViewModel.updateHistory()
}
DialogInterface.BUTTON_NEGATIVE -> {
@ -510,7 +506,7 @@ class SearchFragment : Fragment() {
binding?.mainSearch?.setQuery(searchItem.searchText, true)
}
SEARCH_HISTORY_REMOVE -> {
removeKey(SEARCH_HISTORY_KEY, searchItem.key)
removeKey("$currentAccount/$SEARCH_HISTORY_KEY", searchItem.key)
searchViewModel.updateHistory()
}
else -> {
@ -559,4 +555,4 @@ class SearchFragment : Fragment() {
.commit()*/
}
}
}

View file

@ -14,6 +14,7 @@ import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@ -64,7 +65,7 @@ class SearchViewModel : ViewModel() {
fun updateHistory() = viewModelScope.launch {
ioSafe {
val items = getKeys(SEARCH_HISTORY_KEY)?.mapNotNull {
val items = getKeys("$currentAccount/$SEARCH_HISTORY_KEY")?.mapNotNull {
getKey<SearchHistoryItem>(it)
}?.sortedByDescending { it.searchedAt } ?: emptyList()
_currentHistory.postValue(items)
@ -87,7 +88,7 @@ class SearchViewModel : ViewModel() {
if (!isQuickSearch) {
val key = query.hashCode().toString()
setKey(
SEARCH_HISTORY_KEY,
"$currentAccount/$SEARCH_HISTORY_KEY",
key,
SearchHistoryItem(
searchedAt = System.currentTimeMillis(),
@ -140,4 +141,4 @@ class SearchViewModel : ViewModel() {
_searchResponse.postValue(Resource.Success(list))
}
}
}
}

View file

@ -194,13 +194,13 @@ class SettingsFragment : Fragment() {
}
binding?.apply {
listOf(
settingsGeneral to R.id.action_navigation_settings_to_navigation_settings_general,
settingsPlayer to R.id.action_navigation_settings_to_navigation_settings_player,
settingsCredits to R.id.action_navigation_settings_to_navigation_settings_account,
settingsUi to R.id.action_navigation_settings_to_navigation_settings_ui,
settingsProviders to R.id.action_navigation_settings_to_navigation_settings_providers,
settingsUpdates to R.id.action_navigation_settings_to_navigation_settings_updates,
settingsExtensions to R.id.action_navigation_settings_to_navigation_settings_extensions,
settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general,
settingsPlayer to R.id.action_navigation_global_to_navigation_settings_player,
settingsCredits to R.id.action_navigation_global_to_navigation_settings_account,
settingsUi to R.id.action_navigation_global_to_navigation_settings_ui,
settingsProviders to R.id.action_navigation_global_to_navigation_settings_providers,
settingsUpdates to R.id.action_navigation_global_to_navigation_settings_updates,
settingsExtensions to R.id.action_navigation_global_to_navigation_settings_extensions,
).forEach { (view, navigationId) ->
view.apply {
setOnClickListener {
@ -235,4 +235,4 @@ class SettingsFragment : Fragment() {
}
}
}
}
}

View file

@ -82,6 +82,7 @@ val appLanguages = arrayListOf(
Triple("", "日本語 (にほんご)", "ja"),
Triple("", "ಕನ್ನಡ", "kn"),
Triple("", "한국어", "ko"),
Triple("", "lietuvių kalba", "lt"),
Triple("", "latviešu valoda", "lv"),
Triple("", "македонски", "mk"),
Triple("", "മലയാളം", "ml"),

View file

@ -20,15 +20,17 @@ import com.lagradost.cloudstream3.databinding.LogcatBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
import com.lagradost.cloudstream3.services.BackupWorkManager
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.utils.BackupUtils.backup
import com.lagradost.cloudstream3.utils.BackupUtils
import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.VideoDownloadManager
@ -50,7 +52,30 @@ class SettingsUpdates : PreferenceFragmentCompat() {
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
getPref(R.string.backup_key)?.setOnPreferenceClickListener {
activity?.backup()
BackupUtils.backup(activity)
return@setOnPreferenceClickListener true
}
getPref(R.string.automatic_backup_key)?.setOnPreferenceClickListener {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
val prefNames = resources.getStringArray(R.array.periodic_work_names)
val prefValues = resources.getIntArray(R.array.periodic_work_values)
val current = settingsManager.getInt(getString(R.string.automatic_backup_key), 0)
activity?.showDialog(
prefNames.toList(),
prefValues.indexOf(current),
getString(R.string.backup_frequency),
true,
{}) { index ->
settingsManager.edit()
.putInt(getString(R.string.automatic_backup_key), prefValues[index]).apply()
BackupWorkManager.enqueuePeriodicWork(
context ?: AcraApplication.context,
prefValues[index].toLong()
)
}
return@setOnPreferenceClickListener true
}
@ -67,7 +92,7 @@ class SettingsUpdates : PreferenceFragmentCompat() {
val builder =
AlertDialog.Builder(pref.context, R.style.AlertDialogCustom)
val binding = LogcatBinding.inflate(layoutInflater,null,false )
val binding = LogcatBinding.inflate(layoutInflater, null, false)
builder.setView(binding.root)
val dialog = builder.create()
@ -179,7 +204,8 @@ class SettingsUpdates : PreferenceFragmentCompat() {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context)
val prefNames = resources.getStringArray(R.array.auto_download_plugin)
val prefValues = enumValues<AutoDownloadMode>().sortedBy { x -> x.value }.map { x -> x.value }
val prefValues =
enumValues<AutoDownloadMode>().sortedBy { x -> x.value }.map { x -> x.value }
val current = settingsManager.getInt(getString(R.string.auto_download_plugins_key), 0)
@ -189,7 +215,8 @@ class SettingsUpdates : PreferenceFragmentCompat() {
getString(R.string.automatic_plugin_download_mode_title),
true,
{}) {
settingsManager.edit().putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply()
settingsManager.edit()
.putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply()
(context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) }
}
return@setOnPreferenceClickListener true

View file

@ -583,7 +583,7 @@ object AppUtils {
//private val viewModel: ResultViewModel by activityViewModels()
private fun getResultsId(): Int {
return if (isTrueTvSettings()) {
return if (isTvSettings()) {
R.id.global_to_navigation_results_tv
} else {
R.id.global_to_navigation_results_phone

View file

@ -11,6 +11,7 @@ import androidx.annotation.WorkerThread
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity.Companion.afterBackupRestoreEvent
import com.lagradost.cloudstream3.R
@ -150,10 +151,12 @@ object BackupUtils {
}
@Suppress("UNCHECKED_CAST")
fun Context.getBackup(): BackupFile {
private fun getBackup(context: Context?): BackupFile? {
if (context == null) return null
val syncDataPrefs = getSyncPrefs().all.filter { it.key.isTransferable() }
val allData = getSharedPrefs().all.filter { it.key.isTransferable() }
val allSettings = getDefaultSharedPrefs().all.filter { it.key.isTransferable() }
val allData = context.getSharedPrefs().all.filter { it.key.isTransferable() }
val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() }
val syncData = BackupVars(
syncDataPrefs.filter { it.value is Boolean } as? Map<String, Boolean>,
@ -226,21 +229,23 @@ object BackupUtils {
}
@SuppressLint("SimpleDateFormat")
fun FragmentActivity.backup() = ioSafe {
fun backup(context: Context?) = ioSafe {
if (context == null) return@ioSafe
var fileStream: OutputStream? = null
var printStream: PrintWriter? = null
try {
if (!checkWrite()) {
if (!context.checkWrite()) {
showToast(R.string.backup_failed, Toast.LENGTH_LONG)
requestRW()
context.getActivity()?.requestRW()
return@ioSafe
}
val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis()))
val ext = "txt"
val displayName = "CS3_Backup_${date}"
val backupFile = getBackup()
val stream = setupStream(this@backup, displayName, null, ext, false)
val backupFile = getBackup(context)
val stream = setupStream(context, displayName, null, ext, false)
fileStream = stream.openNew()
printStream = PrintWriter(fileStream)

View file

@ -6,11 +6,14 @@ import android.text.Editable
import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import com.fasterxml.jackson.annotation.JsonProperty
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
@ -24,19 +27,23 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.WhoIsWatchingAdapter
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
import com.lagradost.cloudstream3.ui.account.AccountDialog.showPinInputDialog
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.UiImage
import com.lagradost.cloudstream3.ui.result.VideoWatchState
import com.lagradost.cloudstream3.ui.result.setImage
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
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_SUBSCRIBED_STATE_DATA = "result_subscribed_state_data"
const val RESULT_FAVORITES_STATE_DATA = "result_favorites_state_data"
const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // changed due to id changes
const val RESULT_RESUME_WATCHING_OLD = "result_resume_watching"
const val RESULT_RESUME_WATCHING_HAS_MIGRATED = "result_resume_watching_migrated"
@ -44,6 +51,28 @@ const val RESULT_EPISODE = "result_episode"
const val RESULT_SEASON = "result_season"
const val RESULT_DUB = "result_dub"
class UserPreferenceDelegate<T : Any>(
private val key: String, private val default: T //, private val klass: KClass<T>
) {
private val klass: KClass<out T> = default::class
private val realKey get() = "${DataStoreHelper.currentAccount}/$key"
operator fun getValue(self: Any?, property: KProperty<*>) =
AcraApplication.getKeyClass(realKey, klass.java) ?: default
operator fun setValue(
self: Any?,
property: KProperty<*>,
t: T?
) {
if (t == null) {
removeKey(realKey)
} else {
AcraApplication.setKeyClass(realKey, t)
}
}
}
object DataStoreHelper {
// be aware, don't change the index of these as Account uses the index for the art
private val profileImages = arrayOf(
@ -56,6 +85,49 @@ object DataStoreHelper {
R.drawable.profile_bg_teal
)
private var searchPreferenceProvidersStrings : List<String> by UserPreferenceDelegate(
/** java moment right here, as listOf()::class.java != List(0) { "" }::class.java */
"search_pref_providers", List(0) { "" }
)
private fun serializeTv(data : List<TvType>) : List<String> = data.map { it.name }
private fun deserializeTv(data : List<String>) : List<TvType> {
return data.mapNotNull { listName ->
TvType.values().firstOrNull { it.name == listName }
}
}
var searchPreferenceProviders : List<String>
get() {
val ret = searchPreferenceProvidersStrings
return ret.ifEmpty {
context?.filterProviderByPreferredMedia()?.map { it.name } ?: emptyList()
}
} set(value) {
searchPreferenceProvidersStrings = value
}
private var searchPreferenceTagsStrings : List<String> by UserPreferenceDelegate("search_pref_tags", listOf(TvType.Movie, TvType.TvSeries).map { it.name })
var searchPreferenceTags : List<TvType>
get() = deserializeTv(searchPreferenceTagsStrings)
set(value) {
searchPreferenceTagsStrings = serializeTv(value)
}
private var homePreferenceStrings : List<String> by UserPreferenceDelegate("home_pref_homepage", listOf(TvType.Movie, TvType.TvSeries).map { it.name })
var homePreference : List<TvType>
get() = deserializeTv(homePreferenceStrings)
set(value) {
homePreferenceStrings = serializeTv(value)
}
var homeBookmarkedList : IntArray by UserPreferenceDelegate("home_bookmarked_last_list", IntArray(0))
var playBackSpeed : Float by UserPreferenceDelegate("playback_speed", 1.0f)
var resizeMode : Int by UserPreferenceDelegate("resize_mode", 0)
var librarySortingMode : Int by UserPreferenceDelegate("library_sorting_mode", ListSorting.AlphabeticalA.ordinal)
data class Account(
@JsonProperty("keyIndex")
val keyIndex: Int,
@ -65,6 +137,8 @@ object DataStoreHelper {
val customImage: String? = null,
@JsonProperty("defaultImageIndex")
val defaultImageIndex: Int,
@JsonProperty("lockPin")
val lockPin: String? = null,
) {
val image: UiImage
get() = customImage?.let { UiImage.Image(it) } ?: UiImage.Drawable(
@ -131,7 +205,6 @@ object DataStoreHelper {
// update UI
setAccount(getDefaultAccount(context), true)
MainActivity.bookmarksUpdatedEvent(true)
dialog?.dismissSafe()
}
@ -160,36 +233,86 @@ object DataStoreHelper {
binding.profilePic.setImage(account.image)
binding.profilePic.setOnClickListener {
// rolls the image forwards once
// Roll the image forwards once
currentEditAccount =
currentEditAccount.copy(defaultImageIndex = (currentEditAccount.defaultImageIndex + 1) % profileImages.size)
binding.profilePic.setImage(currentEditAccount.image)
}
binding.applyBtt.setOnClickListener {
val currentAccounts = accounts.toMutableList()
val overrideIndex =
currentAccounts.indexOfFirst { it.keyIndex == currentEditAccount.keyIndex }
// if an account is found that has the same keyIndex then override that one, if not then append it
if (overrideIndex != -1) {
currentAccounts[overrideIndex] = currentEditAccount
if (currentEditAccount.lockPin != null) {
// Ask for the current PIN
showPinInputDialog(context, currentEditAccount.lockPin, false) { pin ->
if (pin == null) return@showPinInputDialog
// PIN is correct, proceed to update the account
performAccountUpdate(currentEditAccount)
dialog.dismissSafe()
}
} else {
currentAccounts.add(currentEditAccount)
// No lock PIN set, proceed to update the account
performAccountUpdate(currentEditAccount)
dialog.dismissSafe()
}
// Save the current homepage for new accounts
val currentHomePage = DataStoreHelper.currentHomePage
// set the new default account as well as add the key for the new account
setAccount(currentEditAccount, false)
DataStoreHelper.currentHomePage = currentHomePage
accounts = currentAccounts.toTypedArray()
dialog.dismissSafe()
}
// Handle setting or changing the PIN
if (currentEditAccount.keyIndex == getDefaultAccount(context).keyIndex) {
binding.lockProfileCheckbox.isVisible = false
if (currentEditAccount.lockPin != null) {
currentEditAccount = currentEditAccount.copy(lockPin = null)
}
}
var canSetPin = true
binding.lockProfileCheckbox.isChecked = currentEditAccount.lockPin != null
binding.lockProfileCheckbox.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
if (canSetPin) {
showPinInputDialog(context, null, true) { pin ->
if (pin == null) {
binding.lockProfileCheckbox.isChecked = false
return@showPinInputDialog
}
currentEditAccount = currentEditAccount.copy(lockPin = pin)
}
}
} else {
if (currentEditAccount.lockPin != null) {
// Ask for the current PIN
showPinInputDialog(context, currentEditAccount.lockPin, true) { pin ->
if (pin == null || pin != currentEditAccount.lockPin) {
canSetPin = false
binding.lockProfileCheckbox.isChecked = true
} else {
currentEditAccount = currentEditAccount.copy(lockPin = null)
}
}
}
}
}
canSetPin = true
}
private fun performAccountUpdate(account: Account) {
val currentAccounts = accounts.toMutableList()
val overrideIndex = currentAccounts.indexOfFirst { it.keyIndex == account.keyIndex }
if (overrideIndex != -1) {
currentAccounts[overrideIndex] = account
} else {
currentAccounts.add(account)
}
val currentHomePage = this.currentHomePage
setAccount(account, false)
this.currentHomePage = currentHomePage
accounts = currentAccounts.toTypedArray()
}
private fun getDefaultAccount(context: Context): Account {
@ -202,10 +325,18 @@ object DataStoreHelper {
}
}
fun getAccounts(context: Context): List<Account> {
return accounts.toMutableList().apply {
val item = getDefaultAccount(context)
remove(item)
add(0, item)
}
}
fun showWhoIsWatching(context: Context) {
val binding: WhoIsWatchingBinding = WhoIsWatchingBinding.inflate(
LayoutInflater.from(context)
)
val binding: WhoIsWatchingBinding = WhoIsWatchingBinding.inflate(LayoutInflater.from(context))
val builder = BottomSheetDialog(context)
builder.setContentView(binding.root)
val showAccount = accounts.toMutableList().apply {
val item = getDefaultAccount(context)
@ -213,22 +344,25 @@ object DataStoreHelper {
add(0, item)
}
val builder =
BottomSheetDialog(context)
builder.setContentView(binding.root)
val accountName = context.getString(R.string.account)
binding.profilesRecyclerview.setLinearListLayout(
isHorizontal = true,
nextUp = FOCUS_SELF,
nextDown = FOCUS_SELF,
nextLeft = FOCUS_SELF,
nextRight = FOCUS_SELF
)
binding.profilesRecyclerview.setLinearListLayout(isHorizontal = true)
binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter(
selectCallBack = { account ->
setAccount(account, true)
builder.dismissSafe()
// Check if the selected account has a lock PIN set
if (account.lockPin != null) {
// Prompt for the lock pin
showPinInputDialog(context, account.lockPin, false) { pin ->
if (pin == null) return@showPinInputDialog
// Pin is correct, unlock the profile
setAccount(account, true)
builder.dismissSafe()
}
} else {
// No lock PIN set, directly set the account
setAccount(account, true)
builder.dismissSafe()
}
},
addAccountCallback = {
val currentAccounts = accounts
@ -264,7 +398,6 @@ object DataStoreHelper {
builder.show()
}
data class PosDur(
@JsonProperty("position") val position: Long,
@JsonProperty("duration") val duration: Long
@ -282,20 +415,35 @@ object DataStoreHelper {
/**
* Used to display notifications on new episodes and posters in library.
**/
data class SubscribedData(
abstract class LibrarySearchResponse(
@JsonProperty("id") override var id: Int?,
@JsonProperty("subscribedTime") val bookmarkedTime: Long,
@JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long,
@JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map<DubStatus, Int?>,
@JsonProperty("latestUpdatedTime") open val latestUpdatedTime: Long,
@JsonProperty("name") override val name: String,
@JsonProperty("url") override val url: String,
@JsonProperty("apiName") override val apiName: String,
@JsonProperty("type") override var type: TvType? = null,
@JsonProperty("type") override var type: TvType?,
@JsonProperty("posterUrl") override var posterUrl: String?,
@JsonProperty("year") val year: Int?,
@JsonProperty("quality") override var quality: SearchQuality? = null,
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
) : SearchResponse {
@JsonProperty("year") open val year: Int?,
@JsonProperty("syncData") open val syncData: Map<String, String>?,
@JsonProperty("quality") override var quality: SearchQuality?,
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>?
) : SearchResponse
data class SubscribedData(
@JsonProperty("subscribedTime") val subscribedTime: Long,
@JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map<DubStatus, Int?>,
override var id: Int?,
override val latestUpdatedTime: Long,
override val name: String,
override val url: String,
override val apiName: String,
override var type: TvType?,
override var posterUrl: String?,
override val year: Int?,
override val syncData: Map<String, String>? = null,
override var quality: SearchQuality? = null,
override var posterHeaders: Map<String, String>? = null
) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) {
fun toLibraryItem(): SyncAPI.LibraryItem? {
return SyncAPI.LibraryItem(
name,
@ -311,18 +459,19 @@ object DataStoreHelper {
}
data class BookmarkedData(
@JsonProperty("id") override var id: Int?,
@JsonProperty("bookmarkedTime") val bookmarkedTime: Long,
@JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long,
@JsonProperty("name") override val name: String,
@JsonProperty("url") override val url: String,
@JsonProperty("apiName") override val apiName: String,
@JsonProperty("type") override var type: TvType? = null,
@JsonProperty("posterUrl") override var posterUrl: String?,
@JsonProperty("year") val year: Int?,
@JsonProperty("quality") override var quality: SearchQuality? = null,
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
) : SearchResponse {
override var id: Int?,
override val latestUpdatedTime: Long,
override val name: String,
override val url: String,
override val apiName: String,
override var type: TvType?,
override var posterUrl: String?,
override val year: Int?,
override val syncData: Map<String, String>? = null,
override var quality: SearchQuality? = null,
override var posterHeaders: Map<String, String>? = null
) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) {
fun toLibraryItem(id: String): SyncAPI.LibraryItem {
return SyncAPI.LibraryItem(
name,
@ -337,6 +486,34 @@ object DataStoreHelper {
}
}
data class FavoritesData(
@JsonProperty("favoritesTime") val favoritesTime: Long,
override var id: Int?,
override val latestUpdatedTime: Long,
override val name: String,
override val url: String,
override val apiName: String,
override var type: TvType?,
override var posterUrl: String?,
override val year: Int?,
override val syncData: Map<String, String>? = null,
override var quality: SearchQuality? = null,
override var posterHeaders: Map<String, String>? = null
) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) {
fun toLibraryItem(): SyncAPI.LibraryItem? {
return SyncAPI.LibraryItem(
name,
url,
id?.toString() ?: return null,
null,
null,
null,
latestUpdatedTime,
apiName, type, posterUrl, posterHeaders, quality, this.id
)
}
}
data class ResumeWatchingResult(
@JsonProperty("name") override val name: String,
@JsonProperty("url") override val url: String,
@ -369,15 +546,9 @@ object DataStoreHelper {
removeKeys(folder)
}
fun deleteAllBookmarkedData() {
val folder1 = "$currentAccount/$RESULT_WATCH_STATE"
val folder2 = "$currentAccount/$RESULT_WATCH_STATE_DATA"
removeKeys(folder1)
removeKeys(folder2)
}
fun deleteBookmarkedData(id: Int?) {
if (id == null) return
AccountManager.localListApi.requireLibraryRefresh = true
removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString())
removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString())
}
@ -475,6 +646,12 @@ object DataStoreHelper {
return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString())
}
fun getAllBookmarkedData(): List<BookmarkedData> {
return getKeys("$currentAccount/$RESULT_WATCH_STATE_DATA")?.mapNotNull {
getKey(it)
} ?: emptyList()
}
fun getAllSubscriptions(): List<SubscribedData> {
return getKeys("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA")?.mapNotNull {
getKey(it)
@ -510,6 +687,29 @@ object DataStoreHelper {
return getKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString())
}
fun getAllFavorites(): List<FavoritesData> {
return getKeys("$currentAccount/$RESULT_FAVORITES_STATE_DATA")?.mapNotNull {
getKey(it)
} ?: emptyList()
}
fun removeFavoritesData(id: Int?) {
if (id == null) return
AccountManager.localListApi.requireLibraryRefresh = true
removeKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString())
}
fun setFavoritesData(id: Int?, data: FavoritesData) {
if (id == null) return
setKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString(), data)
AccountManager.localListApi.requireLibraryRefresh = true
}
fun getFavoritesData(id: Int?): FavoritesData? {
if (id == null) return null
return getKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString())
}
fun setViewPos(id: Int?, pos: Long, dur: Long) {
if (id == null) return
if (dur < 30_000) return // too short

View file

@ -82,6 +82,7 @@ import com.lagradost.cloudstream3.extractors.Maxstream
import com.lagradost.cloudstream3.extractors.Mcloud
import com.lagradost.cloudstream3.extractors.Megacloud
import com.lagradost.cloudstream3.extractors.Meownime
import com.lagradost.cloudstream3.extractors.Minoplres
import com.lagradost.cloudstream3.extractors.MixDrop
import com.lagradost.cloudstream3.extractors.MixDropBz
import com.lagradost.cloudstream3.extractors.MixDropCh
@ -118,9 +119,6 @@ import com.lagradost.cloudstream3.extractors.Sbthe
import com.lagradost.cloudstream3.extractors.Sendvid
import com.lagradost.cloudstream3.extractors.ShaveTape
import com.lagradost.cloudstream3.extractors.Solidfiles
import com.lagradost.cloudstream3.extractors.SpeedoStream
import com.lagradost.cloudstream3.extractors.SpeedoStream1
import com.lagradost.cloudstream3.extractors.SpeedoStream2
import com.lagradost.cloudstream3.extractors.Ssbstream
import com.lagradost.cloudstream3.extractors.StreamM4u
import com.lagradost.cloudstream3.extractors.StreamSB
@ -380,6 +378,15 @@ open class ExtractorLink constructor(
val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8
val isDash : Boolean get() = type == ExtractorLinkType.DASH
fun getAllHeaders() : Map<String, String> {
if (referer.isBlank()) {
return headers
} else if (headers.keys.none { it.equals("referer", ignoreCase = true) }) {
return headers + mapOf("referer" to referer)
}
return headers
}
constructor(
source: String,
name: String,
@ -748,9 +755,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Vido(),
Linkbox(),
Acefile(),
SpeedoStream(),
SpeedoStream1(),
SpeedoStream2(),
Minoplres(), // formerly SpeedoStream
Zorofile(),
Embedgram(),
Mvidoo(),

View file

@ -8,6 +8,7 @@ import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
@ -27,6 +28,10 @@ class GlideModule : AppGlideModule() {
RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.signature(ObjectKey(System.currentTimeMillis().toShort()))
}.setDiskCache {
// Possible to make this a setting in the future.
val memoryCacheSizeBytes: Long = 1024 * 1024 * 100; // 100mb
InternalCacheDiskCacheFactory(context, memoryCacheSizeBytes).build()
}
}

View file

@ -71,7 +71,7 @@ object M3u8Helper2 {
private val QUALITY_REGEX =
Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""")
private val TS_EXTENSION_REGEX =
Regex("""#EXTINF:.*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways
Regex("""#EXTINF:(([0-9]*[.])?[0-9]+|).*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways
//Regex("""(.*\.(ts|jpg|html).*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts
private fun absoluteExtensionDetermination(url: String): String? {
@ -115,13 +115,22 @@ object M3u8Helper2 {
private fun selectBest(qualities: List<M3u8Helper.M3u8Stream>): M3u8Helper.M3u8Stream? {
val result = qualities.sortedBy {
if (it.quality != null && it.quality <= 1080) it.quality else 0
}.filter {
it.quality ?: Qualities.Unknown.value //if (it.quality != null && it.quality <= 1080) else 0
}/*.filter {
listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl))
}
}*/
return result.lastOrNull()
}
private fun selectWorst(qualities: List<M3u8Helper.M3u8Stream>): M3u8Helper.M3u8Stream? {
val result = qualities.sortedBy {
it.quality ?: Qualities.Unknown.value //if (it.quality != null && it.quality <= 1080) else 0
}/*.filter {
listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl))
}*/
return result.firstOrNull()
}
private fun getParentLink(uri: String): String {
val split = uri.split("/").toMutableList()
split.removeLast()
@ -173,14 +182,20 @@ object M3u8Helper2 {
return list
}
data class TsLink(
val url : String,
val time : Double?,
)
data class LazyHlsDownloadData(
private val encryptionData: ByteArray,
private val encryptionIv: ByteArray,
private val isEncrypted: Boolean,
private val allTsLinks: List<String>,
private val relativeUrl: String,
private val headers: Map<String, String>,
val isEncrypted: Boolean,
val allTsLinks: List<TsLink>,
val relativeUrl: String,
val headers: Map<String, String>,
) {
val size get() = allTsLinks.size
suspend fun resolveLinkWhileSafe(
@ -228,9 +243,9 @@ object M3u8Helper2 {
@Throws
suspend fun resolveLink(index: Int): ByteArray {
if (index < 0 || index >= size) throw IllegalArgumentException("index must be in the bounds of the ts")
val url = allTsLinks[index]
val ts = allTsLinks[index]
val tsResponse = app.get(url, headers = headers, verify = false)
val tsResponse = app.get(ts.url, headers = headers, verify = false)
val tsData = tsResponse.body.bytes()
if (tsData.isEmpty()) throw ErrorLoadingException("no data")
@ -244,15 +259,16 @@ object M3u8Helper2 {
@Throws
suspend fun hslLazy(
qualities: List<M3u8Helper.M3u8Stream>
qualities: List<M3u8Helper.M3u8Stream>, selectBest : Boolean = true
): LazyHlsDownloadData {
if (qualities.isEmpty()) throw IllegalArgumentException("qualities must be non empty")
val selected = selectBest(qualities) ?: qualities.first()
val selected = if(selectBest) { selectBest(qualities) } else { selectWorst(qualities) } ?: qualities.first()
val headers = selected.headers
val streams = qualities.map { m3u8Generation(it, false) }.flatten()
// this selects the best quality of the qualities offered,
// due to the recursive nature of m3u8, we only go 2 depth
val secondSelection = selectBest(streams.ifEmpty { listOf(selected) })
val innerStreams = streams.ifEmpty { listOf(selected) }
val secondSelection = if(selectBest) { selectBest(innerStreams) } else { selectWorst(innerStreams) }
?: throw IllegalArgumentException("qualities has no streams")
val m3u8Response =
@ -285,12 +301,14 @@ object M3u8Helper2 {
}
val relativeUrl = getParentLink(secondSelection.streamUrl)
val allTsList = TS_EXTENSION_REGEX.findAll(m3u8Response + "\n").map { ts ->
val value = ts.groupValues[1]
if (isNotCompleteUrl(value)) {
val time = ts.groupValues[1]
val value = ts.groupValues[3]
val url = if (isNotCompleteUrl(value)) {
"$relativeUrl/${value}"
} else {
value
}
TsLink(url = url, time = time.toDoubleOrNull())
}.toList()
if (allTsList.isEmpty()) throw IllegalArgumentException("ts must be non empty")

View file

@ -1,12 +1,12 @@
package com.lagradost.cloudstream3.utils
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Build
import android.os.IBinder
import android.util.Log
@ -54,7 +54,11 @@ class PackageInstallerService : Service() {
UPDATE_CHANNEL_NAME,
UPDATE_CHANNEL_DESCRIPTION
)
startForeground(UPDATE_NOTIFICATION_ID, baseNotification.build())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(UPDATE_NOTIFICATION_ID, baseNotification.build(), FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else{
startForeground(UPDATE_NOTIFICATION_ID, baseNotification.build())
}
}
private val updateLock = Mutex()

View file

@ -0,0 +1,94 @@
package com.lagradost.cloudstream3.utils
//Reference: https://stackoverflow.com/a/29055283
import android.content.Context
import android.graphics.Matrix
import android.graphics.drawable.Drawable
import android.util.AttributeSet
class PercentageCropImageView : androidx.appcompat.widget.AppCompatImageView {
private var mCropYCenterOffsetPct: Float? = null
private var mCropXCenterOffsetPct: Float? = null
constructor(context: Context?) : super(context!!)
constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs)
constructor(
context: Context?, attrs: AttributeSet?,
defStyle: Int
) : super(context!!, attrs, defStyle)
var cropYCenterOffsetPct: Float
get() = mCropYCenterOffsetPct!!
set(cropYCenterOffsetPct) {
require(cropYCenterOffsetPct <= 1.0) { "Value too large: Must be <= 1.0" }
mCropYCenterOffsetPct = cropYCenterOffsetPct
}
var cropXCenterOffsetPct: Float
get() = mCropXCenterOffsetPct!!
set(cropXCenterOffsetPct) {
require(cropXCenterOffsetPct <= 1.0) { "Value too large: Must be <= 1.0" }
mCropXCenterOffsetPct = cropXCenterOffsetPct
}
private fun myConfigureBounds() {
if (this.scaleType == ScaleType.MATRIX) {
val d = this.drawable
if (d != null) {
val dWidth = d.intrinsicWidth
val dHeight = d.intrinsicHeight
val m = Matrix()
val vWidth = width - this.paddingLeft - this.paddingRight
val vHeight = height - this.paddingTop - this.paddingBottom
val scale: Float
var dx = 0f
var dy = 0f
if (dWidth * vHeight > vWidth * dHeight) {
val cropXCenterOffsetPct =
if (mCropXCenterOffsetPct != null) mCropXCenterOffsetPct!!.toFloat() else 0.5f
scale = vHeight.toFloat() / dHeight.toFloat()
dx = (vWidth - dWidth * scale) * cropXCenterOffsetPct
} else {
val cropYCenterOffsetPct =
if (mCropYCenterOffsetPct != null) mCropYCenterOffsetPct!!.toFloat() else 0f
scale = vWidth.toFloat() / dWidth.toFloat()
dy = (vHeight - dHeight * scale) * cropYCenterOffsetPct
}
m.setScale(scale, scale)
m.postTranslate((dx + 0.5f).toInt().toFloat(), (dy + 0.5f).toInt().toFloat())
this.imageMatrix = m
}
}
}
// These 3 methods call configureBounds in ImageView.java class, which
// adjusts the matrix in a call to center_crop (android's built-in
// scaling and centering crop method). We also want to trigger
// in the same place, but using our own matrix, which is then set
// directly at line 588 of ImageView.java and then copied over
// as the draw matrix at line 942 of ImageView.java
override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean {
val changed = super.setFrame(l, t, r, b)
myConfigureBounds()
return changed
}
override fun setImageDrawable(d: Drawable?) {
super.setImageDrawable(d)
myConfigureBounds()
}
override fun setImageResource(resId: Int) {
super.setImageResource(resId)
myConfigureBounds()
}
// In case you can change the ScaleType in code you have to call redraw()
//fullsizeImageView.setScaleType(ScaleType.FIT_CENTER);
//fullsizeImageView.redraw();
fun redraw() {
val d = this.drawable
if (d != null) {
// Force toggle to recalculate our bounds
setImageDrawable(null)
setImageDrawable(d)
}
}
}

View file

@ -2,14 +2,12 @@ package com.lagradost.cloudstream3.utils
import android.app.Activity
import android.app.Dialog
import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.widget.AbsListView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
@ -19,7 +17,10 @@ import androidx.core.view.marginRight
import androidx.core.view.marginTop
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.BottomInputDialogBinding
import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding
import com.lagradost.cloudstream3.databinding.BottomTextDialogBinding
import com.lagradost.cloudstream3.databinding.OptionsPopupTvBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
@ -54,14 +55,14 @@ object SingleSelectionHelper {
if (this == null) return
if (isTvSettings()) {
val builder =
AlertDialog.Builder(this, R.style.AlertDialogCustom)
.setView(R.layout.options_popup_tv)
val binding = OptionsPopupTvBinding.inflate(layoutInflater)
val dialog = AlertDialog.Builder(this, R.style.AlertDialogCustom)
.setView(binding.root)
.create()
val dialog = builder.create()
dialog.show()
dialog.findViewById<ListView>(R.id.listview1)?.let { listView ->
binding.listview1.let { listView ->
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
listView.adapter =
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice_color).apply {
@ -74,7 +75,7 @@ object SingleSelectionHelper {
}
}
dialog.findViewById<ImageView>(R.id.imageView)?.apply {
binding.imageView.apply {
isGone = poster.isNullOrEmpty()
setImage(poster)
}
@ -105,12 +106,12 @@ object SingleSelectionHelper {
if (this == null) return
val realShowApply = showApply || isMultiSelect
val listView = binding.listview1//.findViewById<ListView>(R.id.listview1)!!
val textView = binding.text1//.findViewById<TextView>(R.id.text1)!!
val applyButton = binding.applyBtt//.findViewById<TextView>(R.id.apply_btt)
val cancelButton = binding.cancelBtt//findViewById<TextView>(R.id.cancel_btt)
val listView = binding.listview1
val textView = binding.text1
val applyButton = binding.applyBtt
val cancelButton = binding.cancelBtt
val applyHolder =
binding.applyBttHolder//.findViewById<LinearLayout>(R.id.apply_btt_holder)
binding.applyBttHolder
applyHolder.isVisible = realShowApply
if (!realShowApply) {
@ -173,8 +174,8 @@ object SingleSelectionHelper {
}
}
private fun Activity?.showInputDialog(
binding: BottomInputDialogBinding,
dialog: Dialog,
value: String,
name: String,
@ -184,11 +185,11 @@ object SingleSelectionHelper {
) {
if (this == null) return
val inputView = dialog.findViewById<EditText>(R.id.nginx_text_input)!!
val textView = dialog.findViewById<TextView>(R.id.text1)!!
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!!
val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!!
val inputView = binding.nginxTextInput
val textView = binding.text1
val applyButton = binding.applyBtt
val cancelButton = binding.cancelBtt
val applyHolder = binding.applyBttHolder
applyHolder.isVisible = true
textView.text = name
@ -350,11 +351,17 @@ object SingleSelectionHelper {
dismissCallback: () -> Unit,
callback: (String) -> Unit,
) {
val builder = BottomSheetDialog(this) // probably the stuff at the bottom
builder.setContentView(R.layout.bottom_input_dialog) // input layout
val builder = BottomSheetDialog(this)
val binding: BottomInputDialogBinding = BottomInputDialogBinding.inflate(
LayoutInflater.from(this)
)
builder.setContentView(binding.root)
builder.show()
showInputDialog(
binding,
builder,
value,
name,
@ -363,4 +370,24 @@ object SingleSelectionHelper {
dismissCallback
)
}
fun Activity.showBottomDialogText(
title: String,
text: Spanned,
dismissCallback: () -> Unit
) {
val binding = BottomTextDialogBinding.inflate(layoutInflater)
val dialog = BottomSheetDialog(this)
dialog.setContentView(binding.root)
binding.dialogTitle.text = title
binding.dialogText.text = text
dialog.setOnDismissListener {
dismissCallback.invoke()
}
dialog.show()
}
}

View file

@ -71,7 +71,7 @@ object UIHelper {
val Int.toDp: Int get() = (this / Resources.getSystem().displayMetrics.density).toInt()
val Float.toDp: Float get() = (this / Resources.getSystem().displayMetrics.density)
fun Activity.checkWrite(): Boolean {
fun Context.checkWrite(): Boolean {
return (ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
@ -178,9 +178,10 @@ object UIHelper {
fun Activity?.navigate(@IdRes navigation: Int, arguments: Bundle? = null) {
try {
if (this is FragmentActivity) {
(supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment?)?.navController?.navigate(
navigation, arguments
)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment?
navHostFragment?.navController?.let {
it.navigate(navigation, arguments)
}
}
} catch (t: Throwable) {
logError(t)

View file

@ -2,11 +2,9 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<stroke android:width="2dp"
android:color="?attr/white"/>
<!-- <corners
<corners
android:bottomLeftRadius="@dimen/rounded_image_radius"
android:bottomRightRadius="@dimen/rounded_image_radius"
android:topLeftRadius="@dimen/rounded_image_radius"
android:topRightRadius="@dimen/rounded_image_radius" />
-->
</shape>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!--<item android:state_focused="true"
android:drawable="@drawable/outline"/>--> <!-- focused -->
<item android:state_focused="true"
android:drawable="@drawable/outline"/> <!-- focused -->
</selector>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!--<item android:state_focused="true" android:drawable="@drawable/outline_less" />--> <!-- focused -->
<item android:state_focused="true" android:drawable="@drawable/outline_less" /> <!-- focused -->
</selector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="@dimen/video_frame_width"
android:color="@android:color/white" />
<solid android:color="@android:color/black" />
</shape>

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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/card_view"
android:layout_width="110dp"
android:layout_height="110dp"
android:animateLayoutChanges="true"
android:backgroundTint="?attr/primaryGrayBackground"
android:foreground="?attr/selectableItemBackground"
app:cardCornerRadius="@dimen/rounded_image_radius"
android:layout_margin="10dp"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.4"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/account_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.4"
android:contentDescription="@string/profile_background_des"
android:scaleType="centerCrop" />
<View
android:id="@+id/outline"
tools:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/outline_card"
android:visibility="gone" />
<ImageView
android:id="@+id/lock_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="top|end"
android:layout_margin="4dp"
android:src="@drawable/video_locked"
android:visibility="gone" />
<TextView
android:id="@+id/account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:padding="10dp"
android:textSize="16sp" />
</androidx.cardview.widget.CardView>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:background="?attr/primaryBlackBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="36dp"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/select_an_account"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/account_recycler_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp" />
</LinearLayout>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:background="?attr/primaryBlackBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="36dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textAlignment="center"
android:text="@string/select_an_account"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/account_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp" />
</LinearLayout>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/dialog_title"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:textStyle="bold"
android:textSize="20sp"
android:textColor="?attr/textColor"
android:layout_width="match_parent"
android:layout_rowWeight="1"
tools:text="Test"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/dialog_text"
android:textAppearance="?android:attr/textAppearanceListItem"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingTop="10dp"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_rowWeight="1" />
</LinearLayout>

View file

@ -9,7 +9,7 @@
android:layout_margin="5dp"
android:foreground="@drawable/outline_drawable"
app:cardBackgroundColor="@color/transparent"
android:focusable="true"
app:cardCornerRadius="@dimen/rounded_image_radius"
app:cardElevation="0dp">
@ -17,6 +17,7 @@
android:layout_width="100dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:focusable="true"
android:padding="5dp">
<!--app:cardCornerRadius="@dimen/roundedImageRadius"-->
<FrameLayout
@ -34,7 +35,6 @@
android:id="@+id/actor_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/episode_poster_img_des"
android:scaleType="centerCrop"

View file

@ -137,6 +137,7 @@
android:background="?attr/primaryBlackBackground"
android:descendantFocusability="afterDescendants"
android:nextFocusLeft="@id/nav_rail_view"
android:tag = "@string/tv_no_focus_tag"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/download_header_episode" />

View file

@ -92,15 +92,7 @@
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">
android:orientation="horizontal">
<TextView
android:id="@+id/home_preview_bookmark"
@ -139,7 +131,7 @@
app:drawableTint="?attr/white"
app:drawableTopCompat="@drawable/ic_outline_info_24"
app:tint="?attr/white" />
</LinearLayout>
</LinearLayout>
</FrameLayout>

View file

@ -55,6 +55,7 @@
android:layout_gravity="end"
android:background="@drawable/player_button_tv_attr_no_bg"
android:contentDescription="@string/search"
android:focusable="true"
android:nextFocusLeft="@id/home_preview_change_api"
android:nextFocusRight="@id/home_preview_switch_account"
android:nextFocusDown="@id/home_preview_info_btt"
@ -70,6 +71,7 @@
android:layout_gravity="end"
android:background="@drawable/player_button_tv_attr_no_bg"
android:contentDescription="@string/account"
android:focusable="true"
android:nextFocusLeft="@id/home_preview_search_button"
android:nextFocusRight="@id/home_preview_switch_account"
android:nextFocusDown="@id/home_preview_info_btt"
@ -230,7 +232,9 @@
android:layout_marginStart="@dimen/navbar_width"
android:layout_marginEnd="0dp"
android:padding="12dp"
android:text="@string/continue_watching" />
android:text="@string/continue_watching"
android:background="?android:attr/selectableItemBackground"
app:drawableTint="?attr/white" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/home_watch_child_recyclerview"
@ -256,7 +260,15 @@
android:visibility="gone"
tools:visibility="visible">
<FrameLayout
android:id="@+id/home_bookmark_parent_item_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground">
<HorizontalScrollView
android:id="@+id/horizontal_scroll_chips"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fadingEdge="horizontal"
@ -342,6 +354,18 @@
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<ImageView
android:id="@+id/home_bookmark_parent_item_more_info"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:layout_marginEnd="12dp"
android:contentDescription="@string/home_more_info"
android:src="@drawable/ic_baseline_arrow_forward_24"
android:visibility="gone"
app:drawableTint="?attr/white" />
</FrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/home_bookmarked_child_recyclerview"
android:layout_width="wrap_content"

View file

@ -140,6 +140,7 @@
android:layout_gravity="end"
android:background="@drawable/player_button_tv_attr_no_bg"
android:contentDescription="@string/account"
android:focusable="true"
android:nextFocusLeft="@id/home_preview_search_button"
android:nextFocusRight="@id/home_switch_account"
android:nextFocusDown="@id/home_change_api"

View file

@ -41,6 +41,20 @@
android:src="@drawable/ic_baseline_extension_24"
app:tint="?attr/textColor" />
<ImageView
android:id="@+id/library_sort"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_gravity="end|center_vertical"
android:layout_marginStart="10dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/change_providers_img_des"
android:padding="5dp"
android:visibility="gone"
tools:visibility="visible"
android:src="@drawable/ic_baseline_sort_24"
app:tint="?attr/textColor" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="40dp"
@ -54,6 +68,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginEnd="25dp"
android:iconifiedByDefault="false"
android:imeOptions="actionSearch"
@ -67,6 +82,7 @@
app:queryBackground="@color/transparent"
app:queryHint="@string/search_hint"
app:searchIcon="@drawable/search_icon"
app:closeIcon="@drawable/ic_baseline_close_24"
tools:ignore="RtlSymmetry">
</androidx.appcompat.widget.SearchView>
@ -160,8 +176,7 @@
android:layout_height="40dp"
android:layout_gravity="bottom"
android:background="?attr/primaryGrayBackground"
android:descendantFocusability="blocksDescendants"
android:focusable="false"
android:focusable="true"
android:paddingHorizontal="5dp"
app:layout_scrollFlags="noScroll"
app:tabGravity="center"

View file

@ -0,0 +1,204 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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/library_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/empty_list_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="30dp"
android:gravity="center"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/primaryGrayBackground">
<LinearLayout
android:id="@+id/search_status_bar_padding"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/provider_selector"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_gravity="end|center_vertical"
android:layout_marginStart="10dp"
android:padding="2dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/change_providers_img_des"
android:focusable="true"
android:nextFocusLeft="@id/nav_rail_view"
android:nextFocusRight="@id/library_sort"
android:tag="@string/tv_no_focus_tag"
android:src="@drawable/ic_baseline_extension_24"
app:tint="?attr/textColor" />
<ImageView
android:id="@+id/library_sort"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_gravity="end|center_vertical"
android:layout_marginStart="10dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/change_providers_img_des"
android:focusable="true"
android:nextFocusLeft="@id/provider_selector"
android:nextFocusRight="@id/list_selector"
android:tag="@string/tv_no_focus_tag"
android:src="@drawable/ic_baseline_sort_24"
app:tint="?attr/textColor" />
<ImageView
android:id="@+id/list_selector"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_gravity="end|center_vertical"
android:padding="1dp"
android:layout_marginStart="10dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/change_providers_img_des"
android:focusable="true"
android:nextFocusLeft="@id/library_sort"
android:nextFocusRight="@id/main_search"
android:tag="@string/tv_no_focus_tag"
android:src="@drawable/ic_baseline_filter_list_24"
app:tint="?attr/textColor" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_margin="10dp"
android:background="@drawable/search_background"
android:visibility="visible"
app:layout_scrollFlags="scroll|enterAlways">
<androidx.appcompat.widget.SearchView
android:id="@+id/main_search"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="end"
android:iconifiedByDefault="false"
android:imeOptions="actionSearch"
android:inputType="text"
android:focusable="true"
android:tag="tv_no_focus_tag"
android:nextFocusLeft="@id/list_selector"
android:nextFocusDown="@id/search_result_root"
android:paddingStart="-10dp"
app:iconifiedByDefault="false"
app:queryBackground="@color/transparent"
app:queryHint="@string/search_hint"
app:searchIcon="@drawable/search_icon"
app:closeIcon="@drawable/ic_baseline_close_24"
tools:ignore="RtlSymmetry">
</androidx.appcompat.widget.SearchView>
</FrameLayout>
</LinearLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/library_tab_layout"
style="@style/Theme.Widget.Tabs"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_gravity="top"
android:nextFocusDown="@id/search_result_root"
android:background="?attr/primaryGrayBackground"
android:paddingHorizontal="5dp"
app:layout_scrollFlags="noScroll"
app:tabGravity="center"
app:tabIndicator="@drawable/indicator_background"
app:tabIndicatorColor="?attr/white"
app:tabIndicatorGravity="center"
app:tabIndicatorHeight="30dp"
app:tabMode="scrollable"
app:tabSelectedTextColor="?attr/primaryBlackBackground"
app:tabTextAppearance="@style/TabNoCaps"
app:tabTextColor="?attr/textColor" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="40dp"
android:focusable="true"
android:tag="@string/tv_no_focus_tag"
tools:listitem="@layout/library_viewpager_page" />
<LinearLayout
android:id="@+id/library_loading_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="false"
android:tag="@string/tv_no_focus_tag"
android:background="?attr/primaryBlackBackground"
android:visibility="gone"
tools:visibility="visible">
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/library_loading_shimmer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_margin="2dp"
app:shimmer_auto_start="true"
app:shimmer_base_alpha="0.2"
android:focusable="false"
android:tag="@string/tv_no_focus_tag"
app:shimmer_duration="@integer/loading_time"
app:shimmer_highlight_alpha="0.3">
<GridView
android:id="@+id/gridview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:gravity="center"
android:horizontalSpacing="10dp"
android:numColumns="3"
android:paddingBottom="120dp"
android:focusable="false"
android:tag="@string/tv_no_focus_tag"
android:verticalSpacing="10dp"
tools:listitem="@layout/loading_poster_dynamic" />
</com.facebook.shimmer.ShimmerFrameLayout>
</LinearLayout>
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="40dp">
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/sort_fab"
style="@style/ExtendedFloatingActionButton"
android:text="@string/sort"
android:textColor="?attr/textColor"
android:visibility="gone"
tools:visibility="visible"
app:icon="@drawable/ic_baseline_sort_24"
tools:ignore="ContentDescription" />
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -875,7 +875,7 @@
android:descendantFocusability="afterDescendants"
android:paddingBottom="100dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/result_episode_both_tv" />
tools:listitem="@layout/result_episode" />
</LinearLayout>
</LinearLayout>

View file

@ -74,7 +74,7 @@
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_description"
android:nextFocusLeft="@id/result_add_sync"
android:nextFocusRight="@id/result_share"
android:nextFocusRight="@id/result_favorite"
tools:visibility="visible"
@ -89,10 +89,27 @@
android:layout_gravity="end|center_vertical"
app:tint="?attr/textColor" />
<ImageView
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_description"
android:nextFocusLeft="@id/result_subscribe"
android:nextFocusRight="@id/result_share"
android:id="@+id/result_favorite"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_margin="5dp"
android:elevation="10dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_favorite_border_24"
android:layout_gravity="end|center_vertical"
app:tint="?attr/textColor" />
<ImageView
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_description"
android:nextFocusLeft="@id/result_subscribe"
android:nextFocusLeft="@id/result_favorite"
android:nextFocusRight="@id/result_open_in_browser"
android:id="@+id/result_share"

View file

@ -124,28 +124,27 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:textColor="?attr/textColor" />
</LinearLayout>
<FrameLayout
android:id="@+id/background_poster_holder"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_height="250dp"
android:visibility="visible">
<ImageView
<com.lagradost.cloudstream3.utils.PercentageCropImageView
android:id="@+id/background_poster"
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_height="275dp"
android:layout_gravity="center"
android:alpha="0.8"
android:scaleType="centerCrop"
tools:src="@drawable/profile_bg_dark_blue" />
android:scaleType="matrix"
tools:src="@drawable/profile_bg_dark_blue" >
</com.lagradost.cloudstream3.utils.PercentageCropImageView>
<ImageView
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_height="120dp"
android:layout_gravity="bottom"
android:src="@drawable/background_shadow">
</ImageView>
</FrameLayout>
@ -155,7 +154,6 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -166,8 +164,70 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:orientation="horizontal">
android:orientation="vertical"
android:layout_marginTop="175dp">
<TextView
android:id="@+id/result_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold"
tools:text="The Perfect Run The Perfect Run" />
<LinearLayout
android:id="@+id/result_next_airing_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:orientation="vertical">
<TextView
android:id="@+id/result_episodes_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="20dp"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
android:visibility="gone"
tools:text="8 Episodes" />
<TextView
android:id="@+id/result_next_airing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center"
android:textColor="?attr/grayTextColor"
android:textSize="17sp"
android:textStyle="normal"
tools:text="Episode 1022 will be released in" />
<TextView
android:id="@+id/result_next_airing_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="start"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
tools:text="5d 3h 30m" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="10dp">
<LinearLayout
android:layout_width="250dp"
@ -176,67 +236,11 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:layout_weight="0"
android:orientation="vertical">
<TextView
android:id="@+id/result_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold"
tools:text="The Perfect Run The Perfect Run" />
<LinearLayout
android:id="@+id/result_next_airing_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:orientation="vertical">
<TextView
android:id="@+id/result_episodes_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="20dp"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
android:visibility="gone"
tools:text="8 Episodes" />
<TextView
android:id="@+id/result_next_airing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center"
android:textColor="?attr/grayTextColor"
android:textSize="17sp"
android:textStyle="normal"
tools:text="Episode 1022 will be released in" />
<TextView
android:id="@+id/result_next_airing_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="start"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
tools:text="5d 3h 30m" />
</LinearLayout>
<LinearLayout
android:id="@+id/result_movie_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="10dp"
android:animateLayoutChanges="true"
android:orientation="vertical"
tools:visibility="visible">
@ -310,19 +314,41 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
style="@style/ResultButtonTV"
android:nextFocusRight="@id/result_description"
android:nextFocusUp="@id/result_play_trailer"
android:nextFocusDown="@id/result_episodes_show"
android:nextFocusDown="@id/result_favorite_button"
android:text="@string/type_none"
android:visibility="visible"
app:icon="@drawable/ic_baseline_bookmark_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_favorite_button"
style="@style/ResultButtonTV"
android:nextFocusRight="@id/result_description"
android:nextFocusUp="@id/result_bookmark_button"
android:nextFocusDown="@id/result_subscribe_button"
android:text="@string/action_add_to_favorites"
android:visibility="visible"
app:icon="@drawable/ic_baseline_favorite_border_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_subscribe_button"
style="@style/ResultButtonTV"
android:nextFocusRight="@id/result_description"
android:nextFocusUp="@id/result_favorite_button"
android:nextFocusDown="@id/result_episodes_show"
android:text="@string/action_subscribe"
android:visibility="visible"
app:icon="@drawable/ic_baseline_favorite_border_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_episodes_show"
style="@style/ResultButtonTV"
android:nextFocusRight="@id/redirect_to_episodes"
android:nextFocusUp="@id/result_bookmark_button"
android:nextFocusUp="@id/result_subscribe_button"
android:nextFocusDown="@id/result_cast_items"
android:text="@string/episodes"
@ -404,6 +430,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:fadingEdgeLength="30dp"
android:foreground="@drawable/outline_drawable"
android:maxLines="7"
android:focusable="true"
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_bookmark_button"
android:padding="5dp"
@ -572,6 +599,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
app:layout_constraintTop_toTopOf="@+id/shadow_space_1" />
<ImageView
android:id="@+id/episodes_shadow_background"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="end"

View file

@ -49,6 +49,7 @@
app:queryBackground="@color/transparent"
app:queryHint="@string/search_hint"
app:searchIcon="@drawable/search_icon"
app:closeIcon="@drawable/ic_baseline_close_24"
tools:ignore="RtlSymmetry">
<requestFocus />

View file

@ -50,6 +50,7 @@
app:queryBackground="@color/transparent"
app:queryHint="@string/search_hint"
app:searchIcon="@drawable/search_icon"
app:closeIcon="@drawable/ic_baseline_close_24"
tools:ignore="RtlSymmetry">
<requestFocus />
@ -84,7 +85,8 @@
android:nextFocusLeft="@id/main_search"
android:nextFocusRight="@id/main_search"
android:nextFocusUp="@id/nav_rail_view"
android:nextFocusDown="@id/search_autofit_results"
android:nextFocusDown="@id/tvtypes_chips_scroll"
android:tag = "@string/tv_no_focus_tag"
android:src="@drawable/ic_baseline_tune_24"
app:tint="?attr/textColor" />
</FrameLayout>
@ -141,6 +143,7 @@
android:nextFocusLeft="@id/nav_rail_view"
android:nextFocusUp="@id/tvtypes_chips"
android:nextFocusDown="@id/search_clear_call_history"
android:tag = "@string/tv_no_focus_tag"
android:paddingBottom="50dp"
android:visibility="visible"
tools:listitem="@layout/search_history_item" />

View file

@ -0,0 +1,35 @@
<?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:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/home_child_more_info"
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"
tools: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_child_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:descendantFocusability="afterDescendants"
android:nextFocusUp="@id/home_child_more_info"
android:orientation="horizontal"
android:paddingStart="@dimen/navbar_width"
android:paddingEnd="5dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/home_result_grid" />
</LinearLayout>

View file

@ -5,5 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:focusable="false"
android:tag="tv_no_focus_tag"
tools:listitem="@layout/home_result_grid_expanded" />

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/pinEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/pin"
android:inputType="numberPassword"
android:maxLength="4" />
<TextView
android:id="@+id/pinEditTextError"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:visibility="gone" />
</LinearLayout>

View file

@ -86,7 +86,6 @@
</FrameLayout>
<!-- atm this is useless, however it might be used for PIP one day? -->
<ImageView
android:visibility="gone"
android:id="@+id/player_fullscreen"
android:layout_width="30dp"
android:layout_height="30dp"
@ -94,7 +93,9 @@
android:layout_marginEnd="20dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/baseline_fullscreen_24"
android:visibility="gone"
app:tint="@color/white" />
<FrameLayout
android:id="@+id/player_intro_play"
android:layout_width="0dp"
@ -108,8 +109,8 @@
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:visibility="gone"
android:importantForAccessibility="no" />
android:importantForAccessibility="no"
android:visibility="gone" />
<androidx.constraintlayout.widget.ConstraintLayout
@ -117,6 +118,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/player_top_holder"
android:layout_width="match_parent"
@ -272,6 +274,7 @@
app:tint="@color/white"
tools:ignore="ContentDescription" />
</FrameLayout>
<FrameLayout
android:id="@+id/player_pause_play_holder_holder"
android:layout_width="100dp"
@ -298,6 +301,7 @@
app:tint="@color/white"
tools:ignore="ContentDescription" />
</FrameLayout>
<FrameLayout
android:id="@+id/player_ffwd_holder"
android:layout_width="0dp"
@ -345,14 +349,15 @@
android:layout_width="150dp"
android:layout_height="40dp"
android:layout_marginEnd="100dp"
android:layout_marginTop="60dp"
android:backgroundTint="@color/skipOpTransparent"
android:maxLines="1"
android:padding="10dp"
android:textColor="@color/white"
android:visibility="gone"
app:cornerRadius="@dimen/rounded_button_radius"
app:layout_constraintBottom_toTopOf="@+id/bottom_player_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/player_top_holder"
app:strokeColor="@color/white"
app:strokeWidth="1dp"
tools:text="Skip Opening"
@ -427,20 +432,21 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/player_video_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:visibility="visible"
android:layoutDirection="ltr"
android:orientation="horizontal">
<TextView
android:id="@id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_marginStart="20dp"
android:gravity="end"
android:gravity="end|center_vertical"
android:includeFontPadding="false"
android:minWidth="50dp"
android:paddingLeft="4dp"
@ -448,14 +454,46 @@
android:textColor="@android:color/white"
android:textSize="14sp"
android:textStyle="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="15:30" />
<!--app:buffered_color="@color/videoCache"-->
<androidx.media3.ui.DefaultTimeBar
<FrameLayout
android:id="@+id/previewFrameLayout"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:background="@drawable/video_frame"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@+id/exo_progress"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_percent="0.25"
tools:visibility="visible">
<ImageView
android:id="@+id/previewImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/video_frame_width"
android:importantForAccessibility="no"
android:scaleType="centerCrop" />
</FrameLayout>
<com.github.rubensousa.previewseekbar.media3.PreviewTimeBar
android:id="@id/exo_progress"
android:layout_width="0dp"
android:layout_height="30dp"
android:layout_weight="1"
app:bar_height="2dp"
app:layout_constraintBottom_toBottomOf="@id/exo_position"
app:layout_constraintEnd_toStartOf="@id/exo_duration"
app:layout_constraintStart_toEndOf="@+id/exo_position"
app:played_color="?attr/colorPrimary"
app:scrubber_color="?attr/colorPrimary"
@ -463,12 +501,12 @@
app:scrubber_enabled_size="24dp"
app:unplayed_color="@color/videoProgress" />
<!-- exo_duration-->
<TextView
android:id="@id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_height="30dp"
android:layout_gravity="center|center_vertical"
android:layout_marginEnd="20dp"
android:includeFontPadding="false"
android:minWidth="50dp"
@ -477,9 +515,10 @@
android:textColor="@android:color/white"
android:textSize="14sp"
android:textStyle="normal"
app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
app:layout_constraintEnd_toEndOf="parent"
tools:text="23:20" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<HorizontalScrollView
android:layout_width="wrap_content"

View file

@ -503,13 +503,12 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/player_video_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layoutDirection="ltr"
android:orientation="horizontal">
<ImageView
android:id="@+id/player_pause_play"
@ -526,14 +525,17 @@
android:tag="@string/tv_no_focus_tag"
app:tint="@color/player_button_tv"
tools:ignore="ContentDescription" />
tools:ignore="ContentDescription"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="30dp"
android:layout_gravity="center"
android:gravity="end"
android:layout_marginStart="20dp"
android:gravity="end|center_vertical"
android:includeFontPadding="false"
android:minWidth="50dp"
android:paddingLeft="4dp"
@ -541,29 +543,59 @@
android:textColor="@android:color/white"
android:textSize="14sp"
android:textStyle="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/player_pause_play"
tools:text="15:30" />
<!--app:buffered_color="@color/videoCache"-->
<androidx.media3.ui.DefaultTimeBar
<FrameLayout
android:id="@+id/previewFrameLayout"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:background="@drawable/video_frame"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@+id/exo_progress"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_percent="0.25"
tools:visibility="visible">
<ImageView
android:id="@+id/previewImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/video_frame_width"
android:importantForAccessibility="no"
android:scaleType="centerCrop" />
</FrameLayout>
<com.github.rubensousa.previewseekbar.media3.PreviewTimeBar
android:id="@id/exo_progress"
android:layout_width="0dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_weight="1"
android:focusable="false"
android:focusableInTouchMode="false"
app:bar_height="2dp"
app:layout_constraintBottom_toBottomOf="@id/exo_position"
app:layout_constraintEnd_toStartOf="@id/exo_duration"
app:layout_constraintStart_toEndOf="@+id/exo_position"
app:played_color="?attr/colorPrimary"
app:scrubber_color="?attr/colorPrimary"
app:scrubber_dragged_size="26dp"
app:scrubber_enabled_size="24dp"
app:unplayed_color="@color/videoProgress" />
<!-- exo_duration-->
<TextView
android:id="@id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_height="30dp"
android:layout_gravity="center|center_vertical"
android:layout_marginEnd="20dp"
android:includeFontPadding="false"
android:minWidth="50dp"
@ -572,9 +604,11 @@
android:textColor="@android:color/white"
android:textSize="14sp"
android:textStyle="normal"
app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
app:layout_constraintEnd_toEndOf="parent"
tools:text="23:20" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<HorizontalScrollView
android:layout_width="wrap_content"

View file

@ -22,6 +22,7 @@
android:layout_toStartOf="@id/priority_number"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="10dp"
android:focusable="true"
android:src="@drawable/baseline_remove_24" />
<TextView
@ -43,6 +44,7 @@
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="10dp"
android:focusable="true"
android:src="@drawable/ic_baseline_add_24" />
</RelativeLayout>

View file

@ -10,6 +10,7 @@
android:id="@+id/card_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:focusable="true"
app:layout_constraintDimensionRatio="1"
android:layout_marginStart="10dp"
android:animateLayoutChanges="true"

View file

@ -27,6 +27,7 @@
android:layout_height="wrap_content"
android:layout_rowWeight="1"
android:layout_marginTop="10dp"
android:focusable="true"
android:foreground="@drawable/outline_drawable_forced"
android:gravity="center_vertical"
android:orientation="horizontal">
@ -66,8 +67,9 @@
android:layout_rowWeight="1"
android:background="?attr/primaryBlackBackground"
android:listSelector="@drawable/outline_drawable_forced"
android:nextFocusLeft="@id/sort_subtitles"
android:nextFocusRight="@id/apply_btt"
android:nextFocusUp="@id/profiles_click_settings"
android:nextFocusRight="@id/sort_subtitles"
android:nextFocusDown="@id/apply_btt"
android:requiresFadingEdge="vertical"
tools:layout_height="100dp"
tools:listitem="@layout/sort_bottom_single_choice" />
@ -94,6 +96,7 @@
android:layout_height="wrap_content"
android:layout_rowWeight="1"
android:layout_marginTop="10dp"
android:focusable="true"
android:foreground="@drawable/outline_drawable_forced"
android:orientation="horizontal"
android:paddingTop="10dp"
@ -139,8 +142,10 @@
android:layout_rowWeight="1"
android:background="?attr/primaryBlackBackground"
android:listSelector="@drawable/outline_drawable_forced"
android:nextFocusUp="@id/subtitles_click_settings"
android:nextFocusLeft="@id/sort_providers"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusRight="@id/apply_btt"
android:nextFocusDown="@id/apply_btt"
android:requiresFadingEdge="vertical"
tools:layout_height="200dp"
tools:listfooter="@layout/sort_bottom_footer_add_choice"

View file

@ -42,8 +42,8 @@
android:layout_rowWeight="1"
android:background="?attr/primaryBlackBackground"
android:listSelector="@drawable/outline_drawable_less"
android:nextFocusLeft="@id/sort_subtitles"
android:nextFocusRight="@id/apply_btt"
android:nextFocusRight="@id/sort_subtitles"
android:nextFocusDown="@id/profile_text_editable"
android:requiresFadingEdge="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:layout_height="100dp"
@ -92,6 +92,8 @@
android:layout_height="50dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="12dp"
android:focusable="true"
android:nextFocusLeft="@id/sort_sources"
android:src="@drawable/baseline_help_outline_24"
android:contentDescription="@string/help" />
@ -115,8 +117,10 @@
android:layout_rowWeight="1"
android:background="?attr/primaryBlackBackground"
android:listSelector="@drawable/outline_drawable_less"
android:nextFocusLeft="@id/sort_providers"
android:nextFocusRight="@id/cancel_btt"
android:nextFocusLeft="@id/sort_sources"
android:nextFocusRight="@id/apply_btt"
android:nextFocusUp="@id/help_btt"
android:nextFocusDown="@id/apply_btt"
android:requiresFadingEdge="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:layout_height="200dp"

View file

@ -17,14 +17,15 @@
android:layout_height="wrap_content">
<ImageView
android:id="@+id/quick_search_back"
android:layout_gravity="center"
android:foregroundGravity="center"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_arrow_back_24"
app:tint="@android:color/white"
android:layout_width="25dp"
android:layout_height="wrap_content">
android:id="@+id/quick_search_back"
android:layout_gravity="center"
android:foregroundGravity="center"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_arrow_back_24"
app:tint="@android:color/white"
android:focusable="true"
android:layout_width="25dp"
android:layout_height="wrap_content">
<requestFocus />
</ImageView>
@ -48,6 +49,8 @@
app:queryBackground="@color/transparent"
app:searchIcon="@drawable/search_icon"
app:closeIcon="@drawable/ic_baseline_close_24"
android:paddingStart="-10dp"
android:iconifiedByDefault="false"
app:queryHint="@string/search_hint"

View file

@ -11,7 +11,9 @@
android:nextFocusRight="@id/download_button"
app:cardBackgroundColor="@color/transparent"
app:cardCornerRadius="@dimen/rounded_image_radius"
app:cardElevation="0dp">
app:cardElevation="0dp"
android:foreground="@drawable/outline_drawable"
>
<!--
android:nextFocusLeft="@id/result_episode_download"
-->

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:foreground="@drawable/outline_drawable"
android:layout_height="wrap_content">
<include android:visibility="gone" layout="@layout/result_episode" />
<include android:visibility="visible" layout="@layout/result_episode_large" />
</FrameLayout>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="@drawable/outline_drawable">
<include
layout="@layout/result_episode"
android:visibility="gone" />
<include
layout="@layout/result_episode_large"
android:visibility="visible" />
</FrameLayout>

View file

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:foreground="@drawable/outline_drawable"
android:layout_height="wrap_content">
<include
tools:visibility="visible"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="50dp"
layout="@layout/result_episode_tv" />
<include
tools:visibility="gone"
android:visibility="gone"
android:layout_width="450dp"
android:layout_height="wrap_content"
layout="@layout/result_episode_large_tv" />
</FrameLayout>

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:foreground="@drawable/outline_drawable">
<include
layout="@layout/result_episode_tv_old"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:visibility="gone"
tools:visibility="visible" />
<include
layout="@layout/result_episode_large_tv_old"
android:layout_width="450dp"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="gone" />
</FrameLayout>

View file

@ -8,6 +8,7 @@
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:foreground="@drawable/outline_drawable"
android:nextFocusRight="@id/download_button"
app:cardBackgroundColor="?attr/boxItemBackground"

View file

@ -1,111 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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/episode_holder_large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="@dimen/rounded_image_radius"
app:cardBackgroundColor="?attr/boxItemBackground"
android:layout_marginBottom="10dp">
<LinearLayout
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:padding="10dp"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="wrap_content">
<!--app:cardCornerRadius="@dimen/roundedImageRadius"-->
<androidx.cardview.widget.CardView
android:layout_width="126dp"
android:layout_height="72dp"
android:foreground="@drawable/outline_drawable">
<ImageView
android:id="@+id/episode_poster"
tools:src="@drawable/example_poster"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/episode_poster_img_des" />
<ImageView
android:src="@drawable/play_button"
android:layout_gravity="center"
android:layout_width="36dp"
android:layout_height="36dp"
android:contentDescription="@string/play_episode" />
<androidx.core.widget.ContentLoadingProgressBar
android:layout_marginBottom="-1.5dp"
android:id="@+id/episode_progress"
android:progressTint="?attr/colorPrimary"
android:progressBackgroundTint="?attr/colorPrimary"
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="match_parent"
tools:progress="50"
android:layout_gravity="bottom"
android:layout_height="5dp" />
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_marginStart="15dp"
android:orientation="vertical"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_marginEnd="50dp"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:layout_gravity="start"
style="@style/SmallWhiteButton"
android:layout_marginEnd="10dp"
android:text="@string/filler"
android:id="@+id/episode_filler" />
<TextView
android:layout_gravity="center_vertical"
android:id="@+id/episode_text"
tools:text="1. Jobless"
android:textStyle="bold"
android:textColor="?attr/textColor"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<TextView
android:id="@+id/episode_rating"
tools:text="Rated: 8.8"
android:textColor="?attr/grayTextColor"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
<TextView
android:maxLines="4"
android:ellipsize="end"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:id="@+id/episode_descript"
android:textColor="?attr/grayTextColor"
tools:text="Jon and Daenerys arrive in Winterfell and are met with skepticism. Sam learns about the fate of his family. Cersei gives Euron the reward he aims for. Theon follows his heart. Jon and Daenerys arrive in Winterfell and are met with skepticism. Sam learns about the fate of his family. Cersei gives Euron the reward he aims for. Theon follows his heart."
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View file

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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/episode_holder_large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:background="@drawable/outline_drawable"
app:cardBackgroundColor="?attr/boxItemBackground"
app:cardCornerRadius="@dimen/rounded_image_radius">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/outline_drawable"
android:orientation="vertical"
android:padding="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!--app:cardCornerRadius="@dimen/roundedImageRadius"-->
<androidx.cardview.widget.CardView
android:layout_width="126dp"
android:layout_height="72dp"
android:foreground="@drawable/outline_drawable">
<ImageView
android:id="@+id/episode_poster"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/episode_poster_img_des"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:scaleType="centerCrop"
tools:src="@drawable/example_poster" />
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center"
android:contentDescription="@string/play_episode"
android:src="@drawable/play_button" />
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/episode_progress"
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="5dp"
android:layout_gravity="bottom"
android:layout_marginBottom="-1.5dp"
android:progressBackgroundTint="?attr/colorPrimary"
android:progressTint="?attr/colorPrimary"
tools:progress="50" />
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="15dp"
android:layout_marginEnd="50dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/episode_filler"
style="@style/SmallWhiteButton"
android:layout_gravity="start"
android:layout_marginEnd="10dp"
android:text="@string/filler" />
<TextView
android:id="@+id/episode_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textColor="?attr/textColor"
android:textStyle="bold"
tools:text="1. Jobless" />
</LinearLayout>
<TextView
android:id="@+id/episode_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?attr/grayTextColor"
tools:text="Rated: 8.8" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/episode_descript"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="4"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:textColor="?attr/grayTextColor"
tools:text="Jon and Daenerys arrive in Winterfell and are met with skepticism. Sam learns about the fate of his family. Cersei gives Euron the reward he aims for. Theon follows his heart. Jon and Daenerys arrive in Winterfell and are met with skepticism. Sam learns about the fate of his family. Cersei gives Euron the reward he aims for. Theon follows his heart." />
</LinearLayout>
</androidx.cardview.widget.CardView>

View file

@ -1,60 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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/episode_holder"
android:layout_width="wrap_content"
android:layout_height="50dp"
app:cardCornerRadius="@dimen/rounded_image_radius"
app:cardBackgroundColor="@color/transparent"
app:cardElevation="0dp"
android:layout_marginBottom="5dp">
<androidx.core.widget.ContentLoadingProgressBar
android:layout_marginBottom="-1.5dp"
android:id="@+id/episode_progress"
android:progressTint="?attr/colorPrimary"
android:progressBackgroundTint="?attr/colorPrimary"
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="match_parent"
tools:progress="50"
android:layout_gravity="bottom"
android:layout_height="5dp" />
<LinearLayout
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:gravity="center"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--marquee_forever-->
<com.google.android.material.button.MaterialButton
android:layout_marginEnd="10dp"
tools:visibility="visible"
android:gravity="center"
android:layout_gravity="center"
style="@style/SmallWhiteButton"
android:text="@string/filler"
android:id="@+id/episode_filler" />
<TextView
android:id="@+id/episode_text"
android:layout_gravity="center_vertical"
android:gravity="center"
tools:text="Episode 1"
android:textColor="?attr/textColor"
android:scrollHorizontally="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="0"
android:singleLine="true"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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/episode_holder"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_marginBottom="5dp"
app:cardBackgroundColor="@color/transparent"
app:cardCornerRadius="@dimen/rounded_image_radius"
app:cardElevation="0dp">
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/episode_progress"
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="5dp"
android:layout_gravity="bottom"
android:layout_marginBottom="-1.5dp"
android:progressBackgroundTint="?attr/colorPrimary"
android:progressTint="?attr/colorPrimary"
tools:progress="50" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:paddingStart="20dp"
android:paddingEnd="20dp">
<!--marquee_forever-->
<com.google.android.material.button.MaterialButton
android:id="@+id/episode_filler"
style="@style/SmallWhiteButton"
android:layout_gravity="center"
android:layout_marginEnd="10dp"
android:gravity="center"
android:text="@string/filler"
tools:visibility="visible" />
<TextView
android:id="@+id/episode_text"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:ellipsize="marquee"
android:gravity="center"
android:marqueeRepeatLimit="0"
android:scrollHorizontally="true"
android:singleLine="true"
android:textColor="?attr/textColor"
tools:text="Episode 1" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View file

@ -6,7 +6,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/outline_drawable_less"
android:focusable="true"
android:nextFocusRight="@id/home_history_remove"
android:orientation="horizontal">
<!-- android:foreground="?android:attr/selectableItemBackgroundBorderless"
@ -30,7 +30,7 @@
android:layout_gravity="center_vertical|end"
android:background="@drawable/outline_drawable_less"
android:focusable="true"
android:nextFocusLeft="@id/home_history_tab"
android:padding="10dp"
android:src="@drawable/ic_baseline_close_24"

View file

@ -35,6 +35,7 @@
android:layout_gravity="center"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:focusable="true"
android:nextFocusRight="@id/subtitle_offset_subtract"
android:padding="10dp"
android:src="@drawable/ic_baseline_keyboard_arrow_left_24"
@ -48,6 +49,7 @@
android:layout_gravity="center"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:focusable="true"
android:nextFocusLeft="@id/subtitle_offset_subtract_more"
android:padding="10dp"
android:src="@drawable/baseline_remove_24"
@ -70,6 +72,7 @@
android:layout_gravity="center"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:focusable="true"
android:nextFocusRight="@id/subtitle_offset_add_more"
android:padding="10dp"
android:src="@drawable/ic_baseline_add_24"
@ -83,7 +86,9 @@
android:layout_gravity="center"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:focusable="true"
android:nextFocusLeft="@id/subtitle_offset_add"
android:nextFocusDown="@id/apply_btt"
android:padding="10dp"
android:src="@drawable/ic_baseline_keyboard_arrow_right_24"
app:tint="?attr/white"

View file

@ -212,7 +212,6 @@
tools:visibility="visible" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/player_center_menu"
android:layout_width="match_parent"
@ -294,6 +293,7 @@
app:tint="@color/white"
tools:ignore="ContentDescription" />
</FrameLayout>
<FrameLayout
android:id="@+id/player_ffwd_holder"
android:layout_width="0dp"
@ -420,20 +420,20 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/player_video_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layoutDirection="ltr"
android:orientation="horizontal">
<TextView
android:id="@id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_marginStart="20dp"
android:gravity="end"
android:gravity="end|center_vertical"
android:includeFontPadding="false"
android:minWidth="50dp"
android:paddingLeft="4dp"
@ -441,14 +441,46 @@
android:textColor="@android:color/white"
android:textSize="14sp"
android:textStyle="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="15:30" />
<!--app:buffered_color="@color/videoCache"-->
<androidx.media3.ui.DefaultTimeBar
<FrameLayout
android:id="@+id/previewFrameLayout"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:background="@drawable/video_frame"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@+id/exo_progress"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_percent="0.25"
tools:visibility="visible">
<ImageView
android:id="@+id/previewImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/video_frame_width"
android:importantForAccessibility="no"
android:scaleType="centerCrop" />
</FrameLayout>
<com.github.rubensousa.previewseekbar.media3.PreviewTimeBar
android:id="@id/exo_progress"
android:layout_width="0dp"
android:layout_height="30dp"
android:layout_weight="1"
app:bar_height="2dp"
app:layout_constraintBottom_toBottomOf="@id/exo_position"
app:layout_constraintEnd_toStartOf="@id/exo_duration"
app:layout_constraintStart_toEndOf="@+id/exo_position"
app:played_color="?attr/colorPrimary"
app:scrubber_color="?attr/colorPrimary"
@ -456,13 +488,13 @@
app:scrubber_enabled_size="24dp"
app:unplayed_color="@color/videoProgress" />
<!-- exo_duration-->
<TextView
android:id="@id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="20dp"
android:layout_height="30dp"
android:layout_gravity="center|center_vertical"
android:layout_marginEnd="30dp"
android:includeFontPadding="false"
android:minWidth="50dp"
android:paddingLeft="4dp"
@ -470,19 +502,23 @@
android:textColor="@android:color/white"
android:textSize="14sp"
android:textStyle="normal"
app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
app:layout_constraintEnd_toEndOf="@id/player_fullscreen"
tools:text="23:20" />
</LinearLayout>
<ImageView
android:id="@+id/player_fullscreen"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginEnd="20dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/baseline_fullscreen_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/player_fullscreen"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="20dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/baseline_fullscreen_24"
app:tint="@color/white" />
<HorizontalScrollView
android:layout_width="wrap_content"

View file

@ -11,6 +11,7 @@
android:foreground="?attr/selectableItemBackgroundBorderless"
app:cardCornerRadius="@dimen/rounded_image_radius"
android:layout_margin="5dp"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1"
app:layout_constraintEnd_toEndOf="parent"
@ -34,6 +35,15 @@
android:background="@drawable/outline_card"
android:visibility="gone" />
<ImageView
android:id="@+id/lock_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="top|end"
android:layout_margin="4dp"
android:src="@drawable/video_locked"
android:visibility="gone" />
<TextView
android:id="@+id/profile_text"
tools:text="@string/mobile_data"

View file

@ -11,6 +11,7 @@
android:foreground="?attr/selectableItemBackgroundBorderless"
app:cardCornerRadius="@dimen/rounded_image_radius"
android:layout_margin="5dp"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -77,6 +77,12 @@
android:textColorHint="?attr/grayTextColor"
tools:ignore="LabelFor" />
<CheckBox
android:id="@+id/lockProfileCheckbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/lock_profile" />
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -88,6 +94,7 @@
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_gravity="center"
android:focusable="true"
android:contentDescription="@string/preview_background_img_des"
android:scaleType="centerCrop"
android:src="@drawable/profile_bg_blue" />

View file

@ -331,57 +331,56 @@
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim"
tools:layout="@layout/main_settings">
<action
android:id="@+id/action_navigation_settings_to_navigation_settings_ui"
app:destination="@id/navigation_settings_ui"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_settings_to_navigation_settings_providers"
app:destination="@id/navigation_settings_providers"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_settings_to_navigation_settings_player"
app:destination="@id/navigation_settings_player"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_settings_to_navigation_settings_updates"
app:destination="@id/navigation_settings_updates"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_settings_to_navigation_settings_account"
app:destination="@id/navigation_settings_account"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_settings_to_navigation_settings_general"
app:destination="@id/navigation_settings_general"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_settings_to_navigation_settings_extensions"
app:destination="@id/navigation_settings_extensions"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
</fragment>
tools:layout="@layout/main_settings" />
<action
android:id="@+id/action_navigation_global_to_navigation_settings_ui"
app:destination="@id/navigation_settings_ui"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_global_to_navigation_settings_providers"
app:destination="@id/navigation_settings_providers"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_global_to_navigation_settings_player"
app:destination="@id/navigation_settings_player"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_global_to_navigation_settings_updates"
app:destination="@id/navigation_settings_updates"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_global_to_navigation_settings_account"
app:destination="@id/navigation_settings_account"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_global_to_navigation_settings_general"
app:destination="@id/navigation_settings_general"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_global_to_navigation_settings_extensions"
app:destination="@id/navigation_settings_extensions"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<fragment
android:id="@+id/navigation_subtitles"
@ -638,4 +637,4 @@
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim"
tools:layout="@layout/fragment_setup_media" />
</navigation>
</navigation>

View file

@ -1,2 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources/>
<resources>
<string name="player_speed_text_format" formatted="true">السرعة (%.2fx)</string>
<string name="home_change_provider_img_des">غير المصدر</string>
<string name="next_episode_format" formatted="true">حتنزل الحلقة %d ب</string>
<string name="title_downloads">التنزيلات</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%s الحلقة %d</string>
</resources>

View file

@ -2,4 +2,110 @@
<resources>
<string name="app_dub_sub_episode_text_format" formatted="true">%s ክፍል %d</string>
<string name="cast_format" formatted="true">ተዋናዮች: %s</string>
<string name="player_speed_text_format" formatted="true">ፍጥነት(%.2fx)</string>
<string name="home_next_random_img_des">ቀጣይ በዘፈቀደ</string>
<string name="preview_background_img_des">ዳራ ቅድም እይታ</string>
<string name="next_episode_time_hour_format" formatted="true">%dሰዓት %dደቂቃ</string>
<string name="search_poster_img_des">ፖስተር</string>
<string name="title_downloads">የወረዱ</string>
<string name="new_update_format" formatted="true">አዲስ ማሻሻያ ተገኝቷል!
\n%s -&gt; %s</string>
<string name="go_back_img_des">ተመለስ</string>
<string name="episode_more_options_des">ተጨማሪ አማራጮች</string>
<string name="type_watching">በማየት ላይ</string>
<string name="result_tags">ዘውጎች</string>
<string name="episode_poster_img_des">የክፍሉ ፖስተር</string>
<string name="search_hint_site" formatted="true">%sን ፈልግ…</string>
<string name="filler" formatted="true">መሙያ</string>
<string name="home_change_provider_img_des">አቅራቢ ቀይር</string>
<string name="search_hint">ፍለጋ…</string>
<string name="rated_format" formatted="true">ተመዘነ: %.1f</string>
<string name="browser">አሳሽ</string>
<string name="next_episode_time_day_format" formatted="true">%dቀን %dሰዓት %dደቂቃ</string>
<string name="next_episode">ቀጣይ ክፍል</string>
<string name="next_episode_time_min_format" formatted="true">%dደቂቃ</string>
<string name="duration_format" formatted="true">%d ደቂቃ</string>
<string name="result_poster_img_des">ፖስተር</string>
<string name="result_open_in_browser">በአሳሽ ውስጥ ይክፈቱ</string>
<string name="no_data">ውሂብ የለም</string>
<string name="title_home">መነሻ</string>
<string name="skip_loading">መጫንን ዝለል</string>
<string name="next_episode_format" formatted="true">ክፍል %d በ ይለቀቃል</string>
<string name="result_share">ማጋሪያ</string>
<string name="home_main_poster_img_des">ዋና ፖስተር</string>
<string name="title_settings">ቅንብሮች</string>
<string name="title_search">መፈለጊያ</string>
<string name="loading">በመጫን ላይ…</string>
<string name="app_name">CloudStream</string>
<string name="play_with_app_name">በCloudStream አጫውት</string>
<string name="action_remove_from_bookmarks">ያስወግዱ</string>
<string name="error_bookmarks_text">ዕልባቶች</string>
<string name="download_started">ማውረድ ተጀምሯል</string>
<string name="play_movie_button">ሙቪ አጫውት</string>
<string name="sort_clear">አጽዳ</string>
<string name="subs_outline_color">የዳርቻ ቀለም</string>
<string name="filter_bookmarks">ዕልባቶችን ማጣሪያ</string>
<string name="type_plan_to_watch">በእቅድ ላይ</string>
<string name="update_started">ማዘመን ተጀምሯል</string>
<string name="sort_copy">ቅዳ</string>
<string name="home_more_info">ተጨማሪ መረጃ</string>
<string name="error_loading_links_toast">አገናኞችን መጫን ላይ ስህተት</string>
<string name="subs_edge_type">የጠርዝ ዓይነት</string>
<string name="download_done">ማውረድ ተከናውኗል</string>
<string name="downloading">በማውረድ ላይ</string>
<string name="play_episode">ክፍልን አጫውት</string>
<string name="player_speed">የአጫዋች ፍጥነት</string>
<string name="type_dropped">የተተወ</string>
<string name="subs_background_color">የዳራ ቀለም</string>
<string name="popup_pause_download">ማውረድ ለአፍታ አቁም</string>
<string name="app_dubbed_text">ትርጉም ድምጽ</string>
<string name="subtitles_settings">የትርጉም ጽሑፍ ቅንብሮች</string>
<string name="subs_window_color">የዊንዶው ቀለም</string>
<string name="play_torrent_button">Torrent አጫውት</string>
<string name="download_canceled">ማውረድ ተቋርጧል</string>
<string name="home_expanded_hide">ደብቅ</string>
<string name="sort_apply">አጽድቅ</string>
<string name="pick_subtitle">የትርጉም ጽሑፎች</string>
<string name="download_paused">ማውረድ ቆሟል</string>
<string name="download">ማውረጃ</string>
<string name="reload_error">ግንኙነትን እንደገና ይሞሩ…</string>
<string name="popup_delete_file">ፋይል አጥፋ</string>
<string name="downloaded">ወርዷል</string>
<string name="popup_resume_download">ማውረድ ቀጥል</string>
<string name="subs_text_color">የጽሑፍ ቀለም</string>
<string name="type_completed">የተጠናቀቀ</string>
<string name="play_trailer_button">የፊልም ማስታወቂያ አጫውት</string>
<string name="play_livestream_button">የቀጥታ ስርጭት አጫውት</string>
<string name="popup_play_file">ፋይል አጫውት</string>
<string name="type_re_watching">እንደገና በማየት ላይ</string>
<string name="go_back">ወደ ኋላ መመለሻ</string>
<string name="home_info">መረጃ</string>
<string name="sort_save">ያስቀምጡ</string>
<string name="download_failed">ማውረድ አልተሳካም</string>
<string name="pick_source">ምንጮች</string>
<string name="app_subbed_text">ትርጉም ጽሁፍ</string>
<string name="stream">ዥረት</string>
<string name="type_on_hold">በመቆየት ላይ</string>
<string name="home_play">አጫውት</string>
<string name="download_storage_text">ውስጣዊ ማከማቻ</string>
<string name="sort_close">ዝጋ</string>
<string name="action_add_to_bookmarks">የምልከታ ሁኔታን ያቀናብሩ</string>
<string name="subs_hold_to_reset_to_default">ወደ ነባሪ ዳግም ለማስጀመር ጫን አድርገው ይያዙ</string>
<string name="subs_font_size">የቅርጸ-ቁምፊ መጠን</string>
<string name="subs_auto_select_language">ቋንቋን በራስ ይምረጥ</string>
<string name="continue_watching">መመልከትዎን ይቀጥሉ</string>
<string name="subs_download_languages">ቋንቋዎችን ያውርዱ</string>
<string name="search_provider_text_providers">አቅራቢዎችን በመጠቀም ይፈልጉ</string>
<string name="benene_count_text">%d ሙዝ ለዴቭሎፐሮቹ ተሰጥቷል</string>
<string name="vpn_might_be_needed">ይህ አቅራቢ በትክክል እንዲሰራ ቪፒኤን ሊያስፈልግ ይችላል</string>
<string name="benene_count_text_none">ምንም ሙዝ አልተሰጠም</string>
<string name="subs_subtitle_languages">የትርጉም ጽሑፍ ቋንቋ</string>
<string name="provider_info_meta">ሜታዳታ በድረ-ገጹ ላይ አይገኝም፣ ቪድዮ መጫን ሜታዳታ ድረ-ገጹ ላይ ከሌለ አይሳካም።</string>
<string name="vpn_torrent">ይህ አቅራቢ ቶረንት ነው፣ ቪፒኤን ይመከራል</string>
<string name="subs_font">ቅርጸ-ቁምፊ</string>
<string name="torrent_plot">መግለጫ</string>
<string name="action_remove_watching">ያስወግዱ</string>
<string name="action_open_watching">ተጨማሪ መረጃ</string>
<string name="search_provider_text_types">ዓይነቶችን በመጠቀም ይፈልጉ</string>
<string name="subs_import_text" formatted="true">ቅርጸ-ቁምፊዎችን በ%s ውስጥ በማስቀመጥ ያጫኑ</string>
</resources>

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