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" buildToolsVersion = "34.0.0"
defaultConfig { defaultConfig {
applicationId = "com.lagradost.cloudstream3" applicationId = "com.lagradost.cloudstream3"
minSdk = 21 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 versionCode = 62
versionName = "4.2.0" versionName = "4.2.1"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
@ -175,18 +177,19 @@ repositories {
dependencies { dependencies {
implementation("com.google.android.mediahome:video:1.0.0") implementation("com.google.android.mediahome:video:1.0.0")
implementation("androidx.test.ext:junit-ktx:1.1.5") 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.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1") // need target 32 for 1.5.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.10.0")
implementation("com.google.android.material:material:1.5.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4") 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.navigation:navigation-fragment-ktx:2.7.4")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1") implementation("androidx.navigation:navigation-ui-ktx:2.7.4")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
@ -194,9 +197,10 @@ dependencies {
// implementation("io.karn:khttp-android:0.1.2") //okhttp instead // implementation("io.karn:khttp-android:0.1.2") //okhttp instead
// implementation("org.jsoup:jsoup:1.13.1") // implementation("org.jsoup:jsoup:1.13.1")
// DONT UPDATE, WILL CRASH ANDROID TV ????
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") 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") implementation("com.github.bumptech.glide:glide:4.13.1")
kapt("com.github.bumptech.glide:compiler:4.13.1") kapt("com.github.bumptech.glide:compiler:4.13.1")
@ -220,17 +224,15 @@ dependencies {
// Custom ffmpeg extension for audio codecs // Custom ffmpeg extension for audio codecs
implementation("com.github.recloudstream:media-ffmpeg:1.1.0") implementation("com.github.recloudstream:media-ffmpeg:1.1.0")
//implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
// Bug reports // Bug reports
implementation("ch.acra:acra-core:5.11.0") implementation("ch.acra:acra-core:5.11.2")
implementation("ch.acra:acra-toast:5.11.0") 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: //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): //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 // subtitle color picker
implementation("com.jaredrummler:colorpicker:1.1.0") implementation("com.jaredrummler:colorpicker:1.1.0")
@ -250,14 +252,14 @@ dependencies {
// Networking // Networking
// implementation("com.squareup.okhttp3:okhttp:4.9.2") // implementation("com.squareup.okhttp3:okhttp:4.9.2")
// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") // 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 // 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 🙏 // Util to skip the URI file fuckery 🙏
implementation("com.github.LagradOst:SafeFile:0.0.5") implementation("com.github.LagradOst:SafeFile:0.0.5")
// API because cba maintaining it myself // 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") implementation("com.github.discord:OverlappingPanels:0.1.5")
// debugImplementation because LeakCanary should only run in debug builds. // debugImplementation because LeakCanary should only run in debug builds.
@ -273,7 +275,7 @@ dependencies {
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipeExtractor/commits/dev // newpipe yt taken from https://github.com/TeamNewPipe/NewPipeExtractor/commits/dev
// this should be updated frequently to avoid trailer fu*kery // 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") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
// Library/extensions searching with Levenshtein distance // Library/extensions searching with Levenshtein distance
@ -298,6 +300,8 @@ dependencies {
group = "org.apache.httpcomponents", 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 androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding 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.FragmentPlayerBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding
import com.lagradost.cloudstream3.databinding.FragmentResultBinding 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.FragmentSearchTvBinding
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
import com.lagradost.cloudstream3.databinding.HomepageParentBinding import com.lagradost.cloudstream3.databinding.HomepageParentBinding
import com.lagradost.cloudstream3.databinding.HomepageParentEmulatorBinding
import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutTvBinding 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<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<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<HomepageParentTvBinding>(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) 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.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" /> <!-- 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 --> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!-- Required for getting arbitrary Aniyomi packages -->
<!-- Fixes android tv fuckery --> <!-- Fixes android tv fuckery -->
<uses-feature <uses-feature
@ -61,7 +63,9 @@
android:exported="true" android:exported="true"
android:resizeableActivity="true" android:resizeableActivity="true"
android:screenOrientation="userLandscape" android:screenOrientation="userLandscape"
android:supportsPictureInPicture="true"> android:supportsPictureInPicture="true"
android:taskAffinity="com.lagradost.cloudstream3.downloadedplayer"
android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -92,12 +96,6 @@
android:launchMode="singleTask" android:launchMode="singleTask"
android:resizeableActivity="true" android:resizeableActivity="true"
android:supportsPictureInPicture="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 --> <!-- cloudstreamplayer://encodedUrl?name=Dune -->
<intent-filter> <intent-filter>
@ -172,6 +170,22 @@
</intent-filter> </intent-filter>
</activity> </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 <activity
android:name=".ui.EasterEggMonke" android:name=".ui.EasterEggMonke"
android:exported="true" /> android:exported="true" />
@ -186,6 +200,7 @@
</receiver> </receiver>
<service <service
android:foregroundServiceType="dataSync"
android:name=".services.VideoDownloadService" android:name=".services.VideoDownloadService"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
@ -195,6 +210,7 @@
android:exported="false" /> android:exported="false" />
<service <service
android:foregroundServiceType="dataSync"
android:name=".utils.PackageInstallerService" android:name=".utils.PackageInstallerService"
android:exported="false" /> android:exported="false" />

View file

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

View file

@ -1246,6 +1246,18 @@ interface LoadResponse {
return this.syncData[aniListIdPrefix] 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?) { fun LoadResponse.addMalId(id: Int?) {
this.syncData[malIdPrefix] = (id ?: return).toString() this.syncData[malIdPrefix] = (id ?: return).toString()
this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString()) this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString())
@ -1453,6 +1465,15 @@ interface EpisodeResponse {
var nextAiring: NextAiring? var nextAiring: NextAiring?
var seasonNames: List<SeasonData>? var seasonNames: List<SeasonData>?
fun getLatestEpisodes(): Map<DubStatus, Int?> 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") @JvmName("addSeasonNamesString")
@ -1532,6 +1553,12 @@ data class AnimeLoadResponse(
.takeUnless { it == Int.MIN_VALUE } .takeUnless { it == Int.MIN_VALUE }
}.toMap() }.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 } .takeUnless { it == Int.MIN_VALUE }
return mapOf(DubStatus.None to max) 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( 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.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.screenHeight
import com.lagradost.cloudstream3.CommonActivity.setActivityInstance
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale import com.lagradost.cloudstream3.CommonActivity.updateLocale
import com.lagradost.cloudstream3.databinding.ActivityMainBinding import com.lagradost.cloudstream3.databinding.ActivityMainBinding
@ -283,6 +284,7 @@ var app = Requests(responseParser = object : ResponseParser {
class MainActivity : AppCompatActivity(), ColorPickerDialogListener { class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
companion object { companion object {
const val TAG = "MAINACT" const val TAG = "MAINACT"
const val ANIMATED_OUTLINE : Boolean = false
var lastError: String? = null var lastError: String? = null
/** /**
@ -544,13 +546,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
navRailView.isVisible = isNavVisible && landscape navRailView.isVisible = isNavVisible && landscape
// Hide library on TV since it is not supported yet :( // Hide library on TV since it is not supported yet :(
val isTrueTv = isTrueTvSettings() //val isTrueTv = isTrueTvSettings()
navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv //navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv //navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
// Hide downloads on TV // Hide downloads on TV
navView.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 //navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv
} }
} }
@ -592,6 +594,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
afterPluginsLoadedEvent += ::onAllPluginsLoaded afterPluginsLoadedEvent += ::onAllPluginsLoaded
setActivityInstance(this)
try { try {
if (isCastApiAvailable()) { if (isCastApiAvailable()) {
//mCastSession = mSessionManager.currentCastSession //mCastSession = mSessionManager.currentCastSession
@ -661,7 +664,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val isAtHome = val isAtHome =
navController?.currentDestination?.matchDestination(R.id.navigation_home) == true navController?.currentDestination?.matchDestination(R.id.navigation_home) == true
if (isAtHome && isTrueTvSettings()) { if (isAtHome && isTvSettings()) {
showConfirmExitDialog() showConfirmExitDialog()
} else { } else {
super.onBackPressed() 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?) { override fun onCreate(savedInstanceState: Bundle?) {
app.initClient(this) app.initClient(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
@ -1112,7 +1130,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (appVer != lastAppAutoBackup) { if (appVer != lastAppAutoBackup) {
setKey("VERSION_NAME", BuildConfig.VERSION_NAME) setKey("VERSION_NAME", BuildConfig.VERSION_NAME)
normalSafeApiCall { normalSafeApiCall {
backup() backup(this)
} }
normalSafeApiCall { normalSafeApiCall {
// Recompile oat on new version // Recompile oat on new version
@ -1126,37 +1144,26 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (isTvSettings()) { if (isTvSettings()) {
val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false) val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false)
setContentView(newLocalBinding.root) 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(isTrueTvSettings() && ANIMATED_OUTLINE) {
if(focus is ScrollingView && focus.canScrollVertically()) { TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
focus.scrollBy()
}
when(focus.parent) {
is View -> focus = newFocus
else -> break
}
}*/
}
newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
} }
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
TvFocus.updateFocusView(newFocus)
}
} else {
newLocalBinding.focusOutline.isVisible = false
}
if(isTrueTvSettings()) {
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
centerView(newFocus)
}
}
ActivityMainBinding.bind(newLocalBinding.root) // this may crash ActivityMainBinding.bind(newLocalBinding.root) // this may crash
} else { } else {
@ -1303,7 +1310,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
this@MainActivity.getString(R.string.action_add_to_bookmarks), this@MainActivity.getString(R.string.action_add_to_bookmarks),
showApply = false, 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 override val requiresReferer = true
companion object { companion object {
private const val KEY = "m4H6D9%0\$N&F6rQ&" private const val KEY = "eN0^>\$^#M08uFv%c"
} }
override suspend fun getUrl( 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.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper
class SpeedoStream2 : SpeedoStream() { open class Minoplres : ExtractorApi() {
override val mainUrl = "https://speedostream.mom"
}
class SpeedoStream1 : SpeedoStream() { override val name = "Minoplres" // formerly SpeedoStream
override val mainUrl = "https://speedostream.pm"
}
open class SpeedoStream : ExtractorApi() {
override val name = "SpeedoStream"
override val mainUrl = "https://speedostream.bond"
override val requiresReferer = true override val requiresReferer = true
override val mainUrl = "https://minoplres.xyz" // formerly speedostream.bond
// .bond, .pm, .mom redirect to .bond private val hostUrl = "https://minoplres.xyz"
private val hostUrl = "https://speedostream.bond"
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<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") Log.i(TAG, "Loading plugin: $data")
return try { 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) val loader = PathClassLoader(filePath, context.classLoader)
var manifest: Plugin.Manifest var manifest: Plugin.Manifest
loader.getResourceAsStream("manifest.json").use { stream -> 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.WatchType
import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt 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.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
@ -69,29 +71,52 @@ class LocalList : SyncAPI {
}?.distinctBy { it.first } ?: return null }?.distinctBy { it.first } ?: return null
val list = ioWork { val list = ioWork {
watchStatusIds.groupBy { val isTrueTv = isTrueTvSettings()
it.second.stringRes
}.mapValues { group ->
group.value.mapNotNull {
getBookmarkedData(it.first)?.toLibraryItem(it.first.toString())
}
} + mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull {
it.toLibraryItem()
})
}
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate { val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
// None is not something to display // None is not something to display
it.stringRes to emptyList<SyncAPI.LibraryItem>() it.stringRes to emptyList<SyncAPI.LibraryItem>()
} + mapOf(R.string.subscription_list_name to emptyList()) } + 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())
}
}
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
}
return SyncAPI.LibraryMetadata( return SyncAPI.LibraryMetadata(
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) }, list.map { SyncAPI.LibraryList(txt(it.key), it.value) },
setOf( setOf(
ListSorting.AlphabeticalA, ListSorting.AlphabeticalA,
ListSorting.AlphabeticalZ, ListSorting.AlphabeticalZ,
// ListSorting.UpdatedNew, ListSorting.UpdatedNew,
// ListSorting.UpdatedOld, ListSorting.UpdatedOld,
// ListSorting.RatingHigh, // ListSorting.RatingHigh,
// ListSorting.RatingLow, // ListSorting.RatingLow,
) )

View file

@ -203,7 +203,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
} }
/** Read the id string to get all other ids */ /** 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() return tryParseJson(idString) ?: return emptyMap()
} }
@ -376,6 +376,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
private var status: Int? = null, private var status: Int? = null,
private var addEpisodes: Pair<List<MediaObject.Season>?, List<MediaObject.Season.Episode>?>? = 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, 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 interceptor(interceptor: Interceptor) = apply { this.interceptor = interceptor }
fun apiUrl(url: String) = apply { this.url = url } 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 { fun status(newStatus: Int?, oldStatus: Int?) = apply {
onList = oldStatus != null
// Only set status if its new // Only set status if its new
if (newStatus != oldStatus) { if (newStatus != oldStatus) {
this.status = newStatus this.status = newStatus
@ -412,6 +415,11 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
// Do not add episodes if there is no change // Do not add episodes if there is no change
if (newEpisodes > (oldEpisodes ?: 0)) { if (newEpisodes > (oldEpisodes ?: 0)) {
this.addEpisodes = getEpisodes(allEpisodes.take(newEpisodes)) 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) { if ((oldEpisodes ?: 0) > newEpisodes) {
this.removeEpisodes = getEpisodes(allEpisodes.drop(newEpisodes)) this.removeEpisodes = getEpisodes(allEpisodes.drop(newEpisodes))
@ -431,6 +439,28 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
interceptor = interceptor interceptor = interceptor
).isSuccessful ).isSuccessful
} else { } 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) -> val episodeRemovalResponse = removeEpisodes?.let { (seasons, episodes) ->
app.post( app.post(
"${this.url}/sync/history/remove", "${this.url}/sync/history/remove",
@ -472,28 +502,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
true 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 statusResponse && episodeRemovalResponse && historyResponse
} }
} }

View file

@ -55,7 +55,6 @@ class WhoIsWatchingAdapter(
editCallBack = editCallBack, editCallBack = editCallBack,
) )
override fun onBindViewHolder(holder: WhoIsWatchingHolder, position: Int) = override fun onBindViewHolder(holder: WhoIsWatchingHolder, position: Int) =
holder.bind(currentList.getOrNull(position)) holder.bind(currentList.getOrNull(position))
@ -74,6 +73,11 @@ class WhoIsWatchingAdapter(
outline.isVisible = card.keyIndex == DataStoreHelper.selectedKeyIndex outline.isVisible = card.keyIndex == DataStoreHelper.selectedKeyIndex
profileText.text = card.name profileText.text = card.name
profileImageBackground.setImage(card.image) profileImageBackground.setImage(card.image)
// Handle the lock indicator
val isLocked = card.lockPin != null
lockIcon.isVisible = isLocked
root.setOnClickListener { root.setOnClickListener {
selectCallBack(card) 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.apis
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity.Companion.afterBackupRestoreEvent import com.lagradost.cloudstream3.MainActivity.Companion.afterBackupRestoreEvent
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent 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.ownShow
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe 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
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentHomePage import com.lagradost.cloudstream3.utils.DataStoreHelper.currentHomePage
import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.Event
@ -67,9 +64,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import java.util.* import java.util.*
const val HOME_BOOKMARK_VALUE_LIST = "home_bookmarked_last_list"
const val HOME_PREF_HOMEPAGE = "home_pref_homepage"
class HomeFragment : Fragment() { class HomeFragment : Fragment() {
companion object { companion object {
val configEvent = Event<Int>() val configEvent = Event<Int>()
@ -371,10 +365,7 @@ class HomeFragment : Fragment() {
var currentApiName = selectedApiName var currentApiName = selectedApiName
var currentValidApis: MutableList<MainAPI> = mutableListOf() var currentValidApis: MutableList<MainAPI> = mutableListOf()
val preSelectedTypes = this.getKey<List<String>>(HOME_PREF_HOMEPAGE) val preSelectedTypes = DataStoreHelper.homePreference.toMutableList()
?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } }
?.toMutableList()
?: mutableListOf(TvType.Movie, TvType.TvSeries)
binding.cancelBtt.setOnClickListener { binding.cancelBtt.setOnClickListener {
dialog.dismissSafe() dialog.dismissSafe()
@ -402,7 +393,7 @@ class HomeFragment : Fragment() {
} }
fun updateList() { fun updateList() {
this.setKey(HOME_PREF_HOMEPAGE, preSelectedTypes) DataStoreHelper.homePreference = preSelectedTypes
arrayAdapter.clear() arrayAdapter.clear()
currentValidApis = validAPIs.filter { api -> 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.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
class LoadClickCallback( class LoadClickCallback(
@ -34,11 +35,13 @@ open class ParentItemAdapter(
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val root = LayoutInflater.from(parent.context).inflate( val layoutResId = when {
if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent, isTrueTvSettings() -> R.layout.homepage_parent_tv
parent, parent.context.isEmulatorSettings() -> R.layout.homepage_parent_emulator
false else -> R.layout.homepage_parent
) }
val root = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
val binding = HomepageParentBinding.bind(root) val binding = HomepageParentBinding.bind(root)
@ -234,7 +237,7 @@ open class ParentItemAdapter(
}) })
//(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged() //(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
if (!isTvSettings()) { if (!isTrueTvSettings()) {
title.setOnClickListener { title.setOnClickListener {
moreInfoClickCallback.invoke(expand) 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_LOAD
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
import com.lagradost.cloudstream3.ui.search.SearchClickCallback 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.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
@ -81,6 +82,28 @@ class HomeParentItemAdapterPreview(
parent, parent,
false false
) else FragmentHomeHeadBinding.inflate(inflater, 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( HeaderViewHolder(
binding, binding,
viewModel, viewModel,
@ -355,6 +378,14 @@ class HomeParentItemAdapterPreview(
showApply = false, showApply = false,
{}) { {}) {
val newValue = WatchType.values()[it] val newValue = WatchType.values()[it]
ResultViewModel2().updateWatchStatus(
newValue,
fab.context,
item
) { statusChanged: Boolean ->
if (!statusChanged) return@updateWatchStatus
homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds( homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds(
null, null,
ContextCompat.getDrawable( ContextCompat.getDrawable(
@ -365,11 +396,7 @@ class HomeParentItemAdapterPreview(
null null
) )
homePreviewBookmark.setText(newValue.stringRes) homePreviewBookmark.setText(newValue.stringRes)
}
ResultViewModel2.updateWatchStatus(
item,
newValue
)
} }
} }
} }
@ -553,12 +580,19 @@ class HomeParentItemAdapterPreview(
resumeHolder.isVisible = resumeWatching.isNotEmpty() resumeHolder.isVisible = resumeWatching.isNotEmpty()
resumeAdapter.updateList(resumeWatching) resumeAdapter.updateList(resumeWatching)
if (binding is FragmentHomeHeadBinding) { if (
binding.homeWatchParentItemTitle.setOnClickListener { binding is FragmentHomeHeadBinding ||
binding is FragmentHomeHeadTvBinding &&
binding.root.context.isEmulatorSettings()
) {
val title = (binding as? FragmentHomeHeadBinding)?.homeWatchParentItemTitle
?: (binding as? FragmentHomeHeadTvBinding)?.homeWatchParentItemTitle
title?.setOnClickListener {
viewModel.popup( viewModel.popup(
HomeViewModel.ExpandableHomepageList( HomeViewModel.ExpandableHomepageList(
HomePageList( HomePageList(
binding.homeWatchParentItemTitle.text.toString(), title.text.toString(),
resumeWatching, resumeWatching,
false false
), 1, false ), 1, false
@ -576,8 +610,15 @@ class HomeParentItemAdapterPreview(
bookmarkHolder.isVisible = visible bookmarkHolder.isVisible = visible
bookmarkAdapter.updateList(list) bookmarkAdapter.updateList(list)
if (binding is FragmentHomeHeadBinding) { if (
binding.homeBookmarkParentItemTitle.setOnClickListener { 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 } val items = toggleList.map { it.first }.filter { it.isChecked }
if (items.isEmpty()) return@setOnClickListener // we don't want to show an empty dialog if (items.isEmpty()) return@setOnClickListener // we don't want to show an empty dialog
val textSum = items 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.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.LoadResponse 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.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllResumeStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllResumeStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
@ -103,11 +101,6 @@ class HomeViewModel : ViewModel() {
loadStoredData() loadStoredData()
} }
fun deleteBookmarks() {
deleteAllBookmarkedData()
loadStoredData()
}
var repo: APIRepository? = null var repo: APIRepository? = null
private val _apiName = MutableLiveData<String>() private val _apiName = MutableLiveData<String>()
@ -170,10 +163,7 @@ class HomeViewModel : ViewModel() {
currentWatchTypes.remove(WatchType.NONE) currentWatchTypes.remove(WatchType.NONE)
if (currentWatchTypes.size <= 0) { if (currentWatchTypes.size <= 0) {
setKey( DataStoreHelper.homeBookmarkedList = intArrayOf()
HOME_BOOKMARK_VALUE_LIST,
intArrayOf()
)
_availableWatchStatusTypes.postValue(setOf<WatchType>() to setOf()) _availableWatchStatusTypes.postValue(setOf<WatchType>() to setOf())
_bookmarks.postValue(Pair(false, ArrayList())) _bookmarks.postValue(Pair(false, ArrayList()))
return@launchSafe return@launchSafe
@ -181,15 +171,13 @@ class HomeViewModel : ViewModel() {
val watchPrefNotNull = preferredWatchStatus ?: EnumSet.of(currentWatchTypes.first()) val watchPrefNotNull = preferredWatchStatus ?: EnumSet.of(currentWatchTypes.first())
//if (currentWatchTypes.any { watchPrefNotNull.contains(it) }) watchPrefNotNull else listOf(currentWatchTypes.first()) //if (currentWatchTypes.any { watchPrefNotNull.contains(it) }) watchPrefNotNull else listOf(currentWatchTypes.first())
setKey(
HOME_BOOKMARK_VALUE_LIST, DataStoreHelper.homeBookmarkedList = watchPrefNotNull.map { it.internalId }.toIntArray()
watchPrefNotNull.map { it.internalId }.toIntArray()
)
_availableWatchStatusTypes.postValue( _availableWatchStatusTypes.postValue(
Pair(
watchPrefNotNull, watchPrefNotNull to
currentWatchTypes, currentWatchTypes,
)
) )
val list = withContext(Dispatchers.IO) { val list = withContext(Dispatchers.IO) {
@ -463,7 +451,7 @@ class HomeViewModel : ViewModel() {
fun loadStoredData() { fun loadStoredData() {
val list = EnumSet.noneOf(WatchType::class.java) 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) list.addAll(it)
} }
loadStoredData(list) loadStoredData(list)

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.ui.library package com.lagradost.cloudstream3.ui.library
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
@ -8,14 +9,25 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.FOCUS_AFTER_DESCENDANTS
import android.view.ViewGroup.FOCUS_BLOCK_DESCENDANTS
import android.view.animation.AlphaAnimation 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.annotation.StringRes
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.allViews
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels 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.google.android.material.tabs.TabLayoutMediator
import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.allProviders 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.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.syncproviders.BackupAPI import com.lagradost.cloudstream3.syncproviders.BackupAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD 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.SEARCH_ACTION_SHOW_METADATA
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity 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.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
@ -48,7 +65,7 @@ const val LIBRARY_FOLDER = "library_folder"
enum class LibraryOpenerType(@StringRes val stringRes: Int) { 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), Provider(R.string.none),
Browser(R.string.browser), Browser(R.string.browser),
Search(R.string.search), Search(R.string.search),
@ -83,9 +100,21 @@ class LibraryFragment : Fragment() {
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View { ): View {
MainActivity.afterBackupRestoreEvent += ::onNewSyncData MainActivity.afterBackupRestoreEvent += ::onNewSyncData
val localBinding = FragmentLibraryBinding.inflate(inflater, container, false) val layout =
binding = localBinding if (SettingsFragment.isTvSettings()) R.layout.fragment_library_tv else R.layout.fragment_library
return localBinding.root 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) //return inflater.inflate(R.layout.fragment_library, container, false)
} }
@ -103,25 +132,24 @@ class LibraryFragment : Fragment() {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
@SuppressLint("ResourceType", "CutPasteId")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
fixPaddingStatusbar(binding?.searchStatusBarPadding) fixPaddingStatusbar(binding?.searchStatusBarPadding)
binding?.sortFab?.setOnClickListener { binding?.sortFab?.setOnClickListener(sortChangeClickListener)
val methods = libraryViewModel.sortingMethods.map { binding?.librarySort?.setOnClickListener(sortChangeClickListener)
txt(it.stringRes).asString(view.context)
binding?.libraryRoot?.findViewById<TextView>(R.id.search_src_text)?.apply {
tag = "tv_no_focus_tag"
} }
activity?.showBottomDialog(methods, // Set the color for the search exit icon to the correct theme text color
libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod), val searchExitIcon = binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
txt(R.string.sort_by).asString(view.context), val searchExitIconColor = TypedValue()
false,
{}, activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true)
{ searchExitIcon?.setColorFilter(searchExitIconColor.data)
val method = libraryViewModel.sortingMethods[it]
libraryViewModel.sort(method)
})
}
binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean { override fun onQueryTextSubmit(query: String?): Boolean {
@ -186,7 +214,7 @@ class LibraryFragment : Fragment() {
val items = baseOptions.map { txt(it.stringRes).asString(this) } + availableProviders 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 = val selectedIndex =
when { when {
savedSelection == null -> 0 savedSelection == null -> 0
@ -221,7 +249,7 @@ class LibraryFragment : Fragment() {
} }
setKey( setKey(
LIBRARY_FOLDER, "$currentAccount/$LIBRARY_FOLDER",
key, key,
savedData, savedData,
) )
@ -234,6 +262,7 @@ class LibraryFragment : Fragment() {
} }
binding?.viewpager?.setPageTransformer(LibraryScrollTransformer()) binding?.viewpager?.setPageTransformer(LibraryScrollTransformer())
binding?.viewpager?.adapter = binding?.viewpager?.adapter =
binding?.viewpager?.adapter ?: ViewpagerAdapter( binding?.viewpager?.adapter ?: ViewpagerAdapter(
mutableListOf(), mutableListOf(),
@ -268,8 +297,11 @@ class LibraryFragment : Fragment() {
// This basically first selects the individual opener and if that is default then // This basically first selects the individual opener and if that is default then
// selects the whole list opener // selects the whole list opener
val savedListSelection = val savedListSelection =
getKey<LibraryOpener>(LIBRARY_FOLDER, syncName.name) getKey<LibraryOpener>("$currentAccount/$LIBRARY_FOLDER", syncName.name)
val savedSelection = getKey<LibraryOpener>(LIBRARY_FOLDER, syncId).takeIf { val savedSelection = getKey<LibraryOpener>(
"$currentAccount/$LIBRARY_FOLDER",
syncId
).takeIf {
it?.openType != LibraryOpenerType.Default it?.openType != LibraryOpenerType.Default
} ?: savedListSelection } ?: savedListSelection
@ -357,11 +389,18 @@ class LibraryFragment : Fragment() {
} }
(viewpager.adapter as? ViewpagerAdapter)?.pages = pages (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 // Using notifyItemRangeChanged keeps the animations when sorting
viewpager.adapter?.notifyItemRangeChanged( viewpager.adapter?.notifyItemRangeChanged(
0, 0,
viewpager.adapter?.itemCount ?: 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 // Only stop loading after 300ms to hide the fade effect the viewpager produces when updating
// Without this there would be a flashing effect: // Without this there would be a flashing effect:
@ -398,6 +437,9 @@ class LibraryFragment : Fragment() {
viewpager, viewpager,
) { tab, position -> ) { tab, position ->
tab.text = pages.getOrNull(position)?.title?.asStringNull(context) 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 { tab.view.setOnClickListener {
val currentItem = val currentItem =
binding?.viewpager?.currentItem ?: return@setOnClickListener 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) { override fun onConfigurationChanged(newConfig: Configuration) {
(binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind() (binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind()
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
@ -440,6 +499,21 @@ class LibraryFragment : Fragment() {
private fun onNewSyncData(unused: Unit) { private fun onNewSyncData(unused: Unit) {
Log.d(BackupAPI.LOG_KEY, "will reload pages") Log.d(BackupAPI.LOG_KEY, "will reload pages")
libraryViewModel.reloadPages(true) 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 androidx.lifecycle.ViewModel
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
enum class ListSorting(@StringRes val stringRes: Int) { enum class ListSorting(@StringRes val stringRes: Int) {
Query(R.string.none), Query(R.string.none),
@ -28,6 +31,8 @@ class LibraryViewModel : ViewModel() {
private val _pages: MutableLiveData<Resource<List<SyncAPI.Page>>> = MutableLiveData(null) private val _pages: MutableLiveData<Resource<List<SyncAPI.Page>>> = MutableLiveData(null)
val pages: LiveData<Resource<List<SyncAPI.Page>>> = _pages val pages: LiveData<Resource<List<SyncAPI.Page>>> = _pages
var currentPage: Int = 0
private val _currentApiName: MutableLiveData<String> = MutableLiveData("") private val _currentApiName: MutableLiveData<String> = MutableLiveData("")
val currentApiName: LiveData<String> = _currentApiName val currentApiName: LiveData<String> = _currentApiName
@ -35,12 +40,12 @@ class LibraryViewModel : ViewModel() {
get() = SyncApis.filter { it.hasAccount() } get() = SyncApis.filter { it.hasAccount() }
var currentSyncApi = availableSyncApis.let { allApis -> 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() availableSyncApis.firstOrNull { it.name == lastSelection } ?: allApis.firstOrNull()
} }
private set(value) { private set(value) {
field = value field = value
setKey(LAST_SYNC_API_KEY, field?.name) setKey("$currentAccount/$LAST_SYNC_API_KEY", field?.name)
} }
val availableApiNames: List<String> val availableApiNames: List<String>
@ -58,13 +63,21 @@ class LibraryViewModel : ViewModel() {
reloadPages(true) reloadPages(true)
} }
fun sort(method: ListSorting, query: String? = null) { fun sort(method: ListSorting, query: String? = null) = ioSafe {
val currentList = pages.value ?: return 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 currentSortingMethod = method
(currentList as? Resource.Success)?.value?.forEachIndexed { _, page -> DataStoreHelper.librarySortingMode = method.ordinal
items.forEach { page ->
page.sort(method, query) page.sort(method, query)
} }
_pages.postValue(currentList) _pages.postValue(Resource.Success(items))
} }
fun reloadPages(forceReload: Boolean) { fun reloadPages(forceReload: Boolean) {
@ -85,8 +98,6 @@ class LibraryViewModel : ViewModel() {
val library = (libraryResource as? Resource.Success)?.value ?: return@let val library = (libraryResource as? Resource.Success)?.value ?: return@let
sortingMethods = library.supportedListSorting.toList() sortingMethods = library.supportedListSorting.toList()
currentSortingMethod = null
repo.requireLibraryRefresh = false repo.requireLibraryRefresh = false
val pages = library.allLibraryLists.map { 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) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) { when (holder) {
is PageViewHolder -> { 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) : inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) :
RecyclerView.ViewHolder(binding.root) { 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 { binding.pageRecyclerview.apply {
spanCount = spanCount =
this@PageViewHolder.itemView.context.getSpanCount() ?: 3 this@PageViewHolder.itemView.context.getSpanCount() ?: 3

View file

@ -18,10 +18,9 @@ import android.widget.ProgressBar
import android.widget.Toast import android.widget.Toast
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
@ -30,12 +29,15 @@ import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import androidx.media3.ui.SubtitleView import androidx.media3.ui.SubtitleView
import androidx.media3.ui.TimeBar import androidx.media3.ui.TimeBar
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey 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.canEnterPipMode
import com.lagradost.cloudstream3.CommonActivity.isInPIPMode import com.lagradost.cloudstream3.CommonActivity.isInPIPMode
import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.keyEventListener
import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener
import com.lagradost.cloudstream3.CommonActivity.screenWidth
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
@ -45,6 +47,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.EpisodeSkip
import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
@ -135,8 +138,10 @@ abstract class AbstractPlayerFragment(
} }
} }
private fun updateIsPlaying(wasPlaying : CSPlayerLoading, private fun updateIsPlaying(
isPlaying : CSPlayerLoading) { wasPlaying: CSPlayerLoading,
isPlaying: CSPlayerLoading
) {
val isPlayingRightNow = CSPlayerLoading.IsPlaying == isPlaying val isPlayingRightNow = CSPlayerLoading.IsPlaying == isPlaying
val isPausedRightNow = CSPlayerLoading.IsPaused == isPlaying val isPausedRightNow = CSPlayerLoading.IsPaused == isPlaying
@ -184,7 +189,11 @@ abstract class AbstractPlayerFragment(
canEnterPipMode = isPlayingRightNow && hasPipModeSupport canEnterPipMode = isPlayingRightNow && hasPipModeSupport
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity?.let { act -> activity?.let { act ->
PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow, player.getAspectRatio()) PlayerPipHelper.updatePIPModeActions(
act,
isPlayingRightNow,
player.getAspectRatio()
)
} }
} }
} }
@ -379,30 +388,39 @@ abstract class AbstractPlayerFragment(
is ResizedEvent -> { is ResizedEvent -> {
playerDimensionsLoaded(event.width, event.height) playerDimensionsLoaded(event.width, event.height)
} }
is PlayerAttachedEvent -> { is PlayerAttachedEvent -> {
playerUpdated(event.player) playerUpdated(event.player)
} }
is SubtitlesUpdatedEvent -> { is SubtitlesUpdatedEvent -> {
subtitlesChanged() subtitlesChanged()
} }
is TimestampSkippedEvent -> { is TimestampSkippedEvent -> {
onTimestampSkipped(event.timestamp) onTimestampSkipped(event.timestamp)
} }
is TimestampInvokedEvent -> { is TimestampInvokedEvent -> {
onTimestamp(event.timestamp) onTimestamp(event.timestamp)
} }
is TracksChangedEvent -> { is TracksChangedEvent -> {
onTracksInfoChanged() onTracksInfoChanged()
} }
is EmbeddedSubtitlesFetchedEvent -> { is EmbeddedSubtitlesFetchedEvent -> {
embeddedSubtitlesFetched(event.tracks) embeddedSubtitlesFetched(event.tracks)
} }
is ErrorEvent -> { is ErrorEvent -> {
playerError(event.error) playerError(event.error)
} }
is RequestAudioFocusEvent -> { is RequestAudioFocusEvent -> {
requestAudioFocus() requestAudioFocus()
} }
is EpisodeSeekEvent -> { is EpisodeSeekEvent -> {
when (event.offset) { when (event.offset) {
-1 -> prevEpisode() -1 -> prevEpisode()
@ -410,12 +428,15 @@ abstract class AbstractPlayerFragment(
else -> {} else -> {}
} }
} }
is StatusEvent -> { is StatusEvent -> {
updateIsPlaying(wasPlaying = event.wasPlaying, isPlaying = event.isPlaying) updateIsPlaying(wasPlaying = event.wasPlaying, isPlaying = event.isPlaying)
} }
is PositionEvent -> { is PositionEvent -> {
playerPositionChanged(position = event.toMs, duration = event.durationMs) playerPositionChanged(position = event.toMs, duration = event.durationMs)
} }
is VideoEndedEvent -> { is VideoEndedEvent -> {
context?.let { ctx -> context?.let { ctx ->
// Only play next episode if autoplay is on (default) // Only play next episode if autoplay is on (default)
@ -432,6 +453,7 @@ abstract class AbstractPlayerFragment(
} }
} }
} }
is PauseEvent -> Unit is PauseEvent -> Unit
is PlayEvent -> Unit is PlayEvent -> Unit
} }
@ -439,7 +461,7 @@ abstract class AbstractPlayerFragment(
@SuppressLint("SetTextI18n", "UnsafeOptInUsageError") @SuppressLint("SetTextI18n", "UnsafeOptInUsageError")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
resizeMode = getKey(RESIZE_MODE_KEY) ?: 0 resizeMode = DataStoreHelper.resizeMode
resize(resizeMode, false) resize(resizeMode, false)
player.releaseCallbacks() player.releaseCallbacks()
@ -454,20 +476,71 @@ abstract class AbstractPlayerFragment(
) )
if (player is CS3IPlayer) { 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) subView = playerView?.findViewById(R.id.exo_subtitles)
subStyle = SubtitlesFragment.getCurrentSavedStyle() subStyle = SubtitlesFragment.getCurrentSavedStyle()
player.initSubtitles(subView, subtitleHolder, subStyle) 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 /** 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 */ * 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 { playerView?.findViewById<DefaultTimeBar>(R.id.exo_progress)
?.addListener(object : TimeBar.OnScrubListener {
override fun onScrubStart(timeBar: TimeBar, position: Long) = Unit override fun onScrubStart(timeBar: TimeBar, position: Long) = Unit
override fun onScrubMove(timeBar: TimeBar, position: Long) = Unit override fun onScrubMove(timeBar: TimeBar, position: Long) = Unit
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) { override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
if (canceled) return if (canceled) return
val playerDuration = player.getDuration() ?: return val playerDuration = player.getDuration() ?: return
val playerPosition = player.getPosition() ?: return val playerPosition = player.getPosition() ?: return
mainCallback(PositionEvent(source = PlayerEventSource.UI, durationMs = playerDuration, fromMs = playerPosition, toMs = position)) mainCallback(
PositionEvent(
source = PlayerEventSource.UI,
durationMs = playerDuration,
fromMs = playerPosition,
toMs = position
)
)
} }
}) })
@ -535,7 +608,7 @@ abstract class AbstractPlayerFragment(
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
fun resize(resize: PlayerResize, showToast: Boolean) { fun resize(resize: PlayerResize, showToast: Boolean) {
setKey(RESIZE_MODE_KEY, resize.ordinal) DataStoreHelper.resizeMode = resize.ordinal
val type = when (resize) { val type = when (resize) {
PlayerResize.Fill -> AspectRatioFrameLayout.RESIZE_MODE_FILL PlayerResize.Fill -> AspectRatioFrameLayout.RESIZE_MODE_FILL
PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT

View file

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

View file

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

View file

@ -182,6 +182,7 @@ class GeneratorPlayer : FullScreenPlayer() {
(if (sameEpisode) currentSelectedSubtitles else null) ?: getAutoSelectSubtitle( (if (sameEpisode) currentSelectedSubtitles else null) ?: getAutoSelectSubtitle(
currentSubs, settings = true, downloads = true currentSubs, settings = true, downloads = true
), ),
preview = isFullScreenPlayer
) )
} }
@ -1025,7 +1026,7 @@ class GeneratorPlayer : FullScreenPlayer() {
ctx.getString(R.string.episode_sync_enabled_key), true ctx.getString(R.string.episode_sync_enabled_key), true
) )
) maxEpisodeSet = meta.episode ) 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 package com.lagradost.cloudstream3.ui.player
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.util.Rational import android.util.Rational
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.EpisodeSkip
@ -198,17 +199,8 @@ data class CurrentTracks(
class InvalidFileException(msg: String) : Exception(msg) class InvalidFileException(msg: String) : Exception(msg)
//http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 //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 ACTION_MEDIA_CONTROL = "media_control"
const val EXTRA_CONTROL_TYPE = "control_type" 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 */ /** Abstract Exoplayer logic, can be expanded to other players */
interface IPlayer { interface IPlayer {
@ -246,11 +238,15 @@ interface IPlayer {
startPosition: Long? = null, startPosition: Long? = null,
subtitles: Set<SubtitleData>, subtitles: Set<SubtitleData>,
subtitle: SubtitleData?, subtitle: SubtitleData?,
autoPlay: Boolean? = true autoPlay: Boolean? = true,
preview : Boolean = true,
) )
fun reloadPlayer(context: Context) fun reloadPlayer(context: Context)
fun getPreview(fraction : Float) : Bitmap?
fun hasPreview() : Boolean
fun setActiveSubtitles(subtitles: Set<SubtitleData>) fun setActiveSubtitles(subtitles: Set<SubtitleData>)
fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean // returns true if the player requires a reload, null for nothing fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean // returns true if the player requires a reload, null for nothing
fun getCurrentPreferredSubtitle(): SubtitleData? 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.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -215,11 +216,17 @@ class QuickSearchFragment : Fragment() {
binding?.quickSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn) binding?.quickSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
//val searchMagIcon = //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?.scaleX = 0.65f
// searchMagIcon?.scaleY = 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 { binding?.quickSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean { override fun onQueryTextSubmit(query: String): Boolean {
if (search(context, query, false)) if (search(context, query, false))

View file

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

View file

@ -17,7 +17,6 @@ import android.view.animation.DecelerateInterpolator
import android.widget.AbsListView import android.widget.AbsListView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView import androidx.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.ExtractorLink
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
@ -151,7 +151,8 @@ open class ResultFragmentPhone : FullScreenPlayer() {
startPosition = 0L, startPosition = 0L,
subtitles = emptySet(), subtitles = emptySet(),
subtitle = null, subtitle = null,
autoPlay = false autoPlay = false,
preview = false
) )
true true
} ?: run { } ?: run {
@ -429,10 +430,10 @@ open class ResultFragmentPhone : FullScreenPlayer() {
} }
}) })
resultSubscribe.setOnClickListener { resultSubscribe.setOnClickListener {
val isSubscribed = viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? ->
viewModel.toggleSubscriptionStatus() ?: return@setOnClickListener if (newStatus == null) return@toggleSubscriptionStatus
val message = if (isSubscribed) { val message = if (newStatus) {
// Kinda icky to have this here, but it works. // Kinda icky to have this here, but it works.
SubscriptionWorkManager.enqueuePeriodicWork(context) SubscriptionWorkManager.enqueuePeriodicWork(context)
R.string.subscription_new R.string.subscription_new
@ -444,6 +445,22 @@ open class ResultFragmentPhone : FullScreenPlayer() {
?: txt(R.string.no_data).asStringNull(context) ?: "" ?: txt(R.string.no_data).asStringNull(context) ?: ""
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
} }
}
resultFavorite.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)
}
}
mediaRouteButton.apply { mediaRouteButton.apply {
val chromecastSupport = api?.hasChromecastSupport == true val chromecastSupport = api?.hasChromecastSupport == true
alpha = if (chromecastSupport) 1f else 0.3f alpha = if (chromecastSupport) 1f else 0.3f
@ -563,6 +580,19 @@ open class ResultFragmentPhone : FullScreenPlayer() {
binding?.resultSubscribe?.setImageResource(drawable) 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 -> observe(viewModel.trailers) { trailers ->
setTrailers(trailers.flatMap { it.mirros }) // I dont care about subtitles yet! setTrailers(trailers.flatMap { it.mirros }) // I dont care about subtitles yet!
} }
@ -653,14 +683,13 @@ open class ResultFragmentPhone : FullScreenPlayer() {
resultPoster.setImage(d.posterImage) resultPoster.setImage(d.posterImage)
resultPosterBackground.setImage(d.posterBackgroundImage) resultPosterBackground.setImage(d.posterBackgroundImage)
resultDescription.setTextHtml(d.plotText) resultDescription.setTextHtml(d.plotText)
resultDescription.setOnClickListener { view -> resultDescription.setOnClickListener {
// todo bottom view? activity?.let { activity ->
view.context?.let { ctx -> activity.showBottomDialogText(
val builder: AlertDialog.Builder = d.titleText.asString(activity),
AlertDialog.Builder(ctx, R.style.AlertDialogCustom) d.plotText.asString(activity).html(),
builder.setMessage(d.plotText.asString(ctx).html()) {}
.setTitle(d.plotHeaderText.asString(ctx)) )
.show()
} }
} }
@ -851,16 +880,11 @@ open class ResultFragmentPhone : FullScreenPlayer() {
setRecommendations(recommendations, null) setRecommendations(recommendations, null)
} }
observe(viewModel.episodeSynopsis) { description -> observe(viewModel.episodeSynopsis) { description ->
// TODO bottom dialog activity?.let { activity ->
view.context?.let { ctx -> activity.showBottomDialogText(
val builder: AlertDialog.Builder = activity.getString(R.string.synopsis),
AlertDialog.Builder(ctx, R.style.AlertDialogCustom) description.html()
builder.setMessage(description.html()) ) { viewModel.releaseEpisodeSynopsis() }
.setTitle(R.string.synopsis)
.setOnDismissListener {
viewModel.releaseEpisodeSynopsis()
}
.show()
} }
} }
context?.let { ctx -> context?.let { ctx ->
@ -938,7 +962,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
fab.context.getString(R.string.action_add_to_bookmarks), fab.context.getString(R.string.action_add_to_bookmarks),
showApply = false, 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.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -17,6 +18,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
@ -26,6 +28,7 @@ import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.mvvm.observeNullable
import com.lagradost.cloudstream3.services.SubscriptionWorkManager
import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup
import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator 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.SEARCH_ACTION_FOCUSED
import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.AppUtils.isRtl
@ -216,11 +220,9 @@ class ResultFragmentTv : Fragment() {
episodesShadow.fade(show) episodesShadow.fade(show)
episodeHolderTv.fade(show) episodeHolderTv.fade(show)
if (episodesShadow.isRtl()) { if (episodesShadow.isRtl()) {
episodesShadow.scaleX = -1.0f episodesShadowBackground.scaleX = -1f
episodesShadow.scaleY = -1.0f
} else { } else {
episodesShadow.scaleX = 1.0f episodesShadowBackground.scaleX = 1f
episodesShadow.scaleY = 1.0f
} }
} }
} }
@ -267,6 +269,7 @@ class ResultFragmentTv : Fragment() {
resultEpisodesShow.onFocusChangeListener = rightListener resultEpisodesShow.onFocusChangeListener = rightListener
resultDescription.onFocusChangeListener = leftListener resultDescription.onFocusChangeListener = leftListener
resultBookmarkButton.onFocusChangeListener = leftListener resultBookmarkButton.onFocusChangeListener = leftListener
resultFavoriteButton.onFocusChangeListener = leftListener
resultEpisodesShow.setOnClickListener { resultEpisodesShow.setOnClickListener {
// toggle, to make it more touch accessable just in case someone thinks that a // toggle, to make it more touch accessable just in case someone thinks that a
// tv layout is better but is using a touch device // tv layout is better but is using a touch device
@ -285,7 +288,9 @@ class ResultFragmentTv : Fragment() {
resultPlaySeries, resultPlaySeries,
resultResumeSeries, resultResumeSeries,
resultPlayTrailer, resultPlayTrailer,
resultBookmarkButton resultBookmarkButton,
resultFavoriteButton,
resultSubscribeButton
) )
for (requestView in views) { for (requestView in views) {
if (!requestView.isVisible) continue if (!requestView.isVisible) continue
@ -426,6 +431,8 @@ class ResultFragmentTv : Fragment() {
val aboveCast = listOf( val aboveCast = listOf(
binding?.resultEpisodesShow, binding?.resultEpisodesShow,
binding?.resultBookmarkButton, binding?.resultBookmarkButton,
binding?.resultFavoriteButton,
binding?.resultSubscribeButton,
).firstOrNull { ).firstOrNull {
it?.isVisible == true it?.isVisible == true
} }
@ -528,7 +535,83 @@ class ResultFragmentTv : Fragment() {
view.context.getString(R.string.action_add_to_bookmarks), view.context.getString(R.string.action_add_to_bookmarks),
showApply = false, 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 -> observeNullable(viewModel.movie) { data ->
binding?.apply { binding?.apply {
resultPlayMovie.isVisible = data is Resource.Success resultPlayMovie.isVisible = data is Resource.Success
resultPlaySeries.isVisible = data == null
seriesHolder.isVisible = data == null seriesHolder.isVisible = data == null
resultEpisodesShow.isVisible = data == null resultEpisodesShow.isVisible = data == null
@ -766,12 +850,14 @@ class ResultFragmentTv : Fragment() {
R.drawable.profile_bg_red, R.drawable.profile_bg_red,
R.drawable.profile_bg_teal R.drawable.profile_bg_teal
).random() ).random()
//Change poster crop area to 20% from Top
backgroundPoster.cropYCenterOffsetPct = 0.20F
backgroundPoster.setImage( backgroundPoster.setImage(
d.posterBackgroundImage ?: UiImage.Drawable(error), d.posterBackgroundImage ?: UiImage.Drawable(error),
radius = 0, radius = 0,
errorImageDrawable = error errorImageDrawable = error
) )
resultComingSoon.isVisible = d.comingSoon resultComingSoon.isVisible = d.comingSoon
resultDataHolder.isGone = d.comingSoon resultDataHolder.isGone = d.comingSoon
UIHelper.populateChips(resultTag, d.tags) UIHelper.populateChips(resultTag, d.tags)

View file

@ -7,6 +7,8 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
@ -31,6 +33,7 @@ import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu 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.APIRepository
import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO 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.getNameFull
import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled
import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast 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.CastHelper.startCast
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe
import com.lagradost.cloudstream3.utils.Coroutines.main 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.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.getResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState 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.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.setDub
import com.lagradost.cloudstream3.utils.DataStoreHelper.setFavoritesData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason 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 com.lagradost.cloudstream3.utils.UIHelper.navigate
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
@ -110,6 +131,18 @@ data class ResultData(
val plotHeaderText: UiText, 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? { fun txt(status: DubStatus?): UiText? {
return txt( return txt(
when (status) { when (status) {
@ -425,6 +458,9 @@ class ResultViewModel2 : ViewModel() {
private val _subscribeStatus: MutableLiveData<Boolean?> = MutableLiveData(null) private val _subscribeStatus: MutableLiveData<Boolean?> = MutableLiveData(null)
val subscribeStatus: LiveData<Boolean?> = _subscribeStatus val subscribeStatus: LiveData<Boolean?> = _subscribeStatus
private val _favoriteStatus: MutableLiveData<Boolean?> = MutableLiveData(null)
val favoriteStatus: LiveData<Boolean?> = _favoriteStatus
companion object { companion object {
const val TAG = "RVM2" const val TAG = "RVM2"
//private const val EPISODE_RANGE_SIZE = 20 //private const val EPISODE_RANGE_SIZE = 20
@ -435,33 +471,6 @@ class ResultViewModel2 : ViewModel() {
return this?.firstOrNull { it.season == season } 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? { private fun filterName(name: String?): String? {
if (name == null) return null if (name == null) return null
Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let { Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let {
@ -816,9 +825,77 @@ class ResultViewModel2 : ViewModel() {
val selectPopup: LiveData<SelectPopup?> = _selectPopup val selectPopup: LiveData<SelectPopup?> = _selectPopup
fun updateWatchStatus(status: WatchType) { fun updateWatchStatus(
updateWatchStatus(currentResponse ?: return, status) 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) _watchStatus.postValue(status)
statusChangedCallback?.invoke(true)
}
} }
private fun startChromecast( 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. * Toggles the subscription status of an item.
**/ *
fun toggleSubscriptionStatus(): Boolean? { * @param context The context to use for operations.
val isSubscribed = _subscribeStatus.value ?: return null * @param statusChangedCallback A callback that is invoked when the subscription status changes.
val response = currentResponse ?: return null * It provides the new subscription status (true if subscribed, false if unsubscribed, null if action was canceled).
if (response !is EpisodeResponse) return null */
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() val currentId = response.getId()
if (isSubscribed) { if (isSubscribed) {
DataStoreHelper.removeSubscribedData(currentId) removeSubscribedData(currentId)
statusChangedCallback?.invoke(false)
_subscribeStatus.postValue(false)
} else { } 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( if (duplicateIds.isNotEmpty()) {
duplicateIds.forEach { duplicateId ->
removeSubscribedData(duplicateId)
}
}
val current = getSubscribedData(currentId)
setSubscribedData(
currentId, currentId,
DataStoreHelper.SubscribedData( DataStoreHelper.SubscribedData(
currentId, current?.subscribedTime ?: unixTimeMS,
current?.bookmarkedTime ?: unixTimeMS,
unixTimeMS,
response.getLatestEpisodes(), response.getLatestEpisodes(),
currentId,
unixTimeMS,
response.name, response.name,
response.url, response.url,
response.apiName, response.apiName,
response.type, response.type,
response.posterUrl, response.posterUrl,
response.year response.year,
response.syncData
) )
) )
_subscribeStatus.postValue(true)
statusChangedCallback?.invoke(true)
}
}
} }
_subscribeStatus.postValue(!isSubscribed) /**
return !isSubscribed * 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, " ")
}
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( private fun startChromecast(
@ -1219,7 +1512,7 @@ class ResultViewModel2 : ViewModel() {
// Do not add mark as watched on movies // Do not add mark as watched on movies
if (!listOf(TvType.Movie, TvType.AnimeMovie).contains(click.data.tvType)) { if (!listOf(TvType.Movie, TvType.AnimeMovie).contains(click.data.tvType)) {
val isWatched = 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 val watchedText = if (isWatched) R.string.action_remove_from_watched
else R.string.action_mark_as_watched else R.string.action_mark_as_watched
@ -1468,12 +1761,12 @@ class ResultViewModel2 : ViewModel() {
ACTION_MARK_AS_WATCHED -> { ACTION_MARK_AS_WATCHED -> {
val isWatched = val isWatched =
DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched getVideoWatchState(click.data.id) == VideoWatchState.Watched
if (isWatched) { if (isWatched) {
DataStoreHelper.setVideoWatchState(click.data.id, VideoWatchState.None) setVideoWatchState(click.data.id, VideoWatchState.None)
} else { } else {
DataStoreHelper.setVideoWatchState(click.data.id, VideoWatchState.Watched) setVideoWatchState(click.data.id, VideoWatchState.Watched)
} }
// Kinda dirty to reload all episodes :( // Kinda dirty to reload all episodes :(
@ -1682,7 +1975,7 @@ class ResultViewModel2 : ViewModel() {
list.subList(start, end).map { list.subList(start, end).map {
val posDur = getViewPos(it.id) val posDur = getViewPos(it.id)
val watchState = val watchState =
DataStoreHelper.getVideoWatchState(it.id) ?: VideoWatchState.None getVideoWatchState(it.id) ?: VideoWatchState.None
it.copy( it.copy(
position = posDur?.position ?: 0, position = posDur?.position ?: 0,
duration = posDur?.duration ?: 0, duration = posDur?.duration ?: 0,
@ -1743,13 +2036,19 @@ class ResultViewModel2 : ViewModel() {
private fun postSubscription(loadResponse: LoadResponse) { private fun postSubscription(loadResponse: LoadResponse) {
if (loadResponse.isEpisodeBased()) { if (loadResponse.isEpisodeBased()) {
val id = loadResponse.getId() val id = loadResponse.getId()
val data = DataStoreHelper.getSubscribedData(id) val data = getSubscribedData(id)
DataStoreHelper.updateSubscribedData(id, data, loadResponse as? EpisodeResponse) updateSubscribedData(id, data, loadResponse as? EpisodeResponse)
val isSubscribed = data != null val isSubscribed = data != null
_subscribeStatus.postValue(isSubscribed) _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?) { private fun postEpisodeRange(indexer: EpisodeIndexer?, range: EpisodeRange?) {
if (range == null || indexer == null) { if (range == null || indexer == null) {
return return
@ -1887,6 +2186,7 @@ class ResultViewModel2 : ViewModel() {
currentResponse = loadResponse currentResponse = loadResponse
postPage(loadResponse, apiRepository) postPage(loadResponse, apiRepository)
postSubscription(loadResponse) postSubscription(loadResponse)
postFavorites(loadResponse)
if (updateEpisodes) if (updateEpisodes)
postEpisodes(loadResponse, updateFillers) postEpisodes(loadResponse, updateFillers)
} }
@ -1915,6 +2215,10 @@ class ResultViewModel2 : ViewModel() {
val id = val id =
mainId + episode + idIndex * 1_000_000 + (i.season?.times(10_000) mainId + episode + idIndex * 1_000_000 + (i.season?.times(10_000)
?: 0) ?: 0)
val totalIndex =
i.season?.let { season -> loadResponse.getTotalEpisodeIndex(episode, season) }
if (!existingEpisodes.contains(id)) { if (!existingEpisodes.contains(id)) {
existingEpisodes.add(id) existingEpisodes.add(id)
val seasonData = loadResponse.seasonNames.getSeason(i.season) val seasonData = loadResponse.seasonNames.getSeason(i.season)
@ -1934,7 +2238,8 @@ class ResultViewModel2 : ViewModel() {
i.description, i.description,
fillers.getOrDefault(episode, false), fillers.getOrDefault(episode, false),
loadResponse.type, loadResponse.type,
mainId mainId,
totalIndex
) )
val season = eps.seasonIndex ?: 0 val season = eps.seasonIndex ?: 0
@ -1963,6 +2268,9 @@ class ResultViewModel2 : ViewModel() {
val seasonData = val seasonData =
loadResponse.seasonNames.getSeason(episode.season) loadResponse.seasonNames.getSeason(episode.season)
val totalIndex =
episode.season?.let { season -> loadResponse.getTotalEpisodeIndex(episodeIndex, season) }
val ep = val ep =
buildResultEpisode( buildResultEpisode(
loadResponse.name, loadResponse.name,
@ -1979,7 +2287,8 @@ class ResultViewModel2 : ViewModel() {
episode.description, episode.description,
null, null,
loadResponse.type, loadResponse.type,
mainId mainId,
totalIndex
) )
val season = ep.seasonIndex ?: 0 val season = ep.seasonIndex ?: 0
@ -2010,7 +2319,8 @@ class ResultViewModel2 : ViewModel() {
null, null,
null, null,
loadResponse.type, loadResponse.type,
mainId mainId,
null
) )
) )
} }
@ -2032,7 +2342,8 @@ class ResultViewModel2 : ViewModel() {
null, null,
null, null,
loadResponse.type, loadResponse.type,
mainId mainId,
null
) )
) )
} }
@ -2054,7 +2365,8 @@ class ResultViewModel2 : ViewModel() {
null, null,
null, null,
loadResponse.type, loadResponse.type,
mainId mainId,
null
) )
) )
} }
@ -2115,13 +2427,13 @@ class ResultViewModel2 : ViewModel() {
postResume() postResume()
} }
fun postResume() { private fun postResume() {
_resumeWatching.postValue(resume()) _resumeWatching.postValue(resume())
} }
private fun resume(): ResumeWatchingStatus? { private fun resume(): ResumeWatchingStatus? {
val correctId = currentId ?: return null val correctId = currentId ?: return null
val resume = DataStoreHelper.getLastWatched(correctId) val resume = getLastWatched(correctId)
val resumeParentId = resume?.parentId val resumeParentId = resume?.parentId
if (resumeParentId != correctId) return null // is null or smth went wrong with getLastWatched if (resumeParentId != correctId) return null // is null or smth went wrong with getLastWatched
val resumeId = resume.episodeId ?: return null// invalid episode id 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.DialogInterface
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -11,6 +12,7 @@ import android.widget.AbsListView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.ImageView import android.widget.ImageView
import android.widget.ListView import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible 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.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.APIHolder.getApiSettings 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.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.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.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.FragmentSearchBinding
import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding
import com.lagradost.cloudstream3.mvvm.Resource 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.ownShow
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar 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 com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import java.util.concurrent.locks.ReentrantLock 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() { class SearchFragment : Fragment() {
companion object { companion object {
fun List<SearchResponse>.filterSearchResponse(): List<SearchResponse> { fun List<SearchResponse>.filterSearchResponse(): List<SearchResponse> {
@ -193,7 +197,7 @@ class SearchFragment : Fragment() {
validAPIs.flatMap { api -> api.supportedTypes }.distinct() validAPIs.flatMap { api -> api.supportedTypes }.distinct()
) { list -> ) { list ->
if (selectedSearchTypes.toSet() != list.toSet()) { if (selectedSearchTypes.toSet() != list.toSet()) {
setKey(SEARCH_PREF_TAGS, selectedSearchTypes) DataStoreHelper.searchPreferenceTags = list
selectedSearchTypes.clear() selectedSearchTypes.clear()
selectedSearchTypes.addAll(list) selectedSearchTypes.addAll(list)
search(binding?.mainSearch?.query?.toString()) search(binding?.mainSearch?.query?.toString())
@ -219,7 +223,7 @@ class SearchFragment : Fragment() {
SearchHelper.handleSearchClickCallback(callback) SearchHelper.handleSearchClickCallback(callback)
} }
searchRoot.findViewById<TextView>(R.id.search_src_text)?.tag = "tv_no_focus_tag"
searchAutofitResults.adapter = adapter searchAutofitResults.adapter = adapter
searchLoadingBar.alpha = 0f searchLoadingBar.alpha = 0f
} }
@ -228,17 +232,17 @@ class SearchFragment : Fragment() {
val searchExitIcon = val searchExitIcon =
binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn) binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
// val searchMagIcon = // val searchMagIcon =
// main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon) // binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
// searchMagIcon.scaleX = 0.65f // searchMagIcon.scaleX = 0.65f
// searchMagIcon.scaleY = 0.65f // searchMagIcon.scaleY = 0.65f
context?.let { ctx -> // Set the color for the search exit icon to the correct theme text color
val validAPIs = ctx.filterProviderByPreferredMedia() val searchExitIconColor = TypedValue()
selectedApis = ctx.getKey(
SEARCH_PREF_PROVIDERS, activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true)
defVal = validAPIs.map { it.name } searchExitIcon?.setColorFilter(searchExitIconColor.data)
)!!.toMutableSet()
} selectedApis = DataStoreHelper.searchPreferenceProviders.toMutableSet()
binding?.searchFilter?.setOnClickListener { searchView -> binding?.searchFilter?.setOnClickListener { searchView ->
searchView?.context?.let { ctx -> searchView?.context?.let { ctx ->
@ -286,7 +290,7 @@ class SearchFragment : Fragment() {
} }
fun updateList(types: List<TvType>) { fun updateList(types: List<TvType>) {
setKey(SEARCH_PREF_TAGS, types.map { it.name }) DataStoreHelper.searchPreferenceTags = types
arrayAdapter.clear() arrayAdapter.clear()
currentValidApis = validAPIs.filter { api -> currentValidApis = validAPIs.filter { api ->
@ -311,12 +315,7 @@ class SearchFragment : Fragment() {
arrayAdapter.notifyDataSetChanged() arrayAdapter.notifyDataSetChanged()
} }
val selectedSearchTypes = getKey<List<String>>(SEARCH_PREF_TAGS) val selectedSearchTypes = DataStoreHelper.searchPreferenceTags
?.mapNotNull { listName ->
TvType.values().firstOrNull { it.name == listName }
}
?.toMutableList()
?: mutableListOf(TvType.Movie, TvType.TvSeries)
bindChips( bindChips(
binding.tvtypesChipsScroll.tvtypesChips, binding.tvtypesChipsScroll.tvtypesChips,
@ -342,7 +341,7 @@ class SearchFragment : Fragment() {
} }
dialog.setOnDismissListener { dialog.setOnDismissListener {
context?.setKey(SEARCH_PREF_PROVIDERS, currentSelectedApis.toList()) DataStoreHelper.searchPreferenceProviders = currentSelectedApis.toList()
selectedApis = currentSelectedApis selectedApis = currentSelectedApis
} }
updateList(selectedSearchTypes.toList()) updateList(selectedSearchTypes.toList())
@ -353,10 +352,7 @@ class SearchFragment : Fragment() {
val settingsManager = context?.let { PreferenceManager.getDefaultSharedPreferences(it) } val settingsManager = context?.let { PreferenceManager.getDefaultSharedPreferences(it) }
val isAdvancedSearch = settingsManager?.getBoolean("advanced_search", true) ?: true val isAdvancedSearch = settingsManager?.getBoolean("advanced_search", true) ?: true
selectedSearchTypes = context?.getKey<List<String>>(SEARCH_PREF_TAGS) selectedSearchTypes = DataStoreHelper.searchPreferenceTags.toMutableList()
?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } }
?.toMutableList()
?: mutableListOf(TvType.Movie, TvType.TvSeries)
if (isTrueTvSettings()) { if (isTrueTvSettings()) {
binding?.searchFilter?.isFocusable = true binding?.searchFilter?.isFocusable = true
@ -398,7 +394,7 @@ class SearchFragment : Fragment() {
DialogInterface.OnClickListener { _, which -> DialogInterface.OnClickListener { _, which ->
when (which) { when (which) {
DialogInterface.BUTTON_POSITIVE -> { DialogInterface.BUTTON_POSITIVE -> {
removeKeys(SEARCH_HISTORY_KEY) removeKeys("$currentAccount/$SEARCH_HISTORY_KEY")
searchViewModel.updateHistory() searchViewModel.updateHistory()
} }
DialogInterface.BUTTON_NEGATIVE -> { DialogInterface.BUTTON_NEGATIVE -> {
@ -510,7 +506,7 @@ class SearchFragment : Fragment() {
binding?.mainSearch?.setQuery(searchItem.searchText, true) binding?.mainSearch?.setQuery(searchItem.searchText, true)
} }
SEARCH_HISTORY_REMOVE -> { SEARCH_HISTORY_REMOVE -> {
removeKey(SEARCH_HISTORY_KEY, searchItem.key) removeKey("$currentAccount/$SEARCH_HISTORY_KEY", searchItem.key)
searchViewModel.updateHistory() searchViewModel.updateHistory()
} }
else -> { else -> {

View file

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

View file

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

View file

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

View file

@ -20,15 +20,17 @@ import com.lagradost.cloudstream3.databinding.LogcatBinding
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener 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.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar 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.BackupUtils.restorePrompt
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog 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.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
@ -50,7 +52,30 @@ class SettingsUpdates : PreferenceFragmentCompat() {
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) //val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
getPref(R.string.backup_key)?.setOnPreferenceClickListener { 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 return@setOnPreferenceClickListener true
} }
@ -179,7 +204,8 @@ class SettingsUpdates : PreferenceFragmentCompat() {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context) val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context)
val prefNames = resources.getStringArray(R.array.auto_download_plugin) 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) 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), getString(R.string.automatic_plugin_download_mode_title),
true, 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) } (context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) }
} }
return@setOnPreferenceClickListener true return@setOnPreferenceClickListener true

View file

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

View file

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

View file

@ -6,11 +6,14 @@ import android.text.Editable
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.unixTimeMS 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.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey 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.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.WhoIsWatchingAdapter 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.UiImage
import com.lagradost.cloudstream3.ui.result.VideoWatchState import com.lagradost.cloudstream3.ui.result.VideoWatchState
import com.lagradost.cloudstream3.ui.result.setImage import com.lagradost.cloudstream3.ui.result.setImage
import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe 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_POS_DUR = "video_pos_dur"
const val VIDEO_WATCH_STATE = "video_watch_state" const val VIDEO_WATCH_STATE = "video_watch_state"
const val RESULT_WATCH_STATE = "result_watch_state" const val RESULT_WATCH_STATE = "result_watch_state"
const val RESULT_WATCH_STATE_DATA = "result_watch_state_data" const val RESULT_WATCH_STATE_DATA = "result_watch_state_data"
const val RESULT_SUBSCRIBED_STATE_DATA = "result_subscribed_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 = "result_resume_watching_2" // changed due to id changes
const val RESULT_RESUME_WATCHING_OLD = "result_resume_watching" const val RESULT_RESUME_WATCHING_OLD = "result_resume_watching"
const val RESULT_RESUME_WATCHING_HAS_MIGRATED = "result_resume_watching_migrated" 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_SEASON = "result_season"
const val RESULT_DUB = "result_dub" 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 { object DataStoreHelper {
// be aware, don't change the index of these as Account uses the index for the art // be aware, don't change the index of these as Account uses the index for the art
private val profileImages = arrayOf( private val profileImages = arrayOf(
@ -56,6 +85,49 @@ object DataStoreHelper {
R.drawable.profile_bg_teal 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( data class Account(
@JsonProperty("keyIndex") @JsonProperty("keyIndex")
val keyIndex: Int, val keyIndex: Int,
@ -65,6 +137,8 @@ object DataStoreHelper {
val customImage: String? = null, val customImage: String? = null,
@JsonProperty("defaultImageIndex") @JsonProperty("defaultImageIndex")
val defaultImageIndex: Int, val defaultImageIndex: Int,
@JsonProperty("lockPin")
val lockPin: String? = null,
) { ) {
val image: UiImage val image: UiImage
get() = customImage?.let { UiImage.Image(it) } ?: UiImage.Drawable( get() = customImage?.let { UiImage.Image(it) } ?: UiImage.Drawable(
@ -131,7 +205,6 @@ object DataStoreHelper {
// update UI // update UI
setAccount(getDefaultAccount(context), true) setAccount(getDefaultAccount(context), true)
MainActivity.bookmarksUpdatedEvent(true)
dialog?.dismissSafe() dialog?.dismissSafe()
} }
@ -160,36 +233,86 @@ object DataStoreHelper {
binding.profilePic.setImage(account.image) binding.profilePic.setImage(account.image)
binding.profilePic.setOnClickListener { binding.profilePic.setOnClickListener {
// rolls the image forwards once // Roll the image forwards once
currentEditAccount = currentEditAccount =
currentEditAccount.copy(defaultImageIndex = (currentEditAccount.defaultImageIndex + 1) % profileImages.size) currentEditAccount.copy(defaultImageIndex = (currentEditAccount.defaultImageIndex + 1) % profileImages.size)
binding.profilePic.setImage(currentEditAccount.image) binding.profilePic.setImage(currentEditAccount.image)
} }
binding.applyBtt.setOnClickListener { binding.applyBtt.setOnClickListener {
val currentAccounts = accounts.toMutableList() if (currentEditAccount.lockPin != null) {
// Ask for the current PIN
val overrideIndex = showPinInputDialog(context, currentEditAccount.lockPin, false) { pin ->
currentAccounts.indexOfFirst { it.keyIndex == currentEditAccount.keyIndex } if (pin == null) return@showPinInputDialog
// PIN is correct, proceed to update the account
// if an account is found that has the same keyIndex then override that one, if not then append it performAccountUpdate(currentEditAccount)
if (overrideIndex != -1) {
currentAccounts[overrideIndex] = currentEditAccount
} else {
currentAccounts.add(currentEditAccount)
}
// 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() dialog.dismissSafe()
} }
} else {
// No lock PIN set, proceed to update the account
performAccountUpdate(currentEditAccount)
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 { 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) { fun showWhoIsWatching(context: Context) {
val binding: WhoIsWatchingBinding = WhoIsWatchingBinding.inflate( val binding: WhoIsWatchingBinding = WhoIsWatchingBinding.inflate(LayoutInflater.from(context))
LayoutInflater.from(context) val builder = BottomSheetDialog(context)
) builder.setContentView(binding.root)
val showAccount = accounts.toMutableList().apply { val showAccount = accounts.toMutableList().apply {
val item = getDefaultAccount(context) val item = getDefaultAccount(context)
@ -213,22 +344,25 @@ object DataStoreHelper {
add(0, item) add(0, item)
} }
val builder =
BottomSheetDialog(context)
builder.setContentView(binding.root)
val accountName = context.getString(R.string.account) val accountName = context.getString(R.string.account)
binding.profilesRecyclerview.setLinearListLayout( binding.profilesRecyclerview.setLinearListLayout(isHorizontal = true)
isHorizontal = true,
nextUp = FOCUS_SELF,
nextDown = FOCUS_SELF,
nextLeft = FOCUS_SELF,
nextRight = FOCUS_SELF
)
binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter( binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter(
selectCallBack = { account -> selectCallBack = { account ->
// 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) setAccount(account, true)
builder.dismissSafe() builder.dismissSafe()
}
} else {
// No lock PIN set, directly set the account
setAccount(account, true)
builder.dismissSafe()
}
}, },
addAccountCallback = { addAccountCallback = {
val currentAccounts = accounts val currentAccounts = accounts
@ -264,7 +398,6 @@ object DataStoreHelper {
builder.show() builder.show()
} }
data class PosDur( data class PosDur(
@JsonProperty("position") val position: Long, @JsonProperty("position") val position: Long,
@JsonProperty("duration") val duration: Long @JsonProperty("duration") val duration: Long
@ -282,20 +415,35 @@ object DataStoreHelper {
/** /**
* Used to display notifications on new episodes and posters in library. * Used to display notifications on new episodes and posters in library.
**/ **/
data class SubscribedData( abstract class LibrarySearchResponse(
@JsonProperty("id") override var id: Int?, @JsonProperty("id") override var id: Int?,
@JsonProperty("subscribedTime") val bookmarkedTime: Long, @JsonProperty("latestUpdatedTime") open val latestUpdatedTime: Long,
@JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long,
@JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map<DubStatus, Int?>,
@JsonProperty("name") override val name: String, @JsonProperty("name") override val name: String,
@JsonProperty("url") override val url: String, @JsonProperty("url") override val url: String,
@JsonProperty("apiName") override val apiName: 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("posterUrl") override var posterUrl: String?,
@JsonProperty("year") val year: Int?, @JsonProperty("year") open val year: Int?,
@JsonProperty("quality") override var quality: SearchQuality? = null, @JsonProperty("syncData") open val syncData: Map<String, String>?,
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null, @JsonProperty("quality") override var quality: SearchQuality?,
) : SearchResponse { @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? { fun toLibraryItem(): SyncAPI.LibraryItem? {
return SyncAPI.LibraryItem( return SyncAPI.LibraryItem(
name, name,
@ -311,18 +459,19 @@ object DataStoreHelper {
} }
data class BookmarkedData( data class BookmarkedData(
@JsonProperty("id") override var id: Int?,
@JsonProperty("bookmarkedTime") val bookmarkedTime: Long, @JsonProperty("bookmarkedTime") val bookmarkedTime: Long,
@JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long, override var id: Int?,
@JsonProperty("name") override val name: String, override val latestUpdatedTime: Long,
@JsonProperty("url") override val url: String, override val name: String,
@JsonProperty("apiName") override val apiName: String, override val url: String,
@JsonProperty("type") override var type: TvType? = null, override val apiName: String,
@JsonProperty("posterUrl") override var posterUrl: String?, override var type: TvType?,
@JsonProperty("year") val year: Int?, override var posterUrl: String?,
@JsonProperty("quality") override var quality: SearchQuality? = null, override val year: Int?,
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null, override val syncData: Map<String, String>? = null,
) : SearchResponse { 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 { fun toLibraryItem(id: String): SyncAPI.LibraryItem {
return SyncAPI.LibraryItem( return SyncAPI.LibraryItem(
name, 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( data class ResumeWatchingResult(
@JsonProperty("name") override val name: String, @JsonProperty("name") override val name: String,
@JsonProperty("url") override val url: String, @JsonProperty("url") override val url: String,
@ -369,15 +546,9 @@ object DataStoreHelper {
removeKeys(folder) 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?) { fun deleteBookmarkedData(id: Int?) {
if (id == null) return if (id == null) return
AccountManager.localListApi.requireLibraryRefresh = true
removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString()) removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString())
removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", 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()) 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> { fun getAllSubscriptions(): List<SubscribedData> {
return getKeys("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA")?.mapNotNull { return getKeys("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA")?.mapNotNull {
getKey(it) getKey(it)
@ -510,6 +687,29 @@ object DataStoreHelper {
return getKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString()) 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) { fun setViewPos(id: Int?, pos: Long, dur: Long) {
if (id == null) return if (id == null) return
if (dur < 30_000) return // too short 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.Mcloud
import com.lagradost.cloudstream3.extractors.Megacloud import com.lagradost.cloudstream3.extractors.Megacloud
import com.lagradost.cloudstream3.extractors.Meownime import com.lagradost.cloudstream3.extractors.Meownime
import com.lagradost.cloudstream3.extractors.Minoplres
import com.lagradost.cloudstream3.extractors.MixDrop import com.lagradost.cloudstream3.extractors.MixDrop
import com.lagradost.cloudstream3.extractors.MixDropBz import com.lagradost.cloudstream3.extractors.MixDropBz
import com.lagradost.cloudstream3.extractors.MixDropCh 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.Sendvid
import com.lagradost.cloudstream3.extractors.ShaveTape import com.lagradost.cloudstream3.extractors.ShaveTape
import com.lagradost.cloudstream3.extractors.Solidfiles 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.Ssbstream
import com.lagradost.cloudstream3.extractors.StreamM4u import com.lagradost.cloudstream3.extractors.StreamM4u
import com.lagradost.cloudstream3.extractors.StreamSB import com.lagradost.cloudstream3.extractors.StreamSB
@ -380,6 +378,15 @@ open class ExtractorLink constructor(
val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8 val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8
val isDash : Boolean get() = type == ExtractorLinkType.DASH 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( constructor(
source: String, source: String,
name: String, name: String,
@ -748,9 +755,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Vido(), Vido(),
Linkbox(), Linkbox(),
Acefile(), Acefile(),
SpeedoStream(), Minoplres(), // formerly SpeedoStream
SpeedoStream1(),
SpeedoStream2(),
Zorofile(), Zorofile(),
Embedgram(), Embedgram(),
Mvidoo(), Mvidoo(),

View file

@ -8,6 +8,7 @@ import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.engine.DiskCacheStrategy 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.load.model.GlideUrl
import com.bumptech.glide.module.AppGlideModule import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
@ -27,6 +28,10 @@ class GlideModule : AppGlideModule() {
RequestOptions() RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.ALL) .diskCacheStrategy(DiskCacheStrategy.ALL)
.signature(ObjectKey(System.currentTimeMillis().toShort())) .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 = private val QUALITY_REGEX =
Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""") Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""")
private val TS_EXTENSION_REGEX = 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 //Regex("""(.*\.(ts|jpg|html).*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts
private fun absoluteExtensionDetermination(url: String): String? { private fun absoluteExtensionDetermination(url: String): String? {
@ -115,13 +115,22 @@ object M3u8Helper2 {
private fun selectBest(qualities: List<M3u8Helper.M3u8Stream>): M3u8Helper.M3u8Stream? { private fun selectBest(qualities: List<M3u8Helper.M3u8Stream>): M3u8Helper.M3u8Stream? {
val result = qualities.sortedBy { val result = qualities.sortedBy {
if (it.quality != null && it.quality <= 1080) it.quality else 0 it.quality ?: Qualities.Unknown.value //if (it.quality != null && it.quality <= 1080) else 0
}.filter { }/*.filter {
listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl)) listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl))
} }*/
return result.lastOrNull() 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 { private fun getParentLink(uri: String): String {
val split = uri.split("/").toMutableList() val split = uri.split("/").toMutableList()
split.removeLast() split.removeLast()
@ -173,14 +182,20 @@ object M3u8Helper2 {
return list return list
} }
data class TsLink(
val url : String,
val time : Double?,
)
data class LazyHlsDownloadData( data class LazyHlsDownloadData(
private val encryptionData: ByteArray, private val encryptionData: ByteArray,
private val encryptionIv: ByteArray, private val encryptionIv: ByteArray,
private val isEncrypted: Boolean, val isEncrypted: Boolean,
private val allTsLinks: List<String>, val allTsLinks: List<TsLink>,
private val relativeUrl: String, val relativeUrl: String,
private val headers: Map<String, String>, val headers: Map<String, String>,
) { ) {
val size get() = allTsLinks.size val size get() = allTsLinks.size
suspend fun resolveLinkWhileSafe( suspend fun resolveLinkWhileSafe(
@ -228,9 +243,9 @@ object M3u8Helper2 {
@Throws @Throws
suspend fun resolveLink(index: Int): ByteArray { suspend fun resolveLink(index: Int): ByteArray {
if (index < 0 || index >= size) throw IllegalArgumentException("index must be in the bounds of the ts") 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() val tsData = tsResponse.body.bytes()
if (tsData.isEmpty()) throw ErrorLoadingException("no data") if (tsData.isEmpty()) throw ErrorLoadingException("no data")
@ -244,15 +259,16 @@ object M3u8Helper2 {
@Throws @Throws
suspend fun hslLazy( suspend fun hslLazy(
qualities: List<M3u8Helper.M3u8Stream> qualities: List<M3u8Helper.M3u8Stream>, selectBest : Boolean = true
): LazyHlsDownloadData { ): LazyHlsDownloadData {
if (qualities.isEmpty()) throw IllegalArgumentException("qualities must be non empty") 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 headers = selected.headers
val streams = qualities.map { m3u8Generation(it, false) }.flatten() val streams = qualities.map { m3u8Generation(it, false) }.flatten()
// this selects the best quality of the qualities offered, // this selects the best quality of the qualities offered,
// due to the recursive nature of m3u8, we only go 2 depth // 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") ?: throw IllegalArgumentException("qualities has no streams")
val m3u8Response = val m3u8Response =
@ -285,12 +301,14 @@ object M3u8Helper2 {
} }
val relativeUrl = getParentLink(secondSelection.streamUrl) val relativeUrl = getParentLink(secondSelection.streamUrl)
val allTsList = TS_EXTENSION_REGEX.findAll(m3u8Response + "\n").map { ts -> val allTsList = TS_EXTENSION_REGEX.findAll(m3u8Response + "\n").map { ts ->
val value = ts.groupValues[1] val time = ts.groupValues[1]
if (isNotCompleteUrl(value)) { val value = ts.groupValues[3]
val url = if (isNotCompleteUrl(value)) {
"$relativeUrl/${value}" "$relativeUrl/${value}"
} else { } else {
value value
} }
TsLink(url = url, time = time.toDoubleOrNull())
}.toList() }.toList()
if (allTsList.isEmpty()) throw IllegalArgumentException("ts must be non empty") if (allTsList.isEmpty()) throw IllegalArgumentException("ts must be non empty")

View file

@ -1,12 +1,12 @@
package com.lagradost.cloudstream3.utils package com.lagradost.cloudstream3.utils
import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
@ -54,8 +54,12 @@ class PackageInstallerService : Service() {
UPDATE_CHANNEL_NAME, UPDATE_CHANNEL_NAME,
UPDATE_CHANNEL_DESCRIPTION UPDATE_CHANNEL_DESCRIPTION
) )
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()) startForeground(UPDATE_NOTIFICATION_ID, baseNotification.build())
} }
}
private val updateLock = Mutex() 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.Activity
import android.app.Dialog import android.app.Dialog
import android.text.Spanned
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.AbsListView import android.widget.AbsListView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ListView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone import androidx.core.view.isGone
@ -19,7 +17,10 @@ import androidx.core.view.marginRight
import androidx.core.view.marginTop import androidx.core.view.marginTop
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.BottomInputDialogBinding
import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding 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.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
@ -54,14 +55,14 @@ object SingleSelectionHelper {
if (this == null) return if (this == null) return
if (isTvSettings()) { if (isTvSettings()) {
val builder = val binding = OptionsPopupTvBinding.inflate(layoutInflater)
AlertDialog.Builder(this, R.style.AlertDialogCustom) val dialog = AlertDialog.Builder(this, R.style.AlertDialogCustom)
.setView(R.layout.options_popup_tv) .setView(binding.root)
.create()
val dialog = builder.create()
dialog.show() dialog.show()
dialog.findViewById<ListView>(R.id.listview1)?.let { listView -> binding.listview1.let { listView ->
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
listView.adapter = listView.adapter =
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice_color).apply { 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() isGone = poster.isNullOrEmpty()
setImage(poster) setImage(poster)
} }
@ -105,12 +106,12 @@ object SingleSelectionHelper {
if (this == null) return if (this == null) return
val realShowApply = showApply || isMultiSelect val realShowApply = showApply || isMultiSelect
val listView = binding.listview1//.findViewById<ListView>(R.id.listview1)!! val listView = binding.listview1
val textView = binding.text1//.findViewById<TextView>(R.id.text1)!! val textView = binding.text1
val applyButton = binding.applyBtt//.findViewById<TextView>(R.id.apply_btt) val applyButton = binding.applyBtt
val cancelButton = binding.cancelBtt//findViewById<TextView>(R.id.cancel_btt) val cancelButton = binding.cancelBtt
val applyHolder = val applyHolder =
binding.applyBttHolder//.findViewById<LinearLayout>(R.id.apply_btt_holder) binding.applyBttHolder
applyHolder.isVisible = realShowApply applyHolder.isVisible = realShowApply
if (!realShowApply) { if (!realShowApply) {
@ -173,8 +174,8 @@ object SingleSelectionHelper {
} }
} }
private fun Activity?.showInputDialog( private fun Activity?.showInputDialog(
binding: BottomInputDialogBinding,
dialog: Dialog, dialog: Dialog,
value: String, value: String,
name: String, name: String,
@ -184,11 +185,11 @@ object SingleSelectionHelper {
) { ) {
if (this == null) return if (this == null) return
val inputView = dialog.findViewById<EditText>(R.id.nginx_text_input)!! val inputView = binding.nginxTextInput
val textView = dialog.findViewById<TextView>(R.id.text1)!! val textView = binding.text1
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!! val applyButton = binding.applyBtt
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!! val cancelButton = binding.cancelBtt
val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!! val applyHolder = binding.applyBttHolder
applyHolder.isVisible = true applyHolder.isVisible = true
textView.text = name textView.text = name
@ -350,11 +351,17 @@ object SingleSelectionHelper {
dismissCallback: () -> Unit, dismissCallback: () -> Unit,
callback: (String) -> Unit, callback: (String) -> Unit,
) { ) {
val builder = BottomSheetDialog(this) // probably the stuff at the bottom val builder = BottomSheetDialog(this)
builder.setContentView(R.layout.bottom_input_dialog) // input layout
val binding: BottomInputDialogBinding = BottomInputDialogBinding.inflate(
LayoutInflater.from(this)
)
builder.setContentView(binding.root)
builder.show() builder.show()
showInputDialog( showInputDialog(
binding,
builder, builder,
value, value,
name, name,
@ -363,4 +370,24 @@ object SingleSelectionHelper {
dismissCallback 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 Int.toDp: Int get() = (this / Resources.getSystem().displayMetrics.density).toInt()
val Float.toDp: Float get() = (this / Resources.getSystem().displayMetrics.density) val Float.toDp: Float get() = (this / Resources.getSystem().displayMetrics.density)
fun Activity.checkWrite(): Boolean { fun Context.checkWrite(): Boolean {
return (ContextCompat.checkSelfPermission( return (ContextCompat.checkSelfPermission(
this, this,
Manifest.permission.WRITE_EXTERNAL_STORAGE Manifest.permission.WRITE_EXTERNAL_STORAGE
@ -178,9 +178,10 @@ object UIHelper {
fun Activity?.navigate(@IdRes navigation: Int, arguments: Bundle? = null) { fun Activity?.navigate(@IdRes navigation: Int, arguments: Bundle? = null) {
try { try {
if (this is FragmentActivity) { if (this is FragmentActivity) {
(supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment?)?.navController?.navigate( val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment?
navigation, arguments navHostFragment?.navController?.let {
) it.navigate(navigation, arguments)
}
} }
} catch (t: Throwable) { } catch (t: Throwable) {
logError(t) logError(t)

View file

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

View file

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

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <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> </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:layout_margin="5dp"
android:foreground="@drawable/outline_drawable" android:foreground="@drawable/outline_drawable"
app:cardBackgroundColor="@color/transparent" app:cardBackgroundColor="@color/transparent"
android:focusable="true"
app:cardCornerRadius="@dimen/rounded_image_radius" app:cardCornerRadius="@dimen/rounded_image_radius"
app:cardElevation="0dp"> app:cardElevation="0dp">
@ -17,6 +17,7 @@
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:focusable="true"
android:padding="5dp"> android:padding="5dp">
<!--app:cardCornerRadius="@dimen/roundedImageRadius"--> <!--app:cardCornerRadius="@dimen/roundedImageRadius"-->
<FrameLayout <FrameLayout
@ -34,7 +35,6 @@
android:id="@+id/actor_image" android:id="@+id/actor_image"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:contentDescription="@string/episode_poster_img_des" android:contentDescription="@string/episode_poster_img_des"
android:scaleType="centerCrop" android:scaleType="centerCrop"

View file

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

View file

@ -92,15 +92,7 @@
android:layout_height="100dp" android:layout_height="100dp"
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:gravity="center" android:gravity="center"
android:orientation="vertical"> android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="horizontal"
android:padding="20dp">
<TextView <TextView
android:id="@+id/home_preview_bookmark" android:id="@+id/home_preview_bookmark"
@ -139,7 +131,7 @@
app:drawableTint="?attr/white" app:drawableTint="?attr/white"
app:drawableTopCompat="@drawable/ic_outline_info_24" app:drawableTopCompat="@drawable/ic_outline_info_24"
app:tint="?attr/white" /> app:tint="?attr/white" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>

View file

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

View file

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

View file

@ -41,6 +41,20 @@
android:src="@drawable/ic_baseline_extension_24" android:src="@drawable/ic_baseline_extension_24"
app:tint="?attr/textColor" /> 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 <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="40dp" android:layout_height="40dp"
@ -54,6 +68,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginEnd="25dp"
android:iconifiedByDefault="false" android:iconifiedByDefault="false"
android:imeOptions="actionSearch" android:imeOptions="actionSearch"
@ -67,6 +82,7 @@
app:queryBackground="@color/transparent" app:queryBackground="@color/transparent"
app:queryHint="@string/search_hint" app:queryHint="@string/search_hint"
app:searchIcon="@drawable/search_icon" app:searchIcon="@drawable/search_icon"
app:closeIcon="@drawable/ic_baseline_close_24"
tools:ignore="RtlSymmetry"> tools:ignore="RtlSymmetry">
</androidx.appcompat.widget.SearchView> </androidx.appcompat.widget.SearchView>
@ -160,8 +176,7 @@
android:layout_height="40dp" android:layout_height="40dp"
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:background="?attr/primaryGrayBackground" android:background="?attr/primaryGrayBackground"
android:descendantFocusability="blocksDescendants" android:focusable="true"
android:focusable="false"
android:paddingHorizontal="5dp" android:paddingHorizontal="5dp"
app:layout_scrollFlags="noScroll" app:layout_scrollFlags="noScroll"
app:tabGravity="center" 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:descendantFocusability="afterDescendants"
android:paddingBottom="100dp" android:paddingBottom="100dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/result_episode_both_tv" /> tools:listitem="@layout/result_episode" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View file

@ -74,7 +74,7 @@
android:nextFocusUp="@id/result_back" android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_description" android:nextFocusDown="@id/result_description"
android:nextFocusLeft="@id/result_add_sync" android:nextFocusLeft="@id/result_add_sync"
android:nextFocusRight="@id/result_share" android:nextFocusRight="@id/result_favorite"
tools:visibility="visible" tools:visibility="visible"
@ -93,6 +93,23 @@
android:nextFocusUp="@id/result_back" android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_description" android:nextFocusDown="@id/result_description"
android:nextFocusLeft="@id/result_subscribe" 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_favorite"
android:nextFocusRight="@id/result_open_in_browser" android:nextFocusRight="@id/result_open_in_browser"
android:id="@+id/result_share" 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" /> android:textColor="?attr/textColor" />
</LinearLayout> </LinearLayout>
<FrameLayout <FrameLayout
android:id="@+id/background_poster_holder" android:id="@+id/background_poster_holder"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="150dp" android:layout_height="250dp"
android:visibility="visible"> android:visibility="visible">
<ImageView <com.lagradost.cloudstream3.utils.PercentageCropImageView
android:id="@+id/background_poster" android:id="@+id/background_poster"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="250dp" android:layout_height="275dp"
android:layout_gravity="center" android:layout_gravity="center"
android:alpha="0.8" android:alpha="0.8"
android:scaleType="centerCrop" android:scaleType="matrix"
tools:src="@drawable/profile_bg_dark_blue" /> tools:src="@drawable/profile_bg_dark_blue" >
</com.lagradost.cloudstream3.utils.PercentageCropImageView>
<ImageView <ImageView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="100dp" android:layout_height="120dp"
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:src="@drawable/background_shadow"> android:src="@drawable/background_shadow">
</ImageView> </ImageView>
</FrameLayout> </FrameLayout>
@ -155,7 +154,6 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -166,15 +164,8 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="100dp" android:orientation="vertical"
android:orientation="horizontal"> android:layout_marginTop="175dp">
<LinearLayout
android:layout_width="250dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_weight="0"
android:orientation="vertical">
<TextView <TextView
android:id="@+id/result_title" android:id="@+id/result_title"
@ -230,13 +221,26 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:textStyle="normal" android:textStyle="normal"
tools:text="5d 3h 30m" /> tools:text="5d 3h 30m" />
</LinearLayout> </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"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_weight="0"
android:orientation="vertical">
<LinearLayout <LinearLayout
android:id="@+id/result_movie_parent" android:id="@+id/result_movie_parent"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start" android:layout_gravity="start"
android:layout_marginTop="10dp"
android:animateLayoutChanges="true" android:animateLayoutChanges="true"
android:orientation="vertical" android:orientation="vertical"
tools:visibility="visible"> tools:visibility="visible">
@ -310,19 +314,41 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
style="@style/ResultButtonTV" style="@style/ResultButtonTV"
android:nextFocusRight="@id/result_description" android:nextFocusRight="@id/result_description"
android:nextFocusUp="@id/result_play_trailer" android:nextFocusUp="@id/result_play_trailer"
android:nextFocusDown="@id/result_episodes_show" android:nextFocusDown="@id/result_favorite_button"
android:text="@string/type_none" android:text="@string/type_none"
android:visibility="visible" android:visibility="visible"
app:icon="@drawable/ic_baseline_bookmark_24" /> 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 <com.google.android.material.button.MaterialButton
android:id="@+id/result_episodes_show" android:id="@+id/result_episodes_show"
style="@style/ResultButtonTV" style="@style/ResultButtonTV"
android:nextFocusRight="@id/redirect_to_episodes" 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:nextFocusDown="@id/result_cast_items"
android:text="@string/episodes" android:text="@string/episodes"
@ -404,6 +430,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:fadingEdgeLength="30dp" android:fadingEdgeLength="30dp"
android:foreground="@drawable/outline_drawable" android:foreground="@drawable/outline_drawable"
android:maxLines="7" android:maxLines="7"
android:focusable="true"
android:nextFocusUp="@id/result_back" android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_bookmark_button" android:nextFocusDown="@id/result_bookmark_button"
android:padding="5dp" 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" /> app:layout_constraintTop_toTopOf="@+id/shadow_space_1" />
<ImageView <ImageView
android:id="@+id/episodes_shadow_background"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="end" android:layout_gravity="end"

View file

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

View file

@ -50,6 +50,7 @@
app:queryBackground="@color/transparent" app:queryBackground="@color/transparent"
app:queryHint="@string/search_hint" app:queryHint="@string/search_hint"
app:searchIcon="@drawable/search_icon" app:searchIcon="@drawable/search_icon"
app:closeIcon="@drawable/ic_baseline_close_24"
tools:ignore="RtlSymmetry"> tools:ignore="RtlSymmetry">
<requestFocus /> <requestFocus />
@ -84,7 +85,8 @@
android:nextFocusLeft="@id/main_search" android:nextFocusLeft="@id/main_search"
android:nextFocusRight="@id/main_search" android:nextFocusRight="@id/main_search"
android:nextFocusUp="@id/nav_rail_view" 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" android:src="@drawable/ic_baseline_tune_24"
app:tint="?attr/textColor" /> app:tint="?attr/textColor" />
</FrameLayout> </FrameLayout>
@ -141,6 +143,7 @@
android:nextFocusLeft="@id/nav_rail_view" android:nextFocusLeft="@id/nav_rail_view"
android:nextFocusUp="@id/tvtypes_chips" android:nextFocusUp="@id/tvtypes_chips"
android:nextFocusDown="@id/search_clear_call_history" android:nextFocusDown="@id/search_clear_call_history"
android:tag = "@string/tv_no_focus_tag"
android:paddingBottom="50dp" android:paddingBottom="50dp"
android:visibility="visible" android:visibility="visible"
tools:listitem="@layout/search_history_item" /> 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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:focusable="false"
android:tag="tv_no_focus_tag"
tools:listitem="@layout/home_result_grid_expanded" /> 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> </FrameLayout>
<!-- atm this is useless, however it might be used for PIP one day? --> <!-- atm this is useless, however it might be used for PIP one day? -->
<ImageView <ImageView
android:visibility="gone"
android:id="@+id/player_fullscreen" android:id="@+id/player_fullscreen"
android:layout_width="30dp" android:layout_width="30dp"
android:layout_height="30dp" android:layout_height="30dp"
@ -94,7 +93,9 @@
android:layout_marginEnd="20dp" android:layout_marginEnd="20dp"
android:background="?android:attr/selectableItemBackgroundBorderless" android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/baseline_fullscreen_24" android:src="@drawable/baseline_fullscreen_24"
android:visibility="gone"
app:tint="@color/white" /> app:tint="@color/white" />
<FrameLayout <FrameLayout
android:id="@+id/player_intro_play" android:id="@+id/player_intro_play"
android:layout_width="0dp" android:layout_width="0dp"
@ -108,8 +109,8 @@
android:clickable="false" android:clickable="false"
android:focusable="false" android:focusable="false"
android:focusableInTouchMode="false" android:focusableInTouchMode="false"
android:visibility="gone" android:importantForAccessibility="no"
android:importantForAccessibility="no" /> android:visibility="gone" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@ -117,6 +118,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<FrameLayout <FrameLayout
android:id="@+id/player_top_holder" android:id="@+id/player_top_holder"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -272,6 +274,7 @@
app:tint="@color/white" app:tint="@color/white"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
</FrameLayout> </FrameLayout>
<FrameLayout <FrameLayout
android:id="@+id/player_pause_play_holder_holder" android:id="@+id/player_pause_play_holder_holder"
android:layout_width="100dp" android:layout_width="100dp"
@ -298,6 +301,7 @@
app:tint="@color/white" app:tint="@color/white"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
</FrameLayout> </FrameLayout>
<FrameLayout <FrameLayout
android:id="@+id/player_ffwd_holder" android:id="@+id/player_ffwd_holder"
android:layout_width="0dp" android:layout_width="0dp"
@ -345,14 +349,15 @@
android:layout_width="150dp" android:layout_width="150dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginEnd="100dp" android:layout_marginEnd="100dp"
android:layout_marginTop="60dp"
android:backgroundTint="@color/skipOpTransparent" android:backgroundTint="@color/skipOpTransparent"
android:maxLines="1" android:maxLines="1"
android:padding="10dp" android:padding="10dp"
android:textColor="@color/white" android:textColor="@color/white"
android:visibility="gone" android:visibility="gone"
app:cornerRadius="@dimen/rounded_button_radius" app:cornerRadius="@dimen/rounded_button_radius"
app:layout_constraintBottom_toTopOf="@+id/bottom_player_bar"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/player_top_holder"
app:strokeColor="@color/white" app:strokeColor="@color/white"
app:strokeWidth="1dp" app:strokeWidth="1dp"
tools:text="Skip Opening" tools:text="Skip Opening"
@ -427,20 +432,21 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/player_video_bar" android:id="@+id/player_video_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:visibility="visible"
android:layoutDirection="ltr" android:layoutDirection="ltr"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
android:id="@id/exo_position" android:id="@id/exo_position"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="30dp"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginStart="20dp" android:layout_marginStart="20dp"
android:gravity="end" android:gravity="end|center_vertical"
android:includeFontPadding="false" android:includeFontPadding="false"
android:minWidth="50dp" android:minWidth="50dp"
android:paddingLeft="4dp" android:paddingLeft="4dp"
@ -448,14 +454,46 @@
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="normal" android:textStyle="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="15:30" /> 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:id="@id/exo_progress"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_weight="1" android:layout_weight="1"
app:bar_height="2dp" 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:played_color="?attr/colorPrimary"
app:scrubber_color="?attr/colorPrimary" app:scrubber_color="?attr/colorPrimary"
@ -463,12 +501,12 @@
app:scrubber_enabled_size="24dp" app:scrubber_enabled_size="24dp"
app:unplayed_color="@color/videoProgress" /> app:unplayed_color="@color/videoProgress" />
<!-- exo_duration-->
<TextView <TextView
android:id="@id/exo_duration" android:id="@id/exo_duration"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="30dp"
android:layout_gravity="center" android:layout_gravity="center|center_vertical"
android:layout_marginEnd="20dp" android:layout_marginEnd="20dp"
android:includeFontPadding="false" android:includeFontPadding="false"
android:minWidth="50dp" android:minWidth="50dp"
@ -477,9 +515,10 @@
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="normal" android:textStyle="normal"
app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
app:layout_constraintEnd_toEndOf="parent"
tools:text="23:20" /> tools:text="23:20" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<HorizontalScrollView <HorizontalScrollView
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -503,13 +503,12 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"> app:layout_constraintStart_toStartOf="parent">
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/player_video_bar" android:id="@+id/player_video_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layoutDirection="ltr" android:layoutDirection="ltr"
android:orientation="horizontal"> android:orientation="horizontal">
<ImageView <ImageView
android:id="@+id/player_pause_play" android:id="@+id/player_pause_play"
@ -526,14 +525,17 @@
android:tag="@string/tv_no_focus_tag" android:tag="@string/tv_no_focus_tag"
app:tint="@color/player_button_tv" app:tint="@color/player_button_tv"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView <TextView
android:id="@id/exo_position" android:id="@id/exo_position"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="30dp"
android:layout_gravity="center" android:layout_gravity="center"
android:gravity="end" android:layout_marginStart="20dp"
android:gravity="end|center_vertical"
android:includeFontPadding="false" android:includeFontPadding="false"
android:minWidth="50dp" android:minWidth="50dp"
android:paddingLeft="4dp" android:paddingLeft="4dp"
@ -541,29 +543,59 @@
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="normal" android:textStyle="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/player_pause_play"
tools:text="15:30" /> 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:id="@id/exo_progress"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_gravity="center"
android:layout_weight="1" android:layout_weight="1"
android:focusable="false"
android:focusableInTouchMode="false"
app:bar_height="2dp" 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:played_color="?attr/colorPrimary"
app:scrubber_color="?attr/colorPrimary" app:scrubber_color="?attr/colorPrimary"
app:scrubber_dragged_size="26dp" app:scrubber_dragged_size="26dp"
app:scrubber_enabled_size="24dp" app:scrubber_enabled_size="24dp"
app:unplayed_color="@color/videoProgress" /> app:unplayed_color="@color/videoProgress" />
<!-- exo_duration-->
<TextView <TextView
android:id="@id/exo_duration" android:id="@id/exo_duration"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="30dp"
android:layout_gravity="center" android:layout_gravity="center|center_vertical"
android:layout_marginEnd="20dp" android:layout_marginEnd="20dp"
android:includeFontPadding="false" android:includeFontPadding="false"
android:minWidth="50dp" android:minWidth="50dp"
@ -572,9 +604,11 @@
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="normal" android:textStyle="normal"
app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
app:layout_constraintEnd_toEndOf="parent"
tools:text="23:20" /> tools:text="23:20" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<HorizontalScrollView <HorizontalScrollView
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

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

View file

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

View file

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

View file

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

View file

@ -23,6 +23,7 @@
android:background="?android:attr/selectableItemBackgroundBorderless" android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_arrow_back_24" android:src="@drawable/ic_baseline_arrow_back_24"
app:tint="@android:color/white" app:tint="@android:color/white"
android:focusable="true"
android:layout_width="25dp" android:layout_width="25dp"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@ -48,6 +49,8 @@
app:queryBackground="@color/transparent" app:queryBackground="@color/transparent"
app:searchIcon="@drawable/search_icon" app:searchIcon="@drawable/search_icon"
app:closeIcon="@drawable/ic_baseline_close_24"
android:paddingStart="-10dp" android:paddingStart="-10dp"
android:iconifiedByDefault="false" android:iconifiedByDefault="false"
app:queryHint="@string/search_hint" app:queryHint="@string/search_hint"

View file

@ -11,7 +11,9 @@
android:nextFocusRight="@id/download_button" android:nextFocusRight="@id/download_button"
app:cardBackgroundColor="@color/transparent" app:cardBackgroundColor="@color/transparent"
app:cardCornerRadius="@dimen/rounded_image_radius" app:cardCornerRadius="@dimen/rounded_image_radius"
app:cardElevation="0dp"> app:cardElevation="0dp"
android:foreground="@drawable/outline_drawable"
>
<!-- <!--
android:nextFocusLeft="@id/result_episode_download" 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_height="wrap_content"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:foreground="@drawable/outline_drawable"
android:nextFocusRight="@id/download_button" android:nextFocusRight="@id/download_button"
app:cardBackgroundColor="?attr/boxItemBackground" 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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/outline_drawable_less" android:background="@drawable/outline_drawable_less"
android:focusable="true"
android:nextFocusRight="@id/home_history_remove" android:nextFocusRight="@id/home_history_remove"
android:orientation="horizontal"> android:orientation="horizontal">
<!-- android:foreground="?android:attr/selectableItemBackgroundBorderless" <!-- android:foreground="?android:attr/selectableItemBackgroundBorderless"
@ -30,7 +30,7 @@
android:layout_gravity="center_vertical|end" android:layout_gravity="center_vertical|end"
android:background="@drawable/outline_drawable_less" android:background="@drawable/outline_drawable_less"
android:focusable="true"
android:nextFocusLeft="@id/home_history_tab" android:nextFocusLeft="@id/home_history_tab"
android:padding="10dp" android:padding="10dp"
android:src="@drawable/ic_baseline_close_24" android:src="@drawable/ic_baseline_close_24"

View file

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

View file

@ -212,7 +212,6 @@
tools:visibility="visible" /> tools:visibility="visible" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/player_center_menu" android:id="@+id/player_center_menu"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -294,6 +293,7 @@
app:tint="@color/white" app:tint="@color/white"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
</FrameLayout> </FrameLayout>
<FrameLayout <FrameLayout
android:id="@+id/player_ffwd_holder" android:id="@+id/player_ffwd_holder"
android:layout_width="0dp" android:layout_width="0dp"
@ -420,20 +420,20 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/player_video_bar" android:id="@+id/player_video_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layoutDirection="ltr"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
android:id="@id/exo_position" android:id="@id/exo_position"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="30dp"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginStart="20dp" android:layout_marginStart="20dp"
android:gravity="end" android:gravity="end|center_vertical"
android:includeFontPadding="false" android:includeFontPadding="false"
android:minWidth="50dp" android:minWidth="50dp"
android:paddingLeft="4dp" android:paddingLeft="4dp"
@ -441,14 +441,46 @@
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="normal" android:textStyle="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="15:30" /> 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:id="@id/exo_progress"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_weight="1" android:layout_weight="1"
app:bar_height="2dp" 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:played_color="?attr/colorPrimary"
app:scrubber_color="?attr/colorPrimary" app:scrubber_color="?attr/colorPrimary"
@ -456,13 +488,13 @@
app:scrubber_enabled_size="24dp" app:scrubber_enabled_size="24dp"
app:unplayed_color="@color/videoProgress" /> app:unplayed_color="@color/videoProgress" />
<!-- exo_duration-->
<TextView <TextView
android:id="@id/exo_duration" android:id="@id/exo_duration"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="30dp"
android:layout_gravity="center" android:layout_gravity="center|center_vertical"
android:layout_marginEnd="20dp"
android:layout_marginEnd="30dp"
android:includeFontPadding="false" android:includeFontPadding="false"
android:minWidth="50dp" android:minWidth="50dp"
android:paddingLeft="4dp" android:paddingLeft="4dp"
@ -470,19 +502,23 @@
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="normal" android:textStyle="normal"
app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
app:layout_constraintEnd_toEndOf="@id/player_fullscreen"
tools:text="23:20" /> tools:text="23:20" />
</LinearLayout>
<ImageView <ImageView
android:id="@+id/player_fullscreen" android:id="@+id/player_fullscreen"
android:layout_width="30dp" android:layout_width="30dp"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="20dp" android:layout_marginEnd="20dp"
android:background="?android:attr/selectableItemBackgroundBorderless" android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/baseline_fullscreen_24" android:src="@drawable/baseline_fullscreen_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/white" /> app:tint="@color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>
<HorizontalScrollView <HorizontalScrollView
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -11,6 +11,7 @@
android:foreground="?attr/selectableItemBackgroundBorderless" android:foreground="?attr/selectableItemBackgroundBorderless"
app:cardCornerRadius="@dimen/rounded_image_radius" app:cardCornerRadius="@dimen/rounded_image_radius"
android:layout_margin="5dp" android:layout_margin="5dp"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1" app:layout_constraintDimensionRatio="1"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -34,6 +35,15 @@
android:background="@drawable/outline_card" android:background="@drawable/outline_card"
android:visibility="gone" /> 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 <TextView
android:id="@+id/profile_text" android:id="@+id/profile_text"
tools:text="@string/mobile_data" tools:text="@string/mobile_data"

View file

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

View file

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

View file

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

View file

@ -1,2 +1,8 @@
<?xml version="1.0" encoding="utf-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> <resources>
<string name="app_dub_sub_episode_text_format" formatted="true">%s ክፍል %d</string> <string name="app_dub_sub_episode_text_format" formatted="true">%s ክፍል %d</string>
<string name="cast_format" formatted="true">ተዋናዮች: %s</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> </resources>

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