mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge branch 'master' into feature/remote-sync
This commit is contained in:
commit
3bd7b95350
188 changed files with 4833 additions and 1439 deletions
|
@ -61,16 +61,18 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
compileSdk = 33
|
||||
compileSdk = 34
|
||||
buildToolsVersion = "34.0.0"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.lagradost.cloudstream3"
|
||||
minSdk = 21
|
||||
targetSdk = 33
|
||||
|
||||
// https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading
|
||||
targetSdk = 33 // android 14 is fucked
|
||||
|
||||
versionCode = 62
|
||||
versionName = "4.2.0"
|
||||
versionName = "4.2.1"
|
||||
|
||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
||||
|
@ -175,28 +177,30 @@ repositories {
|
|||
dependencies {
|
||||
implementation("com.google.android.mediahome:video:1.0.0")
|
||||
implementation("androidx.test.ext:junit-ktx:1.1.5")
|
||||
testImplementation("org.json:json:20180813")
|
||||
testImplementation("org.json:json:20230618")
|
||||
|
||||
implementation("androidx.core:core-ktx:1.10.1")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1") // need target 32 for 1.5.0
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
|
||||
// dont change this to 1.6.0 it looks ugly af
|
||||
implementation("com.google.android.material:material:1.5.0")
|
||||
implementation("com.google.android.material:material:1.10.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:2.6.0")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:2.6.0")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
|
||||
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:2.7.4")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:2.7.4")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
|
||||
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
androidTestImplementation("androidx.test:core")
|
||||
|
||||
//implementation("io.karn:khttp-android:0.1.2") //okhttp instead
|
||||
// implementation("io.karn:khttp-android:0.1.2") //okhttp instead
|
||||
// implementation("org.jsoup:jsoup:1.13.1")
|
||||
// DONT UPDATE, WILL CRASH ANDROID TV ????
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
|
||||
|
||||
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||
|
||||
implementation("com.github.bumptech.glide:glide:4.13.1")
|
||||
kapt("com.github.bumptech.glide:compiler:4.13.1")
|
||||
|
@ -220,28 +224,26 @@ dependencies {
|
|||
// Custom ffmpeg extension for audio codecs
|
||||
implementation("com.github.recloudstream:media-ffmpeg:1.1.0")
|
||||
|
||||
//implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
|
||||
|
||||
// Bug reports
|
||||
implementation("ch.acra:acra-core:5.11.0")
|
||||
implementation("ch.acra:acra-toast:5.11.0")
|
||||
implementation("ch.acra:acra-core:5.11.2")
|
||||
implementation("ch.acra:acra-toast:5.11.2")
|
||||
|
||||
compileOnly("com.google.auto.service:auto-service-annotations:1.0")
|
||||
compileOnly("com.google.auto.service:auto-service-annotations:1.1.1")
|
||||
//either for java sources:
|
||||
annotationProcessor("com.google.auto.service:auto-service:1.0")
|
||||
annotationProcessor("com.google.auto.service:auto-service:1.1.1")
|
||||
//or for kotlin sources (requires kapt gradle plugin):
|
||||
kapt("com.google.auto.service:auto-service:1.0")
|
||||
kapt("com.google.auto.service:auto-service:1.1.1")
|
||||
|
||||
// subtitle color picker
|
||||
implementation("com.jaredrummler:colorpicker:1.1.0")
|
||||
|
||||
//run JS
|
||||
// run JS
|
||||
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
|
||||
// available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
|
||||
implementation("org.mozilla:rhino:1.7.13")
|
||||
|
||||
// TorrentStream
|
||||
//implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
|
||||
// implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
|
||||
|
||||
// Downloading
|
||||
implementation("androidx.work:work-runtime:2.8.1")
|
||||
|
@ -250,18 +252,18 @@ dependencies {
|
|||
// Networking
|
||||
// implementation("com.squareup.okhttp3:okhttp:4.9.2")
|
||||
// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1")
|
||||
implementation("com.github.Blatzar:NiceHttp:0.4.3")
|
||||
implementation("com.github.Blatzar:NiceHttp:0.4.4") // http library
|
||||
// To fix SSL fuckery on android 9
|
||||
implementation("org.conscrypt:conscrypt-android:2.2.1")
|
||||
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
||||
// Util to skip the URI file fuckery 🙏
|
||||
implementation("com.github.LagradOst:SafeFile:0.0.5")
|
||||
|
||||
// API because cba maintaining it myself
|
||||
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
|
||||
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.10.0")
|
||||
|
||||
implementation("com.github.discord:OverlappingPanels:0.1.5")
|
||||
// debugImplementation because LeakCanary should only run in debug builds.
|
||||
//debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
|
||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
|
||||
|
||||
// for shimmer when loading
|
||||
implementation("com.facebook.shimmer:shimmer:0.5.0")
|
||||
|
@ -273,7 +275,7 @@ dependencies {
|
|||
|
||||
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipeExtractor/commits/dev
|
||||
// this should be updated frequently to avoid trailer fu*kery
|
||||
implementation("com.github.teamnewpipe:NewPipeExtractor:1f08d28")
|
||||
implementation("com.github.teamnewpipe:NewPipeExtractor:917554a")
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
|
||||
|
||||
// Library/extensions searching with Levenshtein distance
|
||||
|
@ -298,6 +300,8 @@ dependencies {
|
|||
group = "org.apache.httpcomponents",
|
||||
)
|
||||
}
|
||||
// seekbar https://github.com/rubensousa/PreviewSeekBar
|
||||
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0")
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||
import androidx.viewbinding.ViewBinding
|
||||
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
|
||||
import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding
|
||||
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
|
||||
import com.lagradost.cloudstream3.databinding.FragmentLibraryTvBinding
|
||||
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
||||
import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding
|
||||
import com.lagradost.cloudstream3.databinding.FragmentResultBinding
|
||||
|
@ -17,6 +19,7 @@ import com.lagradost.cloudstream3.databinding.FragmentSearchBinding
|
|||
import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding
|
||||
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
|
||||
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
|
||||
import com.lagradost.cloudstream3.databinding.HomepageParentEmulatorBinding
|
||||
import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding
|
||||
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
|
||||
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutTvBinding
|
||||
|
@ -117,9 +120,12 @@ class ExampleInstrumentedTest {
|
|||
// testAllLayouts<HomeScrollViewBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
|
||||
// testAllLayouts<HomeScrollViewTvBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
|
||||
|
||||
testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
|
||||
testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
|
||||
testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
|
||||
testAllLayouts<HomepageParentEmulatorBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
|
||||
testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
|
||||
|
||||
testAllLayouts<FragmentLibraryTvBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
|
||||
testAllLayouts<FragmentLibraryBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!-- Required for getting arbitrary Aniyomi packages -->
|
||||
<!-- Fixes android tv fuckery -->
|
||||
<uses-feature
|
||||
|
@ -61,7 +63,9 @@
|
|||
android:exported="true"
|
||||
android:resizeableActivity="true"
|
||||
android:screenOrientation="userLandscape"
|
||||
android:supportsPictureInPicture="true">
|
||||
android:supportsPictureInPicture="true"
|
||||
android:taskAffinity="com.lagradost.cloudstream3.downloadedplayer"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
|
@ -92,12 +96,6 @@
|
|||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsPictureInPicture="true">
|
||||
<intent-filter android:exported="true">
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- cloudstreamplayer://encodedUrl?name=Dune -->
|
||||
<intent-filter>
|
||||
|
@ -172,6 +170,22 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.account.AccountSelectActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||
android:exported="true"
|
||||
android:resizeableActivity="true">
|
||||
<intent-filter android:exported="true">
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.EasterEggMonke"
|
||||
android:exported="true" />
|
||||
|
@ -186,6 +200,7 @@
|
|||
</receiver>
|
||||
|
||||
<service
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:name=".services.VideoDownloadService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
@ -195,6 +210,7 @@
|
|||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:name=".utils.PackageInstallerService"
|
||||
android:exported="false" />
|
||||
|
||||
|
|
|
@ -65,6 +65,11 @@ object CommonActivity {
|
|||
_activity = WeakReference(value)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun setActivityInstance(newActivity: Activity?) {
|
||||
activity = newActivity
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun Activity?.getCastSession(): CastSession? {
|
||||
return (this as MainActivity?)?.mSessionManager?.currentCastSession
|
||||
|
@ -203,23 +208,25 @@ object CommonActivity {
|
|||
setLocale(this, localeCode)
|
||||
}
|
||||
|
||||
fun init(act: ComponentActivity?) {
|
||||
if (act == null) return
|
||||
activity = act
|
||||
fun init(act: Activity) {
|
||||
setActivityInstance(act)
|
||||
|
||||
val componentActivity = activity as? ComponentActivity ?: return
|
||||
|
||||
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
|
||||
//https://developer.android.com/guide/topics/ui/picture-in-picture
|
||||
canShowPipMode =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT
|
||||
act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
|
||||
act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
|
||||
componentActivity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
|
||||
componentActivity.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
|
||||
|
||||
act.updateLocale()
|
||||
act.updateTv()
|
||||
componentActivity.updateLocale()
|
||||
componentActivity.updateTv()
|
||||
NewPipe.init(DownloaderTestImpl.getInstance())
|
||||
|
||||
for (resumeApp in resumeApps) {
|
||||
resumeApp.launcher =
|
||||
act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
val resultCode = result.resultCode
|
||||
val data = result.data
|
||||
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
|
||||
|
@ -236,11 +243,11 @@ object CommonActivity {
|
|||
// Ask for notification permissions on Android 13
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
ContextCompat.checkSelfPermission(
|
||||
act,
|
||||
componentActivity,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
val requestPermissionLauncher = act.registerForActivityResult(
|
||||
val requestPermissionLauncher = componentActivity.registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted: Boolean ->
|
||||
Log.d(TAG, "Notification permission: $isGranted")
|
||||
|
@ -295,12 +302,15 @@ object CommonActivity {
|
|||
val currentOverlayTheme =
|
||||
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
|
||||
"Normal" -> R.style.OverlayPrimaryColorNormal
|
||||
"DandelionYellow" -> R.style.OverlayPrimaryColorDandelionYellow
|
||||
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
|
||||
"Orange" -> R.style.OverlayPrimaryColorOrange
|
||||
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
|
||||
"Maroon" -> R.style.OverlayPrimaryColorMaroon
|
||||
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
|
||||
"Grey" -> R.style.OverlayPrimaryColorGrey
|
||||
"White" -> R.style.OverlayPrimaryColorWhite
|
||||
"CoolBlue" -> R.style.OverlayPrimaryColorCoolBlue
|
||||
"Brown" -> R.style.OverlayPrimaryColorBrown
|
||||
"Purple" -> R.style.OverlayPrimaryColorPurple
|
||||
"Green" -> R.style.OverlayPrimaryColorGreen
|
||||
|
|
|
@ -1246,6 +1246,18 @@ interface LoadResponse {
|
|||
return this.syncData[aniListIdPrefix]
|
||||
}
|
||||
|
||||
fun LoadResponse.getImdbId(): String? {
|
||||
return normalSafeApiCall {
|
||||
SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Imdb)
|
||||
}
|
||||
}
|
||||
|
||||
fun LoadResponse.getTMDbId(): String? {
|
||||
return normalSafeApiCall {
|
||||
SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Tmdb)
|
||||
}
|
||||
}
|
||||
|
||||
fun LoadResponse.addMalId(id: Int?) {
|
||||
this.syncData[malIdPrefix] = (id ?: return).toString()
|
||||
this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString())
|
||||
|
@ -1453,6 +1465,15 @@ interface EpisodeResponse {
|
|||
var nextAiring: NextAiring?
|
||||
var seasonNames: List<SeasonData>?
|
||||
fun getLatestEpisodes(): Map<DubStatus, Int?>
|
||||
|
||||
/** Count all episodes in all previous seasons up until this episode to get a total count.
|
||||
* Example:
|
||||
* Season 1: 10 episodes.
|
||||
* Season 2: 6 episodes.
|
||||
*
|
||||
* getTotalEpisodeIndex(episode = 3, season = 2) -> 10 + 3 = 13
|
||||
* */
|
||||
fun getTotalEpisodeIndex(episode: Int, season: Int): Int
|
||||
}
|
||||
|
||||
@JvmName("addSeasonNamesString")
|
||||
|
@ -1532,6 +1553,12 @@ data class AnimeLoadResponse(
|
|||
.takeUnless { it == Int.MIN_VALUE }
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
override fun getTotalEpisodeIndex(episode: Int, season: Int): Int {
|
||||
return this.episodes.maxOf { (_, episodes) ->
|
||||
episodes.count { ((it.season ?: Int.MIN_VALUE) < season) && it.season != 0 }
|
||||
} + episode
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1740,6 +1767,12 @@ data class TvSeriesLoadResponse(
|
|||
.takeUnless { it == Int.MIN_VALUE }
|
||||
return mapOf(DubStatus.None to max)
|
||||
}
|
||||
|
||||
override fun getTotalEpisodeIndex(episode: Int, season: Int): Int {
|
||||
return episodes.count {
|
||||
(it.season ?: Int.MIN_VALUE) < season && it.season != 0
|
||||
} + episode
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun MainAPI.newTvSeriesLoadResponse(
|
||||
|
|
|
@ -67,6 +67,7 @@ import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
|
|||
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
|
||||
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
|
||||
import com.lagradost.cloudstream3.CommonActivity.screenHeight
|
||||
import com.lagradost.cloudstream3.CommonActivity.setActivityInstance
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.CommonActivity.updateLocale
|
||||
import com.lagradost.cloudstream3.databinding.ActivityMainBinding
|
||||
|
@ -283,6 +284,7 @@ var app = Requests(responseParser = object : ResponseParser {
|
|||
class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||
companion object {
|
||||
const val TAG = "MAINACT"
|
||||
const val ANIMATED_OUTLINE : Boolean = false
|
||||
var lastError: String? = null
|
||||
|
||||
/**
|
||||
|
@ -544,13 +546,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
navRailView.isVisible = isNavVisible && landscape
|
||||
|
||||
// Hide library on TV since it is not supported yet :(
|
||||
val isTrueTv = isTrueTvSettings()
|
||||
navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
|
||||
navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
|
||||
//val isTrueTv = isTrueTvSettings()
|
||||
//navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
|
||||
//navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
|
||||
|
||||
// Hide downloads on TV
|
||||
navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv
|
||||
navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv
|
||||
//navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv
|
||||
//navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -592,6 +594,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
override fun onResume() {
|
||||
super.onResume()
|
||||
afterPluginsLoadedEvent += ::onAllPluginsLoaded
|
||||
setActivityInstance(this)
|
||||
try {
|
||||
if (isCastApiAvailable()) {
|
||||
//mCastSession = mSessionManager.currentCastSession
|
||||
|
@ -661,7 +664,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
val isAtHome =
|
||||
navController?.currentDestination?.matchDestination(R.id.navigation_home) == true
|
||||
|
||||
if (isAtHome && isTrueTvSettings()) {
|
||||
if (isAtHome && isTvSettings()) {
|
||||
showConfirmExitDialog()
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
|
@ -1072,7 +1075,22 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private fun centerView(view : View?) {
|
||||
if(view == null) return
|
||||
try {
|
||||
Log.v(TAG, "centerView: $view")
|
||||
val r = Rect(0, 0, 0, 0)
|
||||
view.getDrawingRect(r)
|
||||
val x = r.centerX()
|
||||
val y = r.centerY()
|
||||
val dx = r.width() / 2 //screenWidth / 2
|
||||
val dy = screenHeight / 2
|
||||
val r2 = Rect(x - dx, y - dy, x + dx, y + dy)
|
||||
view.requestRectangleOnScreen(r2, false)
|
||||
// TvFocus.current =TvFocus.current.copy(y=y.toFloat())
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
app.initClient(this)
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
@ -1112,7 +1130,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
if (appVer != lastAppAutoBackup) {
|
||||
setKey("VERSION_NAME", BuildConfig.VERSION_NAME)
|
||||
normalSafeApiCall {
|
||||
backup()
|
||||
backup(this)
|
||||
}
|
||||
normalSafeApiCall {
|
||||
// Recompile oat on new version
|
||||
|
@ -1126,38 +1144,27 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
if (isTvSettings()) {
|
||||
val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false)
|
||||
setContentView(newLocalBinding.root)
|
||||
TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
|
||||
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
|
||||
// println("refocus $oldFocus -> $newFocus")
|
||||
try {
|
||||
val r = Rect(0, 0, 0, 0)
|
||||
newFocus.getDrawingRect(r)
|
||||
val x = r.centerX()
|
||||
val y = r.centerY()
|
||||
val dx = 0 //screenWidth / 2
|
||||
val dy = screenHeight / 2
|
||||
val r2 = Rect(x - dx, y - dy, x + dx, y + dy)
|
||||
newFocus.requestRectangleOnScreen(r2, false)
|
||||
// TvFocus.current =TvFocus.current.copy(y=y.toFloat())
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
TvFocus.updateFocusView(newFocus)
|
||||
/*var focus = newFocus
|
||||
|
||||
while(focus != null) {
|
||||
if(focus is ScrollingView && focus.canScrollVertically()) {
|
||||
focus.scrollBy()
|
||||
}
|
||||
when(focus.parent) {
|
||||
is View -> focus = newFocus
|
||||
else -> break
|
||||
}
|
||||
}*/
|
||||
if(isTrueTvSettings() && ANIMATED_OUTLINE) {
|
||||
TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
|
||||
newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
|
||||
TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
|
||||
}
|
||||
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
|
||||
TvFocus.updateFocusView(newFocus)
|
||||
}
|
||||
} else {
|
||||
newLocalBinding.focusOutline.isVisible = false
|
||||
}
|
||||
newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
|
||||
TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
|
||||
|
||||
if(isTrueTvSettings()) {
|
||||
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
|
||||
centerView(newFocus)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
ActivityMainBinding.bind(newLocalBinding.root) // this may crash
|
||||
} else {
|
||||
val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false)
|
||||
|
@ -1303,7 +1310,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
this@MainActivity.getString(R.string.action_add_to_bookmarks),
|
||||
showApply = false,
|
||||
{}) {
|
||||
viewModel.updateWatchStatus(WatchType.values()[it])
|
||||
viewModel.updateWatchStatus(WatchType.values()[it], this@MainActivity)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ open class Chillx : ExtractorApi() {
|
|||
override val requiresReferer = true
|
||||
|
||||
companion object {
|
||||
private const val KEY = "m4H6D9%0\$N&F6rQ&"
|
||||
private const val KEY = "eN0^>\$^#M08uFv%c"
|
||||
}
|
||||
|
||||
override suspend fun getUrl(
|
||||
|
|
|
@ -7,21 +7,12 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
|
|||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
|
||||
class SpeedoStream2 : SpeedoStream() {
|
||||
override val mainUrl = "https://speedostream.mom"
|
||||
}
|
||||
open class Minoplres : ExtractorApi() {
|
||||
|
||||
class SpeedoStream1 : SpeedoStream() {
|
||||
override val mainUrl = "https://speedostream.pm"
|
||||
}
|
||||
|
||||
open class SpeedoStream : ExtractorApi() {
|
||||
override val name = "SpeedoStream"
|
||||
override val mainUrl = "https://speedostream.bond"
|
||||
override val name = "Minoplres" // formerly SpeedoStream
|
||||
override val requiresReferer = true
|
||||
|
||||
// .bond, .pm, .mom redirect to .bond
|
||||
private val hostUrl = "https://speedostream.bond"
|
||||
override val mainUrl = "https://minoplres.xyz" // formerly speedostream.bond
|
||||
private val hostUrl = "https://minoplres.xyz"
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val sources = mutableListOf<ExtractorLink>()
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -477,6 +477,14 @@ object PluginManager {
|
|||
Log.i(TAG, "Loading plugin: $data")
|
||||
|
||||
return try {
|
||||
/* in case of android 14 then
|
||||
try {
|
||||
File(filePath).setReadOnly()
|
||||
} catch (t : Throwable) {
|
||||
Log.e(TAG, "Failed to set dex as readonly")
|
||||
logError(t)
|
||||
}*/
|
||||
|
||||
val loader = PathClassLoader(filePath, context.classLoader)
|
||||
var manifest: Plugin.Manifest
|
||||
loader.getResourceAsStream("manifest.json").use { stream ->
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -8,7 +8,9 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
|||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
|
||||
|
@ -69,29 +71,52 @@ class LocalList : SyncAPI {
|
|||
}?.distinctBy { it.first } ?: return null
|
||||
|
||||
val list = ioWork {
|
||||
watchStatusIds.groupBy {
|
||||
it.second.stringRes
|
||||
}.mapValues { group ->
|
||||
val isTrueTv = isTrueTvSettings()
|
||||
|
||||
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
|
||||
// None is not something to display
|
||||
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
||||
} + mapOf(
|
||||
R.string.favorites_list_name to emptyList()
|
||||
) + if (!isTrueTv) {
|
||||
mapOf(
|
||||
R.string.subscription_list_name to emptyList()
|
||||
)
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
val watchStatusMap = watchStatusIds.groupBy { it.second.stringRes }.mapValues { group ->
|
||||
group.value.mapNotNull {
|
||||
getBookmarkedData(it.first)?.toLibraryItem(it.first.toString())
|
||||
}
|
||||
} + mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull {
|
||||
}
|
||||
|
||||
val favoritesMap = mapOf(R.string.favorites_list_name to getAllFavorites().mapNotNull {
|
||||
it.toLibraryItem()
|
||||
})
|
||||
|
||||
// Don't show subscriptions on TV
|
||||
val result = if (isTrueTv) {
|
||||
baseMap + watchStatusMap + favoritesMap
|
||||
} else {
|
||||
val subscriptionsMap = mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull {
|
||||
it.toLibraryItem()
|
||||
})
|
||||
|
||||
baseMap + watchStatusMap + subscriptionsMap + favoritesMap
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
|
||||
// None is not something to display
|
||||
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
||||
} + mapOf(R.string.subscription_list_name to emptyList())
|
||||
|
||||
return SyncAPI.LibraryMetadata(
|
||||
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
|
||||
list.map { SyncAPI.LibraryList(txt(it.key), it.value) },
|
||||
setOf(
|
||||
ListSorting.AlphabeticalA,
|
||||
ListSorting.AlphabeticalZ,
|
||||
// ListSorting.UpdatedNew,
|
||||
// ListSorting.UpdatedOld,
|
||||
ListSorting.UpdatedNew,
|
||||
ListSorting.UpdatedOld,
|
||||
// ListSorting.RatingHigh,
|
||||
// ListSorting.RatingLow,
|
||||
)
|
||||
|
|
|
@ -203,7 +203,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
}
|
||||
|
||||
/** Read the id string to get all other ids */
|
||||
private fun readIdFromString(idString: String?): Map<SyncServices, String> {
|
||||
fun readIdFromString(idString: String?): Map<SyncServices, String> {
|
||||
return tryParseJson(idString) ?: return emptyMap()
|
||||
}
|
||||
|
||||
|
@ -376,6 +376,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
private var status: Int? = null,
|
||||
private var addEpisodes: Pair<List<MediaObject.Season>?, List<MediaObject.Season.Episode>?>? = null,
|
||||
private var removeEpisodes: Pair<List<MediaObject.Season>?, List<MediaObject.Season.Episode>?>? = null,
|
||||
// Required for knowing if the status should be overwritten
|
||||
private var onList: Boolean = false
|
||||
) {
|
||||
fun interceptor(interceptor: Interceptor) = apply { this.interceptor = interceptor }
|
||||
fun apiUrl(url: String) = apply { this.url = url }
|
||||
|
@ -387,6 +389,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
}
|
||||
|
||||
fun status(newStatus: Int?, oldStatus: Int?) = apply {
|
||||
onList = oldStatus != null
|
||||
// Only set status if its new
|
||||
if (newStatus != oldStatus) {
|
||||
this.status = newStatus
|
||||
|
@ -412,6 +415,11 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
// Do not add episodes if there is no change
|
||||
if (newEpisodes > (oldEpisodes ?: 0)) {
|
||||
this.addEpisodes = getEpisodes(allEpisodes.take(newEpisodes))
|
||||
|
||||
// Set to watching if episodes are added and there is no current status
|
||||
if (!onList) {
|
||||
status = SimklListStatusType.Watching.value
|
||||
}
|
||||
}
|
||||
if ((oldEpisodes ?: 0) > newEpisodes) {
|
||||
this.removeEpisodes = getEpisodes(allEpisodes.drop(newEpisodes))
|
||||
|
@ -431,6 +439,28 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
interceptor = interceptor
|
||||
).isSuccessful
|
||||
} else {
|
||||
val statusResponse = status?.let { setStatus ->
|
||||
val newStatus =
|
||||
SimklListStatusType.values()
|
||||
.firstOrNull { it.value == setStatus }?.originalName
|
||||
?: SimklListStatusType.Watching.originalName!!
|
||||
|
||||
app.post(
|
||||
"${this.url}/sync/add-to-list",
|
||||
json = StatusRequest(
|
||||
shows = listOf(
|
||||
StatusMediaObject(
|
||||
null,
|
||||
null,
|
||||
ids,
|
||||
newStatus,
|
||||
)
|
||||
), movies = emptyList()
|
||||
),
|
||||
interceptor = interceptor
|
||||
).isSuccessful
|
||||
} ?: true
|
||||
|
||||
val episodeRemovalResponse = removeEpisodes?.let { (seasons, episodes) ->
|
||||
app.post(
|
||||
"${this.url}/sync/history/remove",
|
||||
|
@ -472,28 +502,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
true
|
||||
}
|
||||
|
||||
val statusResponse = status?.let { setStatus ->
|
||||
val newStatus =
|
||||
SimklListStatusType.values()
|
||||
.firstOrNull { it.value == setStatus }?.originalName
|
||||
?: SimklListStatusType.Watching.originalName!!
|
||||
|
||||
app.post(
|
||||
"${this.url}/sync/add-to-list",
|
||||
json = StatusRequest(
|
||||
shows = listOf(
|
||||
StatusMediaObject(
|
||||
null,
|
||||
null,
|
||||
ids,
|
||||
newStatus,
|
||||
)
|
||||
), movies = emptyList()
|
||||
),
|
||||
interceptor = interceptor
|
||||
).isSuccessful
|
||||
} ?: true
|
||||
|
||||
statusResponse && episodeRemovalResponse && historyResponse
|
||||
}
|
||||
}
|
||||
|
@ -1051,4 +1059,4 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,6 @@ class WhoIsWatchingAdapter(
|
|||
editCallBack = editCallBack,
|
||||
)
|
||||
|
||||
|
||||
override fun onBindViewHolder(holder: WhoIsWatchingHolder, position: Int) =
|
||||
holder.bind(currentList.getOrNull(position))
|
||||
|
||||
|
@ -70,10 +69,15 @@ class WhoIsWatchingAdapter(
|
|||
fun bind(card: DataStoreHelper.Account?) {
|
||||
when (binding) {
|
||||
is WhoIsWatchingAccountBinding -> binding.apply {
|
||||
if(card == null) return@apply
|
||||
if (card == null) return@apply
|
||||
outline.isVisible = card.keyIndex == DataStoreHelper.selectedKeyIndex
|
||||
profileText.text = card.name
|
||||
profileImageBackground.setImage(card.image)
|
||||
|
||||
// Handle the lock indicator
|
||||
val isLocked = card.lockPin != null
|
||||
lockIcon.isVisible = isLocked
|
||||
|
||||
root.setOnClickListener {
|
||||
selectCallBack(card)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -27,7 +27,6 @@ import com.lagradost.cloudstream3.*
|
|||
import com.lagradost.cloudstream3.APIHolder.apis
|
||||
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterBackupRestoreEvent
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
|
||||
|
@ -55,8 +54,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.ownHide
|
|||
import com.lagradost.cloudstream3.utils.AppUtils.ownShow
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentHomePage
|
||||
import com.lagradost.cloudstream3.utils.Event
|
||||
|
@ -67,9 +64,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
|
|||
import java.util.*
|
||||
|
||||
|
||||
const val HOME_BOOKMARK_VALUE_LIST = "home_bookmarked_last_list"
|
||||
const val HOME_PREF_HOMEPAGE = "home_pref_homepage"
|
||||
|
||||
class HomeFragment : Fragment() {
|
||||
companion object {
|
||||
val configEvent = Event<Int>()
|
||||
|
@ -371,10 +365,7 @@ class HomeFragment : Fragment() {
|
|||
var currentApiName = selectedApiName
|
||||
|
||||
var currentValidApis: MutableList<MainAPI> = mutableListOf()
|
||||
val preSelectedTypes = this.getKey<List<String>>(HOME_PREF_HOMEPAGE)
|
||||
?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } }
|
||||
?.toMutableList()
|
||||
?: mutableListOf(TvType.Movie, TvType.TvSeries)
|
||||
val preSelectedTypes = DataStoreHelper.homePreference.toMutableList()
|
||||
|
||||
binding.cancelBtt.setOnClickListener {
|
||||
dialog.dismissSafe()
|
||||
|
@ -402,7 +393,7 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
|
||||
fun updateList() {
|
||||
this.setKey(HOME_PREF_HOMEPAGE, preSelectedTypes)
|
||||
DataStoreHelper.homePreference = preSelectedTypes
|
||||
|
||||
arrayAdapter.clear()
|
||||
currentValidApis = validAPIs.filter { api ->
|
||||
|
|
|
@ -15,7 +15,8 @@ import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
|||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
|
||||
|
||||
class LoadClickCallback(
|
||||
|
@ -34,11 +35,13 @@ open class ParentItemAdapter(
|
|||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
|
||||
val root = LayoutInflater.from(parent.context).inflate(
|
||||
if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
val layoutResId = when {
|
||||
isTrueTvSettings() -> R.layout.homepage_parent_tv
|
||||
parent.context.isEmulatorSettings() -> R.layout.homepage_parent_emulator
|
||||
else -> R.layout.homepage_parent
|
||||
}
|
||||
|
||||
val root = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
|
||||
|
||||
val binding = HomepageParentBinding.bind(root)
|
||||
|
||||
|
@ -234,7 +237,7 @@ open class ParentItemAdapter(
|
|||
})
|
||||
|
||||
//(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
|
||||
if (!isTvSettings()) {
|
||||
if (!isTrueTvSettings()) {
|
||||
title.setOnClickListener {
|
||||
moreInfoClickCallback.invoke(expand)
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
|||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
|
||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
|
@ -81,6 +82,28 @@ class HomeParentItemAdapterPreview(
|
|||
parent,
|
||||
false
|
||||
) else FragmentHomeHeadBinding.inflate(inflater, parent, false)
|
||||
|
||||
if (binding is FragmentHomeHeadTvBinding && parent.context.isEmulatorSettings()) {
|
||||
binding.homeBookmarkParentItemMoreInfo.isVisible = true
|
||||
|
||||
val marginInDp = 50
|
||||
val density = binding.horizontalScrollChips.context.resources.displayMetrics.density
|
||||
val marginInPixels = (marginInDp * density).toInt()
|
||||
|
||||
val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams
|
||||
params.marginEnd = marginInPixels
|
||||
binding.horizontalScrollChips.layoutParams = params
|
||||
binding.homeWatchParentItemTitle.setCompoundDrawablesWithIntrinsicBounds(
|
||||
null,
|
||||
null,
|
||||
ContextCompat.getDrawable(
|
||||
parent.context,
|
||||
R.drawable.ic_baseline_arrow_forward_24
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
HeaderViewHolder(
|
||||
binding,
|
||||
viewModel,
|
||||
|
@ -355,21 +378,25 @@ class HomeParentItemAdapterPreview(
|
|||
showApply = false,
|
||||
{}) {
|
||||
val newValue = WatchType.values()[it]
|
||||
homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds(
|
||||
null,
|
||||
ContextCompat.getDrawable(
|
||||
homePreviewBookmark.context,
|
||||
newValue.iconRes
|
||||
),
|
||||
null,
|
||||
null
|
||||
)
|
||||
homePreviewBookmark.setText(newValue.stringRes)
|
||||
|
||||
ResultViewModel2.updateWatchStatus(
|
||||
item,
|
||||
newValue
|
||||
)
|
||||
ResultViewModel2().updateWatchStatus(
|
||||
newValue,
|
||||
fab.context,
|
||||
item
|
||||
) { statusChanged: Boolean ->
|
||||
if (!statusChanged) return@updateWatchStatus
|
||||
|
||||
homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds(
|
||||
null,
|
||||
ContextCompat.getDrawable(
|
||||
homePreviewBookmark.context,
|
||||
newValue.iconRes
|
||||
),
|
||||
null,
|
||||
null
|
||||
)
|
||||
homePreviewBookmark.setText(newValue.stringRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -553,12 +580,19 @@ class HomeParentItemAdapterPreview(
|
|||
resumeHolder.isVisible = resumeWatching.isNotEmpty()
|
||||
resumeAdapter.updateList(resumeWatching)
|
||||
|
||||
if (binding is FragmentHomeHeadBinding) {
|
||||
binding.homeWatchParentItemTitle.setOnClickListener {
|
||||
if (
|
||||
binding is FragmentHomeHeadBinding ||
|
||||
binding is FragmentHomeHeadTvBinding &&
|
||||
binding.root.context.isEmulatorSettings()
|
||||
) {
|
||||
val title = (binding as? FragmentHomeHeadBinding)?.homeWatchParentItemTitle
|
||||
?: (binding as? FragmentHomeHeadTvBinding)?.homeWatchParentItemTitle
|
||||
|
||||
title?.setOnClickListener {
|
||||
viewModel.popup(
|
||||
HomeViewModel.ExpandableHomepageList(
|
||||
HomePageList(
|
||||
binding.homeWatchParentItemTitle.text.toString(),
|
||||
title.text.toString(),
|
||||
resumeWatching,
|
||||
false
|
||||
), 1, false
|
||||
|
@ -576,8 +610,15 @@ class HomeParentItemAdapterPreview(
|
|||
bookmarkHolder.isVisible = visible
|
||||
bookmarkAdapter.updateList(list)
|
||||
|
||||
if (binding is FragmentHomeHeadBinding) {
|
||||
binding.homeBookmarkParentItemTitle.setOnClickListener {
|
||||
if (
|
||||
binding is FragmentHomeHeadBinding ||
|
||||
binding is FragmentHomeHeadTvBinding &&
|
||||
binding.root.context.isEmulatorSettings()
|
||||
) {
|
||||
val title = (binding as? FragmentHomeHeadBinding)?.homeBookmarkParentItemTitle
|
||||
?: (binding as? FragmentHomeHeadTvBinding)?.homeBookmarkParentItemTitle
|
||||
|
||||
title?.setOnClickListener {
|
||||
val items = toggleList.map { it.first }.filter { it.isChecked }
|
||||
if (items.isEmpty()) return@setOnClickListener // we don't want to show an empty dialog
|
||||
val textSum = items
|
||||
|
|
|
@ -12,7 +12,6 @@ import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality
|
|||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.CommonActivity.activity
|
||||
import com.lagradost.cloudstream3.HomePageList
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
|
@ -41,7 +40,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
|||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllBookmarkedData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllResumeStateIds
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
|
||||
|
@ -103,11 +101,6 @@ class HomeViewModel : ViewModel() {
|
|||
loadStoredData()
|
||||
}
|
||||
|
||||
fun deleteBookmarks() {
|
||||
deleteAllBookmarkedData()
|
||||
loadStoredData()
|
||||
}
|
||||
|
||||
var repo: APIRepository? = null
|
||||
|
||||
private val _apiName = MutableLiveData<String>()
|
||||
|
@ -170,10 +163,7 @@ class HomeViewModel : ViewModel() {
|
|||
currentWatchTypes.remove(WatchType.NONE)
|
||||
|
||||
if (currentWatchTypes.size <= 0) {
|
||||
setKey(
|
||||
HOME_BOOKMARK_VALUE_LIST,
|
||||
intArrayOf()
|
||||
)
|
||||
DataStoreHelper.homeBookmarkedList = intArrayOf()
|
||||
_availableWatchStatusTypes.postValue(setOf<WatchType>() to setOf())
|
||||
_bookmarks.postValue(Pair(false, ArrayList()))
|
||||
return@launchSafe
|
||||
|
@ -181,16 +171,14 @@ class HomeViewModel : ViewModel() {
|
|||
|
||||
val watchPrefNotNull = preferredWatchStatus ?: EnumSet.of(currentWatchTypes.first())
|
||||
//if (currentWatchTypes.any { watchPrefNotNull.contains(it) }) watchPrefNotNull else listOf(currentWatchTypes.first())
|
||||
setKey(
|
||||
HOME_BOOKMARK_VALUE_LIST,
|
||||
watchPrefNotNull.map { it.internalId }.toIntArray()
|
||||
)
|
||||
|
||||
DataStoreHelper.homeBookmarkedList = watchPrefNotNull.map { it.internalId }.toIntArray()
|
||||
_availableWatchStatusTypes.postValue(
|
||||
Pair(
|
||||
watchPrefNotNull,
|
||||
currentWatchTypes,
|
||||
|
||||
watchPrefNotNull to
|
||||
currentWatchTypes,
|
||||
|
||||
)
|
||||
)
|
||||
|
||||
val list = withContext(Dispatchers.IO) {
|
||||
watchStatusIds.filter { watchPrefNotNull.contains(it.second) }
|
||||
|
@ -463,7 +451,7 @@ class HomeViewModel : ViewModel() {
|
|||
|
||||
fun loadStoredData() {
|
||||
val list = EnumSet.noneOf(WatchType::class.java)
|
||||
getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let {
|
||||
DataStoreHelper.homeBookmarkedList.map { WatchType.fromInternalId(it) }.let {
|
||||
list.addAll(it)
|
||||
}
|
||||
loadStoredData(list)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.lagradost.cloudstream3.ui.library
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
|
@ -8,14 +9,25 @@ import android.os.Handler
|
|||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.FOCUS_AFTER_DESCENDANTS
|
||||
import android.view.ViewGroup.FOCUS_BLOCK_DESCENDANTS
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.allViews
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.lagradost.cloudstream3.APIHolder
|
||||
import com.lagradost.cloudstream3.APIHolder.allProviders
|
||||
|
@ -23,21 +35,26 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.CommonActivity
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.debugAssert
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.syncproviders.BackupAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
|
||||
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
||||
|
@ -48,7 +65,7 @@ const val LIBRARY_FOLDER = "library_folder"
|
|||
|
||||
|
||||
enum class LibraryOpenerType(@StringRes val stringRes: Int) {
|
||||
Default(R.string.default_subtitles), // TODO FIX AFTER MERGE
|
||||
Default(R.string.action_default),
|
||||
Provider(R.string.none),
|
||||
Browser(R.string.browser),
|
||||
Search(R.string.search),
|
||||
|
@ -83,9 +100,21 @@ class LibraryFragment : Fragment() {
|
|||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||
): View {
|
||||
MainActivity.afterBackupRestoreEvent += ::onNewSyncData
|
||||
val localBinding = FragmentLibraryBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root
|
||||
val layout =
|
||||
if (SettingsFragment.isTvSettings()) R.layout.fragment_library_tv else R.layout.fragment_library
|
||||
val root = inflater.inflate(layout, container, false)
|
||||
binding = try {
|
||||
FragmentLibraryBinding.bind(root)
|
||||
} catch (t: Throwable) {
|
||||
CommonActivity.showToast(
|
||||
txt(R.string.unable_to_inflate, t.message ?: ""),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
logError(t)
|
||||
null
|
||||
}
|
||||
|
||||
return root
|
||||
|
||||
//return inflater.inflate(R.layout.fragment_library, container, false)
|
||||
}
|
||||
|
@ -103,26 +132,25 @@ class LibraryFragment : Fragment() {
|
|||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType", "CutPasteId")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
fixPaddingStatusbar(binding?.searchStatusBarPadding)
|
||||
|
||||
binding?.sortFab?.setOnClickListener {
|
||||
val methods = libraryViewModel.sortingMethods.map {
|
||||
txt(it.stringRes).asString(view.context)
|
||||
}
|
||||
binding?.sortFab?.setOnClickListener(sortChangeClickListener)
|
||||
binding?.librarySort?.setOnClickListener(sortChangeClickListener)
|
||||
|
||||
activity?.showBottomDialog(methods,
|
||||
libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod),
|
||||
txt(R.string.sort_by).asString(view.context),
|
||||
false,
|
||||
{},
|
||||
{
|
||||
val method = libraryViewModel.sortingMethods[it]
|
||||
libraryViewModel.sort(method)
|
||||
})
|
||||
binding?.libraryRoot?.findViewById<TextView>(R.id.search_src_text)?.apply {
|
||||
tag = "tv_no_focus_tag"
|
||||
}
|
||||
|
||||
// Set the color for the search exit icon to the correct theme text color
|
||||
val searchExitIcon = binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
|
||||
val searchExitIconColor = TypedValue()
|
||||
|
||||
activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true)
|
||||
searchExitIcon?.setColorFilter(searchExitIconColor.data)
|
||||
|
||||
binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
libraryViewModel.sort(ListSorting.Query, query)
|
||||
|
@ -186,7 +214,7 @@ class LibraryFragment : Fragment() {
|
|||
|
||||
val items = baseOptions.map { txt(it.stringRes).asString(this) } + availableProviders
|
||||
|
||||
val savedSelection = getKey<LibraryOpener>(LIBRARY_FOLDER, key)
|
||||
val savedSelection = getKey<LibraryOpener>("$currentAccount/$LIBRARY_FOLDER", key)
|
||||
val selectedIndex =
|
||||
when {
|
||||
savedSelection == null -> 0
|
||||
|
@ -221,7 +249,7 @@ class LibraryFragment : Fragment() {
|
|||
}
|
||||
|
||||
setKey(
|
||||
LIBRARY_FOLDER,
|
||||
"$currentAccount/$LIBRARY_FOLDER",
|
||||
key,
|
||||
savedData,
|
||||
)
|
||||
|
@ -234,6 +262,7 @@ class LibraryFragment : Fragment() {
|
|||
}
|
||||
|
||||
binding?.viewpager?.setPageTransformer(LibraryScrollTransformer())
|
||||
|
||||
binding?.viewpager?.adapter =
|
||||
binding?.viewpager?.adapter ?: ViewpagerAdapter(
|
||||
mutableListOf(),
|
||||
|
@ -268,8 +297,11 @@ class LibraryFragment : Fragment() {
|
|||
// This basically first selects the individual opener and if that is default then
|
||||
// selects the whole list opener
|
||||
val savedListSelection =
|
||||
getKey<LibraryOpener>(LIBRARY_FOLDER, syncName.name)
|
||||
val savedSelection = getKey<LibraryOpener>(LIBRARY_FOLDER, syncId).takeIf {
|
||||
getKey<LibraryOpener>("$currentAccount/$LIBRARY_FOLDER", syncName.name)
|
||||
val savedSelection = getKey<LibraryOpener>(
|
||||
"$currentAccount/$LIBRARY_FOLDER",
|
||||
syncId
|
||||
).takeIf {
|
||||
it?.openType != LibraryOpenerType.Default
|
||||
} ?: savedListSelection
|
||||
|
||||
|
@ -357,11 +389,18 @@ class LibraryFragment : Fragment() {
|
|||
}
|
||||
|
||||
(viewpager.adapter as? ViewpagerAdapter)?.pages = pages
|
||||
//fix focus on the viewpager itself
|
||||
(viewpager.getChildAt(0) as RecyclerView).apply {
|
||||
tag = "tv_no_focus_tag"
|
||||
//isFocusable = false
|
||||
}
|
||||
|
||||
// Using notifyItemRangeChanged keeps the animations when sorting
|
||||
viewpager.adapter?.notifyItemRangeChanged(
|
||||
0,
|
||||
viewpager.adapter?.itemCount ?: 0
|
||||
)
|
||||
binding?.viewpager?.setCurrentItem(libraryViewModel.currentPage, false)
|
||||
|
||||
// Only stop loading after 300ms to hide the fade effect the viewpager produces when updating
|
||||
// Without this there would be a flashing effect:
|
||||
|
@ -398,6 +437,9 @@ class LibraryFragment : Fragment() {
|
|||
viewpager,
|
||||
) { tab, position ->
|
||||
tab.text = pages.getOrNull(position)?.title?.asStringNull(context)
|
||||
tab.view.tag = "tv_no_focus_tag"
|
||||
tab.view.nextFocusDownId = R.id.search_result_root
|
||||
|
||||
tab.view.setOnClickListener {
|
||||
val currentItem =
|
||||
binding?.viewpager?.currentItem ?: return@setOnClickListener
|
||||
|
@ -420,8 +462,25 @@ class LibraryFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
binding?.viewpager?.registerOnPageChangeCallback(object :
|
||||
ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
val all = binding?.viewpager?.allViews?.toList()
|
||||
?.filterIsInstance<AutofitRecyclerView>()
|
||||
|
||||
all?.forEach { view ->
|
||||
view.isVisible = view.tag == position
|
||||
view.isFocusable = view.tag == position
|
||||
|
||||
if (view.tag == position)
|
||||
view.descendantFocusability = FOCUS_AFTER_DESCENDANTS
|
||||
else
|
||||
view.descendantFocusability = FOCUS_BLOCK_DESCENDANTS
|
||||
}
|
||||
super.onPageSelected(position)
|
||||
}
|
||||
})
|
||||
}
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
(binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind()
|
||||
super.onConfigurationChanged(newConfig)
|
||||
|
@ -440,6 +499,21 @@ class LibraryFragment : Fragment() {
|
|||
private fun onNewSyncData(unused: Unit) {
|
||||
Log.d(BackupAPI.LOG_KEY, "will reload pages")
|
||||
libraryViewModel.reloadPages(true)
|
||||
|
||||
private val sortChangeClickListener = View.OnClickListener { view ->
|
||||
val methods = libraryViewModel.sortingMethods.map {
|
||||
txt(it.stringRes).asString(view.context)
|
||||
}
|
||||
|
||||
activity?.showBottomDialog(methods,
|
||||
libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod),
|
||||
txt(R.string.sort_by).asString(view.context),
|
||||
false,
|
||||
{},
|
||||
{
|
||||
val method = libraryViewModel.sortingMethods[it]
|
||||
libraryViewModel.sort(method)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,11 +6,14 @@ import androidx.lifecycle.MutableLiveData
|
|||
import androidx.lifecycle.ViewModel
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
|
||||
|
||||
enum class ListSorting(@StringRes val stringRes: Int) {
|
||||
Query(R.string.none),
|
||||
|
@ -28,6 +31,8 @@ class LibraryViewModel : ViewModel() {
|
|||
private val _pages: MutableLiveData<Resource<List<SyncAPI.Page>>> = MutableLiveData(null)
|
||||
val pages: LiveData<Resource<List<SyncAPI.Page>>> = _pages
|
||||
|
||||
var currentPage: Int = 0
|
||||
|
||||
private val _currentApiName: MutableLiveData<String> = MutableLiveData("")
|
||||
val currentApiName: LiveData<String> = _currentApiName
|
||||
|
||||
|
@ -35,12 +40,12 @@ class LibraryViewModel : ViewModel() {
|
|||
get() = SyncApis.filter { it.hasAccount() }
|
||||
|
||||
var currentSyncApi = availableSyncApis.let { allApis ->
|
||||
val lastSelection = getKey<String>(LAST_SYNC_API_KEY)
|
||||
val lastSelection = getKey<String>("$currentAccount/$LAST_SYNC_API_KEY")
|
||||
availableSyncApis.firstOrNull { it.name == lastSelection } ?: allApis.firstOrNull()
|
||||
}
|
||||
private set(value) {
|
||||
field = value
|
||||
setKey(LAST_SYNC_API_KEY, field?.name)
|
||||
setKey("$currentAccount/$LAST_SYNC_API_KEY", field?.name)
|
||||
}
|
||||
|
||||
val availableApiNames: List<String>
|
||||
|
@ -58,13 +63,21 @@ class LibraryViewModel : ViewModel() {
|
|||
reloadPages(true)
|
||||
}
|
||||
|
||||
fun sort(method: ListSorting, query: String? = null) {
|
||||
val currentList = pages.value ?: return
|
||||
fun sort(method: ListSorting, query: String? = null) = ioSafe {
|
||||
val value = _pages.value ?: return@ioSafe
|
||||
if (value is Resource.Success) {
|
||||
sort(method, query, value.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sort(method: ListSorting, query: String? = null, items: List<SyncAPI.Page>) {
|
||||
currentSortingMethod = method
|
||||
(currentList as? Resource.Success)?.value?.forEachIndexed { _, page ->
|
||||
DataStoreHelper.librarySortingMode = method.ordinal
|
||||
|
||||
items.forEach { page ->
|
||||
page.sort(method, query)
|
||||
}
|
||||
_pages.postValue(currentList)
|
||||
_pages.postValue(Resource.Success(items))
|
||||
}
|
||||
|
||||
fun reloadPages(forceReload: Boolean) {
|
||||
|
@ -85,8 +98,6 @@ class LibraryViewModel : ViewModel() {
|
|||
val library = (libraryResource as? Resource.Success)?.value ?: return@let
|
||||
|
||||
sortingMethods = library.supportedListSorting.toList()
|
||||
currentSortingMethod = null
|
||||
|
||||
repo.requireLibraryRefresh = false
|
||||
|
||||
val pages = library.allLibraryLists.map {
|
||||
|
@ -96,8 +107,24 @@ class LibraryViewModel : ViewModel() {
|
|||
)
|
||||
}
|
||||
|
||||
_pages.postValue(Resource.Success(pages))
|
||||
val desiredSortingMethod =
|
||||
ListSorting.values().getOrNull(DataStoreHelper.librarySortingMode)
|
||||
if (desiredSortingMethod != null && library.supportedListSorting.contains(desiredSortingMethod)) {
|
||||
sort(desiredSortingMethod, null, pages)
|
||||
} else {
|
||||
// null query = no sorting
|
||||
sort(ListSorting.Query, null, pages)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
MainActivity.reloadHomeEvent += ::reloadPages
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
MainActivity.reloadHomeEvent -= ::reloadPages
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ class ViewpagerAdapter(
|
|||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is PageViewHolder -> {
|
||||
holder.bind(pages[position], unbound.remove(position))
|
||||
holder.bind(pages[position], position, unbound.remove(position))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,8 @@ class ViewpagerAdapter(
|
|||
|
||||
inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(page: SyncAPI.Page, rebind: Boolean) {
|
||||
fun bind(page: SyncAPI.Page, position: Int, rebind: Boolean) {
|
||||
binding.pageRecyclerview.tag = position
|
||||
binding.pageRecyclerview.apply {
|
||||
spanCount =
|
||||
this@PageViewHolder.itemView.context.getSpanCount() ?: 3
|
||||
|
|
|
@ -18,10 +18,9 @@ import android.widget.ProgressBar
|
|||
import android.widget.Toast
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaSession
|
||||
|
@ -30,12 +29,15 @@ import androidx.media3.ui.DefaultTimeBar
|
|||
import androidx.media3.ui.PlayerView
|
||||
import androidx.media3.ui.SubtitleView
|
||||
import androidx.media3.ui.TimeBar
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.github.rubensousa.previewseekbar.PreviewBar
|
||||
import com.github.rubensousa.previewseekbar.media3.PreviewTimeBar
|
||||
import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode
|
||||
import com.lagradost.cloudstream3.CommonActivity.isInPIPMode
|
||||
import com.lagradost.cloudstream3.CommonActivity.keyEventListener
|
||||
import com.lagradost.cloudstream3.CommonActivity.playerEventListener
|
||||
import com.lagradost.cloudstream3.CommonActivity.screenWidth
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
|
@ -45,6 +47,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
|||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
||||
import com.lagradost.cloudstream3.utils.UIHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
||||
|
@ -77,12 +80,12 @@ abstract class AbstractPlayerFragment(
|
|||
var isBuffering = true
|
||||
protected open var hasPipModeSupport = true
|
||||
|
||||
var playerPausePlayHolderHolder : FrameLayout? = null
|
||||
var playerPausePlay : ImageView? = null
|
||||
var playerBuffering : ProgressBar? = null
|
||||
var playerView : PlayerView? = null
|
||||
var piphide : FrameLayout? = null
|
||||
var subtitleHolder : FrameLayout? = null
|
||||
var playerPausePlayHolderHolder: FrameLayout? = null
|
||||
var playerPausePlay: ImageView? = null
|
||||
var playerBuffering: ProgressBar? = null
|
||||
var playerView: PlayerView? = null
|
||||
var piphide: FrameLayout? = null
|
||||
var subtitleHolder: FrameLayout? = null
|
||||
|
||||
@LayoutRes
|
||||
protected open var layout: Int = R.layout.fragment_player
|
||||
|
@ -95,11 +98,11 @@ abstract class AbstractPlayerFragment(
|
|||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
open fun playerPositionChanged(position: Long, duration : Long) {
|
||||
open fun playerPositionChanged(position: Long, duration: Long) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
open fun playerDimensionsLoaded(width: Int, height : Int) {
|
||||
open fun playerDimensionsLoaded(width: Int, height: Int) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
|
@ -135,8 +138,10 @@ abstract class AbstractPlayerFragment(
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateIsPlaying(wasPlaying : CSPlayerLoading,
|
||||
isPlaying : CSPlayerLoading) {
|
||||
private fun updateIsPlaying(
|
||||
wasPlaying: CSPlayerLoading,
|
||||
isPlaying: CSPlayerLoading
|
||||
) {
|
||||
val isPlayingRightNow = CSPlayerLoading.IsPlaying == isPlaying
|
||||
val isPausedRightNow = CSPlayerLoading.IsPaused == isPlaying
|
||||
|
||||
|
@ -184,7 +189,11 @@ abstract class AbstractPlayerFragment(
|
|||
canEnterPipMode = isPlayingRightNow && hasPipModeSupport
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
activity?.let { act ->
|
||||
PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow, player.getAspectRatio())
|
||||
PlayerPipHelper.updatePIPModeActions(
|
||||
act,
|
||||
isPlayingRightNow,
|
||||
player.getAspectRatio()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -373,49 +382,61 @@ abstract class AbstractPlayerFragment(
|
|||
/** This receives the events from the player, if you want to append functionality you do it here,
|
||||
* do note that this only receives events for UI changes,
|
||||
* and returning early WONT stop it from changing in eg the player time or pause status */
|
||||
open fun mainCallback(event : PlayerEvent) {
|
||||
open fun mainCallback(event: PlayerEvent) {
|
||||
Log.i(TAG, "Handle event: $event")
|
||||
when(event) {
|
||||
when (event) {
|
||||
is ResizedEvent -> {
|
||||
playerDimensionsLoaded(event.width, event.height)
|
||||
}
|
||||
|
||||
is PlayerAttachedEvent -> {
|
||||
playerUpdated(event.player)
|
||||
}
|
||||
|
||||
is SubtitlesUpdatedEvent -> {
|
||||
subtitlesChanged()
|
||||
}
|
||||
|
||||
is TimestampSkippedEvent -> {
|
||||
onTimestampSkipped(event.timestamp)
|
||||
}
|
||||
|
||||
is TimestampInvokedEvent -> {
|
||||
onTimestamp(event.timestamp)
|
||||
}
|
||||
|
||||
is TracksChangedEvent -> {
|
||||
onTracksInfoChanged()
|
||||
}
|
||||
|
||||
is EmbeddedSubtitlesFetchedEvent -> {
|
||||
embeddedSubtitlesFetched(event.tracks)
|
||||
}
|
||||
|
||||
is ErrorEvent -> {
|
||||
playerError(event.error)
|
||||
}
|
||||
|
||||
is RequestAudioFocusEvent -> {
|
||||
requestAudioFocus()
|
||||
}
|
||||
|
||||
is EpisodeSeekEvent -> {
|
||||
when(event.offset) {
|
||||
when (event.offset) {
|
||||
-1 -> prevEpisode()
|
||||
1 -> nextEpisode()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
is StatusEvent -> {
|
||||
updateIsPlaying(wasPlaying = event.wasPlaying, isPlaying = event.isPlaying)
|
||||
}
|
||||
|
||||
is PositionEvent -> {
|
||||
playerPositionChanged(position = event.toMs, duration = event.durationMs)
|
||||
}
|
||||
|
||||
is VideoEndedEvent -> {
|
||||
context?.let { ctx ->
|
||||
// Only play next episode if autoplay is on (default)
|
||||
|
@ -432,6 +453,7 @@ abstract class AbstractPlayerFragment(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
is PauseEvent -> Unit
|
||||
is PlayEvent -> Unit
|
||||
}
|
||||
|
@ -439,7 +461,7 @@ abstract class AbstractPlayerFragment(
|
|||
|
||||
@SuppressLint("SetTextI18n", "UnsafeOptInUsageError")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
resizeMode = getKey(RESIZE_MODE_KEY) ?: 0
|
||||
resizeMode = DataStoreHelper.resizeMode
|
||||
resize(resizeMode, false)
|
||||
|
||||
player.releaseCallbacks()
|
||||
|
@ -454,22 +476,73 @@ abstract class AbstractPlayerFragment(
|
|||
)
|
||||
|
||||
if (player is CS3IPlayer) {
|
||||
// preview bar
|
||||
val progressBar: PreviewTimeBar? = playerView?.findViewById(R.id.exo_progress)
|
||||
val previewImageView: ImageView? = playerView?.findViewById(R.id.previewImageView)
|
||||
val previewFrameLayout: FrameLayout? = playerView?.findViewById(R.id.previewFrameLayout)
|
||||
if (progressBar != null && previewImageView != null && previewFrameLayout != null) {
|
||||
var resume = false
|
||||
progressBar.addOnScrubListener(object : PreviewBar.OnScrubListener {
|
||||
override fun onScrubStart(previewBar: PreviewBar?) {
|
||||
val hasPreview = player.hasPreview()
|
||||
progressBar.isPreviewEnabled = hasPreview
|
||||
resume = player.getIsPlaying()
|
||||
if (resume) player.handleEvent(
|
||||
CSPlayerEvent.Pause,
|
||||
PlayerEventSource.Player
|
||||
)
|
||||
}
|
||||
|
||||
override fun onScrubMove(
|
||||
previewBar: PreviewBar?,
|
||||
progress: Int,
|
||||
fromUser: Boolean
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onScrubStop(previewBar: PreviewBar?) {
|
||||
if (resume) player.handleEvent(CSPlayerEvent.Play, PlayerEventSource.Player)
|
||||
}
|
||||
})
|
||||
progressBar.attachPreviewView(previewFrameLayout)
|
||||
progressBar.setPreviewLoader { currentPosition, max ->
|
||||
val bitmap = player.getPreview(currentPosition.toFloat().div(max.toFloat()))
|
||||
previewImageView.isGone = bitmap == null
|
||||
previewImageView.setImageBitmap(bitmap)
|
||||
}
|
||||
}
|
||||
|
||||
subView = playerView?.findViewById(R.id.exo_subtitles)
|
||||
subStyle = SubtitlesFragment.getCurrentSavedStyle()
|
||||
player.initSubtitles(subView, subtitleHolder, subStyle)
|
||||
(player.imageGenerator as? PreviewGenerator)?.params = ImageParams.new16by9(screenWidth)
|
||||
|
||||
/*previewImageView?.doOnLayout {
|
||||
(player.imageGenerator as? PreviewGenerator)?.params = ImageParams(
|
||||
it.measuredWidth,
|
||||
it.measuredHeight
|
||||
)
|
||||
}*/
|
||||
/** this might seam a bit fucky and that is because it is, the seek event is captured twice, once by the player
|
||||
* and once by the UI even if it should only be registered once by the UI */
|
||||
playerView?.findViewById<DefaultTimeBar>(R.id.exo_progress)?.addListener(object : TimeBar.OnScrubListener {
|
||||
override fun onScrubStart(timeBar: TimeBar, position: Long) = Unit
|
||||
override fun onScrubMove(timeBar: TimeBar, position: Long) = Unit
|
||||
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
|
||||
if (canceled) return
|
||||
val playerDuration = player.getDuration() ?: return
|
||||
val playerPosition = player.getPosition() ?: return
|
||||
mainCallback(PositionEvent(source = PlayerEventSource.UI, durationMs = playerDuration, fromMs = playerPosition, toMs = position))
|
||||
}
|
||||
})
|
||||
playerView?.findViewById<DefaultTimeBar>(R.id.exo_progress)
|
||||
?.addListener(object : TimeBar.OnScrubListener {
|
||||
override fun onScrubStart(timeBar: TimeBar, position: Long) = Unit
|
||||
override fun onScrubMove(timeBar: TimeBar, position: Long) = Unit
|
||||
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
|
||||
if (canceled) return
|
||||
val playerDuration = player.getDuration() ?: return
|
||||
val playerPosition = player.getPosition() ?: return
|
||||
mainCallback(
|
||||
PositionEvent(
|
||||
source = PlayerEventSource.UI,
|
||||
durationMs = playerDuration,
|
||||
fromMs = playerPosition,
|
||||
toMs = position
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged
|
||||
|
||||
|
@ -535,7 +608,7 @@ abstract class AbstractPlayerFragment(
|
|||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
fun resize(resize: PlayerResize, showToast: Boolean) {
|
||||
setKey(RESIZE_MODE_KEY, resize.ordinal)
|
||||
DataStoreHelper.resizeMode = resize.ordinal
|
||||
val type = when (resize) {
|
||||
PlayerResize.Fill -> AspectRatioFrameLayout.RESIZE_MODE_FILL
|
||||
PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
@ -56,6 +57,7 @@ import com.lagradost.cloudstream3.mvvm.logError
|
|||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
|
||||
import com.lagradost.cloudstream3.utils.DrmExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
|
@ -88,7 +90,9 @@ class CS3IPlayer : IPlayer {
|
|||
private var exoPlayer: ExoPlayer? = null
|
||||
set(value) {
|
||||
// If the old value is not null then the player has not been properly released.
|
||||
debugAssert({ field != null && value != null }, { "Previous player instance should be released!" })
|
||||
debugAssert(
|
||||
{ field != null && value != null },
|
||||
{ "Previous player instance should be released!" })
|
||||
field = value
|
||||
}
|
||||
|
||||
|
@ -96,6 +100,8 @@ class CS3IPlayer : IPlayer {
|
|||
var simpleCacheSize = 0L
|
||||
var videoBufferMs = 0L
|
||||
|
||||
val imageGenerator = IPreviewGenerator.new()
|
||||
|
||||
private val seekActionTime = 30000L
|
||||
|
||||
private var ignoreSSL: Boolean = true
|
||||
|
@ -182,6 +188,14 @@ class CS3IPlayer : IPlayer {
|
|||
subtitleHelper.initSubtitles(subView, subHolder, style)
|
||||
}
|
||||
|
||||
override fun getPreview(fraction: Float): Bitmap? {
|
||||
return imageGenerator.getPreviewImage(fraction)
|
||||
}
|
||||
|
||||
override fun hasPreview(): Boolean {
|
||||
return imageGenerator.hasPreview()
|
||||
}
|
||||
|
||||
override fun loadPlayer(
|
||||
context: Context,
|
||||
sameEpisode: Boolean,
|
||||
|
@ -190,7 +204,8 @@ class CS3IPlayer : IPlayer {
|
|||
startPosition: Long?,
|
||||
subtitles: Set<SubtitleData>,
|
||||
subtitle: SubtitleData?,
|
||||
autoPlay: Boolean?
|
||||
autoPlay: Boolean?,
|
||||
preview: Boolean,
|
||||
) {
|
||||
Log.i(TAG, "loadPlayer")
|
||||
if (sameEpisode) {
|
||||
|
@ -209,11 +224,30 @@ class CS3IPlayer : IPlayer {
|
|||
|
||||
// release the current exoplayer and cache
|
||||
releasePlayer()
|
||||
|
||||
if (link != null) {
|
||||
// only video support atm
|
||||
(imageGenerator as? PreviewGenerator)?.let { gen ->
|
||||
if (preview) {
|
||||
gen.load(link, sameEpisode)
|
||||
} else {
|
||||
gen.clear(sameEpisode)
|
||||
}
|
||||
}
|
||||
loadOnlinePlayer(context, link)
|
||||
} else if (data != null) {
|
||||
(imageGenerator as? PreviewGenerator)?.let { gen ->
|
||||
if (preview) {
|
||||
gen.load(context, data, sameEpisode)
|
||||
} else {
|
||||
gen.clear(sameEpisode)
|
||||
}
|
||||
}
|
||||
loadOfflinePlayer(context, data)
|
||||
} else {
|
||||
throw IllegalArgumentException("Requires link or uri")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun setActiveSubtitles(subtitles: Set<SubtitleData>) {
|
||||
|
@ -494,6 +528,7 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
|
||||
override fun release() {
|
||||
imageGenerator.release()
|
||||
releasePlayer()
|
||||
}
|
||||
|
||||
|
@ -508,12 +543,15 @@ class CS3IPlayer : IPlayer {
|
|||
**/
|
||||
var preferredAudioTrackLanguage: String? = null
|
||||
get() {
|
||||
return field ?: getKey(PREFERRED_AUDIO_LANGUAGE_KEY, field)?.also {
|
||||
return field ?: getKey(
|
||||
"$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY",
|
||||
field
|
||||
)?.also {
|
||||
field = it
|
||||
}
|
||||
}
|
||||
set(value) {
|
||||
setKey(PREFERRED_AUDIO_LANGUAGE_KEY, value)
|
||||
setKey("$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY", value)
|
||||
field = value
|
||||
}
|
||||
|
||||
|
@ -871,8 +909,20 @@ class CS3IPlayer : IPlayer {
|
|||
|
||||
CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source)
|
||||
CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source)
|
||||
CSPlayerEvent.NextEpisode -> event(EpisodeSeekEvent(offset = 1, source = source))
|
||||
CSPlayerEvent.PrevEpisode -> event(EpisodeSeekEvent(offset = -1, source = source))
|
||||
CSPlayerEvent.NextEpisode -> event(
|
||||
EpisodeSeekEvent(
|
||||
offset = 1,
|
||||
source = source
|
||||
)
|
||||
)
|
||||
|
||||
CSPlayerEvent.PrevEpisode -> event(
|
||||
EpisodeSeekEvent(
|
||||
offset = -1,
|
||||
source = source
|
||||
)
|
||||
)
|
||||
|
||||
CSPlayerEvent.SkipCurrentChapter -> {
|
||||
//val dur = this@CS3IPlayer.getDuration() ?: return@apply
|
||||
getCurrentTimestamp()?.let { lastTimeStamp ->
|
||||
|
|
|
@ -110,4 +110,9 @@ class DownloadedPlayerActivity : AppCompatActivity() {
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
CommonActivity.setActivityInstance(this)
|
||||
}
|
||||
}
|
|
@ -49,6 +49,7 @@ import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
|
|||
import com.lagradost.cloudstream3.ui.result.setText
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
|
@ -356,7 +357,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
|
||||
private fun setPlayBackSpeed(speed: Float) {
|
||||
try {
|
||||
setKey(PLAYBACK_SPEED_KEY, speed)
|
||||
DataStoreHelper.playBackSpeed = speed
|
||||
playerBinding?.playerSpeedBtt?.text =
|
||||
getString(R.string.player_speed_text_format).format(speed)
|
||||
.replace(".0x", "x")
|
||||
|
@ -1194,7 +1195,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
// init variables
|
||||
setPlayBackSpeed(getKey(PLAYBACK_SPEED_KEY) ?: 1.0f)
|
||||
setPlayBackSpeed(DataStoreHelper.playBackSpeed)
|
||||
savedInstanceState?.getLong(SUBTITLE_DELAY_BUNDLE_KEY)?.let {
|
||||
subtitleDelay = it
|
||||
}
|
||||
|
|
|
@ -182,6 +182,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
(if (sameEpisode) currentSelectedSubtitles else null) ?: getAutoSelectSubtitle(
|
||||
currentSubs, settings = true, downloads = true
|
||||
),
|
||||
preview = isFullScreenPlayer
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1025,7 +1026,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
ctx.getString(R.string.episode_sync_enabled_key), true
|
||||
)
|
||||
) maxEpisodeSet = meta.episode
|
||||
sync.modifyMaxEpisode(meta.episode)
|
||||
sync.modifyMaxEpisode(meta.totalEpisodeIndex ?: meta.episode)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Rational
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
||||
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
||||
|
@ -198,17 +199,8 @@ data class CurrentTracks(
|
|||
class InvalidFileException(msg: String) : Exception(msg)
|
||||
|
||||
//http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
|
||||
const val STATE_RESUME_WINDOW = "resumeWindow"
|
||||
const val STATE_RESUME_POSITION = "resumePosition"
|
||||
const val STATE_PLAYER_FULLSCREEN = "playerFullscreen"
|
||||
const val STATE_PLAYER_PLAYING = "playerOnPlay"
|
||||
const val ACTION_MEDIA_CONTROL = "media_control"
|
||||
const val EXTRA_CONTROL_TYPE = "control_type"
|
||||
const val PLAYBACK_SPEED = "playback_speed"
|
||||
const val RESIZE_MODE_KEY = "resize_mode" // Last used resize mode
|
||||
const val PLAYBACK_SPEED_KEY = "playback_speed" // Last used playback speed
|
||||
const val PREFERRED_SUBS_KEY = "preferred_subtitles" // Last used resize mode
|
||||
//const val PLAYBACK_FASTFORWARD = "playback_fastforward" // Last used resize mode
|
||||
|
||||
/** Abstract Exoplayer logic, can be expanded to other players */
|
||||
interface IPlayer {
|
||||
|
@ -246,11 +238,15 @@ interface IPlayer {
|
|||
startPosition: Long? = null,
|
||||
subtitles: Set<SubtitleData>,
|
||||
subtitle: SubtitleData?,
|
||||
autoPlay: Boolean? = true
|
||||
autoPlay: Boolean? = true,
|
||||
preview : Boolean = true,
|
||||
)
|
||||
|
||||
fun reloadPlayer(context: Context)
|
||||
|
||||
fun getPreview(fraction : Float) : Bitmap?
|
||||
fun hasPreview() : Boolean
|
||||
|
||||
fun setActiveSubtitles(subtitles: Set<SubtitleData>)
|
||||
fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean // returns true if the player requires a reload, null for nothing
|
||||
fun getCurrentPreferredSubtitle(): SubtitleData?
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import android.app.Activity
|
|||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -215,10 +216,16 @@ class QuickSearchFragment : Fragment() {
|
|||
binding?.quickSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
|
||||
|
||||
//val searchMagIcon =
|
||||
// binding.quickSearch.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
|
||||
// binding?.quickSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
|
||||
|
||||
//searchMagIcon?.scaleX = 0.65f
|
||||
//searchMagIcon?.scaleY = 0.65f
|
||||
// searchMagIcon?.scaleX = 0.65f
|
||||
// searchMagIcon?.scaleY = 0.65f
|
||||
|
||||
// Set the color for the search exit icon to the correct theme text color
|
||||
val searchExitIconColor = TypedValue()
|
||||
|
||||
activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true)
|
||||
searchExitIcon?.setColorFilter(searchExitIconColor.data)
|
||||
|
||||
binding?.quickSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
|
|
|
@ -47,7 +47,9 @@ data class ResultEpisode(
|
|||
/**
|
||||
* Conveys if the episode itself is marked as watched
|
||||
**/
|
||||
val videoWatchState: VideoWatchState
|
||||
val videoWatchState: VideoWatchState,
|
||||
/** Sum of all previous season episode counts + episode */
|
||||
val totalEpisodeIndex: Int? = null,
|
||||
)
|
||||
|
||||
fun ResultEpisode.getRealPosition(): Long {
|
||||
|
@ -82,6 +84,7 @@ fun buildResultEpisode(
|
|||
isFiller: Boolean? = null,
|
||||
tvType: TvType,
|
||||
parentId: Int,
|
||||
totalEpisodeIndex: Int? = null,
|
||||
): ResultEpisode {
|
||||
val posDur = getViewPos(id)
|
||||
val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None
|
||||
|
@ -103,7 +106,8 @@ fun buildResultEpisode(
|
|||
isFiller,
|
||||
tvType,
|
||||
parentId,
|
||||
videoWatchState
|
||||
videoWatchState,
|
||||
totalEpisodeIndex
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ import android.view.animation.DecelerateInterpolator
|
|||
import android.widget.AbsListView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.NestedScrollView
|
||||
|
@ -66,6 +65,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
|
|||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
|
@ -151,7 +151,8 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
|||
startPosition = 0L,
|
||||
subtitles = emptySet(),
|
||||
subtitle = null,
|
||||
autoPlay = false
|
||||
autoPlay = false,
|
||||
preview = false
|
||||
)
|
||||
true
|
||||
} ?: run {
|
||||
|
@ -429,20 +430,36 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
|||
}
|
||||
})
|
||||
resultSubscribe.setOnClickListener {
|
||||
val isSubscribed =
|
||||
viewModel.toggleSubscriptionStatus() ?: return@setOnClickListener
|
||||
viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? ->
|
||||
if (newStatus == null) return@toggleSubscriptionStatus
|
||||
|
||||
val message = if (isSubscribed) {
|
||||
// Kinda icky to have this here, but it works.
|
||||
SubscriptionWorkManager.enqueuePeriodicWork(context)
|
||||
R.string.subscription_new
|
||||
} else {
|
||||
R.string.subscription_deleted
|
||||
val message = if (newStatus) {
|
||||
// Kinda icky to have this here, but it works.
|
||||
SubscriptionWorkManager.enqueuePeriodicWork(context)
|
||||
R.string.subscription_new
|
||||
} else {
|
||||
R.string.subscription_deleted
|
||||
}
|
||||
|
||||
val name = (viewModel.page.value as? Resource.Success)?.value?.title
|
||||
?: txt(R.string.no_data).asStringNull(context) ?: ""
|
||||
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
resultFavorite.setOnClickListener {
|
||||
viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? ->
|
||||
if (newStatus == null) return@toggleFavoriteStatus
|
||||
|
||||
val name = (viewModel.page.value as? Resource.Success)?.value?.title
|
||||
?: txt(R.string.no_data).asStringNull(context) ?: ""
|
||||
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
|
||||
val message = if (newStatus) {
|
||||
R.string.favorite_added
|
||||
} else {
|
||||
R.string.favorite_removed
|
||||
}
|
||||
|
||||
val name = (viewModel.page.value as? Resource.Success)?.value?.title
|
||||
?: txt(R.string.no_data).asStringNull(context) ?: ""
|
||||
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
mediaRouteButton.apply {
|
||||
val chromecastSupport = api?.hasChromecastSupport == true
|
||||
|
@ -563,6 +580,19 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
|||
binding?.resultSubscribe?.setImageResource(drawable)
|
||||
}
|
||||
|
||||
observeNullable(viewModel.favoriteStatus) { isFavorite ->
|
||||
binding?.resultFavorite?.isVisible = isFavorite != null
|
||||
if (isFavorite == null) return@observeNullable
|
||||
|
||||
val drawable = if (isFavorite) {
|
||||
R.drawable.ic_baseline_favorite_24
|
||||
} else {
|
||||
R.drawable.ic_baseline_favorite_border_24
|
||||
}
|
||||
|
||||
binding?.resultFavorite?.setImageResource(drawable)
|
||||
}
|
||||
|
||||
observe(viewModel.trailers) { trailers ->
|
||||
setTrailers(trailers.flatMap { it.mirros }) // I dont care about subtitles yet!
|
||||
}
|
||||
|
@ -653,14 +683,13 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
|||
resultPoster.setImage(d.posterImage)
|
||||
resultPosterBackground.setImage(d.posterBackgroundImage)
|
||||
resultDescription.setTextHtml(d.plotText)
|
||||
resultDescription.setOnClickListener { view ->
|
||||
// todo bottom view?
|
||||
view.context?.let { ctx ->
|
||||
val builder: AlertDialog.Builder =
|
||||
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
|
||||
builder.setMessage(d.plotText.asString(ctx).html())
|
||||
.setTitle(d.plotHeaderText.asString(ctx))
|
||||
.show()
|
||||
resultDescription.setOnClickListener {
|
||||
activity?.let { activity ->
|
||||
activity.showBottomDialogText(
|
||||
d.titleText.asString(activity),
|
||||
d.plotText.asString(activity).html(),
|
||||
{}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -851,16 +880,11 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
|||
setRecommendations(recommendations, null)
|
||||
}
|
||||
observe(viewModel.episodeSynopsis) { description ->
|
||||
// TODO bottom dialog
|
||||
view.context?.let { ctx ->
|
||||
val builder: AlertDialog.Builder =
|
||||
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
|
||||
builder.setMessage(description.html())
|
||||
.setTitle(R.string.synopsis)
|
||||
.setOnDismissListener {
|
||||
viewModel.releaseEpisodeSynopsis()
|
||||
}
|
||||
.show()
|
||||
activity?.let { activity ->
|
||||
activity.showBottomDialogText(
|
||||
activity.getString(R.string.synopsis),
|
||||
description.html()
|
||||
) { viewModel.releaseEpisodeSynopsis() }
|
||||
}
|
||||
}
|
||||
context?.let { ctx ->
|
||||
|
@ -938,7 +962,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
|||
fab.context.getString(R.string.action_add_to_bookmarks),
|
||||
showApply = false,
|
||||
{}) {
|
||||
viewModel.updateWatchStatus(WatchType.values()[it])
|
||||
viewModel.updateWatchStatus(WatchType.values()[it], context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
|
@ -17,6 +18,7 @@ import androidx.lifecycle.ViewModelProvider
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
|
||||
import com.lagradost.cloudstream3.CommonActivity
|
||||
import com.lagradost.cloudstream3.DubStatus
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
|
||||
|
@ -26,6 +28,7 @@ import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding
|
|||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.mvvm.observeNullable
|
||||
import com.lagradost.cloudstream3.services.SubscriptionWorkManager
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup
|
||||
import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator
|
||||
|
@ -35,6 +38,7 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent
|
|||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
|
||||
import com.lagradost.cloudstream3.ui.search.SearchAdapter
|
||||
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.html
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
|
||||
|
@ -216,11 +220,9 @@ class ResultFragmentTv : Fragment() {
|
|||
episodesShadow.fade(show)
|
||||
episodeHolderTv.fade(show)
|
||||
if (episodesShadow.isRtl()) {
|
||||
episodesShadow.scaleX = -1.0f
|
||||
episodesShadow.scaleY = -1.0f
|
||||
episodesShadowBackground.scaleX = -1f
|
||||
} else {
|
||||
episodesShadow.scaleX = 1.0f
|
||||
episodesShadow.scaleY = 1.0f
|
||||
episodesShadowBackground.scaleX = 1f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -247,7 +249,7 @@ class ResultFragmentTv : Fragment() {
|
|||
|
||||
binding?.apply {
|
||||
//episodesShadow.rotationX = 180.0f//if(episodesShadow.isRtl()) 180.0f else 0.0f
|
||||
|
||||
|
||||
val leftListener: View.OnFocusChangeListener =
|
||||
View.OnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) return@OnFocusChangeListener
|
||||
|
@ -267,6 +269,7 @@ class ResultFragmentTv : Fragment() {
|
|||
resultEpisodesShow.onFocusChangeListener = rightListener
|
||||
resultDescription.onFocusChangeListener = leftListener
|
||||
resultBookmarkButton.onFocusChangeListener = leftListener
|
||||
resultFavoriteButton.onFocusChangeListener = leftListener
|
||||
resultEpisodesShow.setOnClickListener {
|
||||
// toggle, to make it more touch accessable just in case someone thinks that a
|
||||
// tv layout is better but is using a touch device
|
||||
|
@ -285,7 +288,9 @@ class ResultFragmentTv : Fragment() {
|
|||
resultPlaySeries,
|
||||
resultResumeSeries,
|
||||
resultPlayTrailer,
|
||||
resultBookmarkButton
|
||||
resultBookmarkButton,
|
||||
resultFavoriteButton,
|
||||
resultSubscribeButton
|
||||
)
|
||||
for (requestView in views) {
|
||||
if (!requestView.isVisible) continue
|
||||
|
@ -426,6 +431,8 @@ class ResultFragmentTv : Fragment() {
|
|||
val aboveCast = listOf(
|
||||
binding?.resultEpisodesShow,
|
||||
binding?.resultBookmarkButton,
|
||||
binding?.resultFavoriteButton,
|
||||
binding?.resultSubscribeButton,
|
||||
).firstOrNull {
|
||||
it?.isVisible == true
|
||||
}
|
||||
|
@ -528,7 +535,83 @@ class ResultFragmentTv : Fragment() {
|
|||
view.context.getString(R.string.action_add_to_bookmarks),
|
||||
showApply = false,
|
||||
{}) {
|
||||
viewModel.updateWatchStatus(WatchType.values()[it])
|
||||
viewModel.updateWatchStatus(WatchType.values()[it], context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observeNullable(viewModel.favoriteStatus) { isFavorite ->
|
||||
binding?.resultFavoriteButton?.apply {
|
||||
isVisible = isFavorite != null
|
||||
if (isFavorite == null) return@observeNullable
|
||||
|
||||
val drawable = if (isFavorite) {
|
||||
R.drawable.ic_baseline_favorite_24
|
||||
} else {
|
||||
R.drawable.ic_baseline_favorite_border_24
|
||||
}
|
||||
|
||||
val text = if (isFavorite) {
|
||||
R.string.action_remove_from_favorites
|
||||
} else {
|
||||
R.string.action_add_to_favorites
|
||||
}
|
||||
|
||||
setIconResource(drawable)
|
||||
setText(text)
|
||||
setOnClickListener {
|
||||
viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? ->
|
||||
if (newStatus == null) return@toggleFavoriteStatus
|
||||
|
||||
val message = if (newStatus) {
|
||||
R.string.favorite_added
|
||||
} else {
|
||||
R.string.favorite_removed
|
||||
}
|
||||
|
||||
val name = (viewModel.page.value as? Resource.Success)?.value?.title
|
||||
?: txt(R.string.no_data).asStringNull(context) ?: ""
|
||||
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observeNullable(viewModel.subscribeStatus) { isSubscribed ->
|
||||
binding?.resultSubscribeButton?.apply {
|
||||
isVisible = isSubscribed != null && context.isEmulatorSettings()
|
||||
if (isSubscribed == null) return@observeNullable
|
||||
|
||||
val drawable = if (isSubscribed) {
|
||||
R.drawable.ic_baseline_notifications_active_24
|
||||
} else {
|
||||
R.drawable.baseline_notifications_none_24
|
||||
}
|
||||
|
||||
val text = if (isSubscribed) {
|
||||
R.string.action_unsubscribe
|
||||
} else {
|
||||
R.string.action_subscribe
|
||||
}
|
||||
|
||||
setIconResource(drawable)
|
||||
setText(text)
|
||||
setOnClickListener {
|
||||
viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? ->
|
||||
if (newStatus == null) return@toggleSubscriptionStatus
|
||||
|
||||
val message = if (newStatus) {
|
||||
// Kinda icky to have this here, but it works.
|
||||
SubscriptionWorkManager.enqueuePeriodicWork(context)
|
||||
R.string.subscription_new
|
||||
} else {
|
||||
R.string.subscription_deleted
|
||||
}
|
||||
|
||||
val name = (viewModel.page.value as? Resource.Success)?.value?.title
|
||||
?: txt(R.string.no_data).asStringNull(context) ?: ""
|
||||
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -537,6 +620,7 @@ class ResultFragmentTv : Fragment() {
|
|||
observeNullable(viewModel.movie) { data ->
|
||||
binding?.apply {
|
||||
resultPlayMovie.isVisible = data is Resource.Success
|
||||
resultPlaySeries.isVisible = data == null
|
||||
seriesHolder.isVisible = data == null
|
||||
resultEpisodesShow.isVisible = data == null
|
||||
|
||||
|
@ -766,12 +850,14 @@ class ResultFragmentTv : Fragment() {
|
|||
R.drawable.profile_bg_red,
|
||||
R.drawable.profile_bg_teal
|
||||
).random()
|
||||
//Change poster crop area to 20% from Top
|
||||
backgroundPoster.cropYCenterOffsetPct = 0.20F
|
||||
|
||||
backgroundPoster.setImage(
|
||||
d.posterBackgroundImage ?: UiImage.Drawable(error),
|
||||
radius = 0,
|
||||
errorImageDrawable = error
|
||||
)
|
||||
|
||||
resultComingSoon.isVisible = d.comingSoon
|
||||
resultDataHolder.isGone = d.comingSoon
|
||||
UIHelper.populateChips(resultTag, d.tags)
|
||||
|
|
|
@ -7,6 +7,8 @@ import android.os.Build
|
|||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.LiveData
|
||||
|
@ -31,6 +33,7 @@ import com.lagradost.cloudstream3.mvvm.*
|
|||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
|
||||
import com.lagradost.cloudstream3.ui.APIRepository
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
|
||||
|
@ -45,19 +48,37 @@ import com.lagradost.cloudstream3.utils.*
|
|||
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||
import com.lagradost.cloudstream3.utils.CastHelper.startCast
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteBookmarkedData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllBookmarkedData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getFavoritesData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultEpisode
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getSubscribedData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getVideoWatchState
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.removeFavoritesData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.removeSubscribedData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setBookmarkedData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setFavoritesData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setSubscribedData
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
|
@ -110,6 +131,18 @@ data class ResultData(
|
|||
val plotHeaderText: UiText,
|
||||
)
|
||||
|
||||
data class CheckDuplicateData(
|
||||
val name: String,
|
||||
val year: Int?,
|
||||
val syncData: Map<String, String>?
|
||||
)
|
||||
|
||||
enum class LibraryListType {
|
||||
BOOKMARKS,
|
||||
FAVORITES,
|
||||
SUBSCRIPTIONS
|
||||
}
|
||||
|
||||
fun txt(status: DubStatus?): UiText? {
|
||||
return txt(
|
||||
when (status) {
|
||||
|
@ -425,6 +458,9 @@ class ResultViewModel2 : ViewModel() {
|
|||
private val _subscribeStatus: MutableLiveData<Boolean?> = MutableLiveData(null)
|
||||
val subscribeStatus: LiveData<Boolean?> = _subscribeStatus
|
||||
|
||||
private val _favoriteStatus: MutableLiveData<Boolean?> = MutableLiveData(null)
|
||||
val favoriteStatus: LiveData<Boolean?> = _favoriteStatus
|
||||
|
||||
companion object {
|
||||
const val TAG = "RVM2"
|
||||
//private const val EPISODE_RANGE_SIZE = 20
|
||||
|
@ -435,33 +471,6 @@ class ResultViewModel2 : ViewModel() {
|
|||
return this?.firstOrNull { it.season == season }
|
||||
}
|
||||
|
||||
fun updateWatchStatus(currentResponse: LoadResponse, status: WatchType) {
|
||||
val currentId = currentResponse.getId()
|
||||
|
||||
val currentWatchType = getResultWatchState(currentId)
|
||||
|
||||
DataStoreHelper.setResultWatchState(currentId, status.internalId)
|
||||
val current = DataStoreHelper.getBookmarkedData(currentId)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
DataStoreHelper.setBookmarkedData(
|
||||
currentId,
|
||||
DataStoreHelper.BookmarkedData(
|
||||
currentId,
|
||||
current?.bookmarkedTime ?: currentTime,
|
||||
currentTime,
|
||||
currentResponse.name,
|
||||
currentResponse.url,
|
||||
currentResponse.apiName,
|
||||
currentResponse.type,
|
||||
currentResponse.posterUrl,
|
||||
currentResponse.year
|
||||
)
|
||||
)
|
||||
if (currentWatchType != status) {
|
||||
MainActivity.bookmarksUpdatedEvent(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterName(name: String?): String? {
|
||||
if (name == null) return null
|
||||
Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let {
|
||||
|
@ -816,9 +825,77 @@ class ResultViewModel2 : ViewModel() {
|
|||
val selectPopup: LiveData<SelectPopup?> = _selectPopup
|
||||
|
||||
|
||||
fun updateWatchStatus(status: WatchType) {
|
||||
updateWatchStatus(currentResponse ?: return, status)
|
||||
_watchStatus.postValue(status)
|
||||
fun updateWatchStatus(
|
||||
status: WatchType,
|
||||
context: Context?,
|
||||
loadResponse: LoadResponse? = null,
|
||||
statusChangedCallback: ((statusChanged: Boolean) -> Unit)? = null
|
||||
) {
|
||||
val response = loadResponse ?: currentResponse ?: return
|
||||
|
||||
val currentId = response.getId()
|
||||
|
||||
val currentStatus = getResultWatchState(currentId)
|
||||
|
||||
// If the current status is "NONE" and the new status is not "NONE",
|
||||
// fetch the bookmarked data to check for duplicates, otherwise set this
|
||||
// to an empty list, so that we don't show the duplicate warning dialog,
|
||||
// but we still want to update the current bookmark and refresh the data anyway.
|
||||
val bookmarkedData = if (currentStatus == WatchType.NONE && status != WatchType.NONE) {
|
||||
getAllBookmarkedData()
|
||||
} else emptyList()
|
||||
|
||||
checkAndWarnDuplicates(
|
||||
context,
|
||||
LibraryListType.BOOKMARKS,
|
||||
CheckDuplicateData(
|
||||
name = response.name,
|
||||
year = response.year,
|
||||
syncData = response.syncData,
|
||||
),
|
||||
bookmarkedData
|
||||
) { shouldContinue: Boolean, duplicateIds: List<Int?> ->
|
||||
if (!shouldContinue) return@checkAndWarnDuplicates
|
||||
|
||||
if (duplicateIds.isNotEmpty()) {
|
||||
duplicateIds.forEach { duplicateId ->
|
||||
deleteBookmarkedData(duplicateId)
|
||||
}
|
||||
}
|
||||
|
||||
setResultWatchState(currentId, status.internalId)
|
||||
|
||||
// We don't need to store if WatchType.NONE.
|
||||
// The key is removed in setResultWatchState, we don't want to
|
||||
// re-add it again here if it was just removed.
|
||||
if (status != WatchType.NONE) {
|
||||
val current = getBookmarkedData(currentId)
|
||||
|
||||
setBookmarkedData(
|
||||
currentId,
|
||||
DataStoreHelper.BookmarkedData(
|
||||
current?.bookmarkedTime ?: unixTimeMS,
|
||||
currentId,
|
||||
unixTimeMS,
|
||||
response.name,
|
||||
response.url,
|
||||
response.apiName,
|
||||
response.type,
|
||||
response.posterUrl,
|
||||
response.year,
|
||||
response.syncData
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (currentStatus != status) {
|
||||
MainActivity.bookmarksUpdatedEvent(true)
|
||||
}
|
||||
|
||||
_watchStatus.postValue(status)
|
||||
|
||||
statusChangedCallback?.invoke(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startChromecast(
|
||||
|
@ -833,39 +910,255 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
|
||||
/**
|
||||
* @return true if the new status is Subscribed, false if not. Null if not possible to subscribe.
|
||||
**/
|
||||
fun toggleSubscriptionStatus(): Boolean? {
|
||||
val isSubscribed = _subscribeStatus.value ?: return null
|
||||
val response = currentResponse ?: return null
|
||||
if (response !is EpisodeResponse) return null
|
||||
* Toggles the subscription status of an item.
|
||||
*
|
||||
* @param context The context to use for operations.
|
||||
* @param statusChangedCallback A callback that is invoked when the subscription status changes.
|
||||
* It provides the new subscription status (true if subscribed, false if unsubscribed, null if action was canceled).
|
||||
*/
|
||||
fun toggleSubscriptionStatus(
|
||||
context: Context?,
|
||||
statusChangedCallback: ((newStatus: Boolean?) -> Unit)? = null
|
||||
) {
|
||||
val isSubscribed = _subscribeStatus.value ?: return
|
||||
val response = currentResponse ?: return
|
||||
if (response !is EpisodeResponse) return
|
||||
|
||||
val currentId = response.getId()
|
||||
|
||||
if (isSubscribed) {
|
||||
DataStoreHelper.removeSubscribedData(currentId)
|
||||
removeSubscribedData(currentId)
|
||||
statusChangedCallback?.invoke(false)
|
||||
_subscribeStatus.postValue(false)
|
||||
} else {
|
||||
val current = DataStoreHelper.getSubscribedData(currentId)
|
||||
checkAndWarnDuplicates(
|
||||
context,
|
||||
LibraryListType.SUBSCRIPTIONS,
|
||||
CheckDuplicateData(
|
||||
name = response.name,
|
||||
year = response.year,
|
||||
syncData = response.syncData,
|
||||
),
|
||||
getAllSubscriptions(),
|
||||
) { shouldContinue: Boolean, duplicateIds: List<Int?> ->
|
||||
if (!shouldContinue) {
|
||||
statusChangedCallback?.invoke(null)
|
||||
return@checkAndWarnDuplicates
|
||||
}
|
||||
|
||||
DataStoreHelper.setSubscribedData(
|
||||
currentId,
|
||||
DataStoreHelper.SubscribedData(
|
||||
if (duplicateIds.isNotEmpty()) {
|
||||
duplicateIds.forEach { duplicateId ->
|
||||
removeSubscribedData(duplicateId)
|
||||
}
|
||||
}
|
||||
|
||||
val current = getSubscribedData(currentId)
|
||||
|
||||
setSubscribedData(
|
||||
currentId,
|
||||
current?.bookmarkedTime ?: unixTimeMS,
|
||||
unixTimeMS,
|
||||
response.getLatestEpisodes(),
|
||||
response.name,
|
||||
response.url,
|
||||
response.apiName,
|
||||
response.type,
|
||||
response.posterUrl,
|
||||
response.year
|
||||
DataStoreHelper.SubscribedData(
|
||||
current?.subscribedTime ?: unixTimeMS,
|
||||
response.getLatestEpisodes(),
|
||||
currentId,
|
||||
unixTimeMS,
|
||||
response.name,
|
||||
response.url,
|
||||
response.apiName,
|
||||
response.type,
|
||||
response.posterUrl,
|
||||
response.year,
|
||||
response.syncData
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
_subscribeStatus.postValue(true)
|
||||
|
||||
statusChangedCallback?.invoke(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the favorite status of an item.
|
||||
*
|
||||
* @param context The context to use.
|
||||
* @param statusChangedCallback A callback that is invoked when the favorite status changes.
|
||||
* It provides the new favorite status (true if added to favorites, false if removed, null if action was canceled).
|
||||
*/
|
||||
fun toggleFavoriteStatus(
|
||||
context: Context?,
|
||||
statusChangedCallback: ((newStatus: Boolean?) -> Unit)? = null
|
||||
) {
|
||||
val isFavorite = _favoriteStatus.value ?: return
|
||||
val response = currentResponse ?: return
|
||||
|
||||
val currentId = response.getId()
|
||||
|
||||
if (isFavorite) {
|
||||
removeFavoritesData(currentId)
|
||||
statusChangedCallback?.invoke(false)
|
||||
_favoriteStatus.postValue(false)
|
||||
} else {
|
||||
checkAndWarnDuplicates(
|
||||
context,
|
||||
LibraryListType.FAVORITES,
|
||||
CheckDuplicateData(
|
||||
name = response.name,
|
||||
year = response.year,
|
||||
syncData = response.syncData,
|
||||
),
|
||||
getAllFavorites(),
|
||||
) { shouldContinue: Boolean, duplicateIds: List<Int?> ->
|
||||
if (!shouldContinue) {
|
||||
statusChangedCallback?.invoke(null)
|
||||
return@checkAndWarnDuplicates
|
||||
}
|
||||
|
||||
if (duplicateIds.isNotEmpty()) {
|
||||
duplicateIds.forEach { duplicateId ->
|
||||
removeFavoritesData(duplicateId)
|
||||
}
|
||||
}
|
||||
|
||||
val current = getFavoritesData(currentId)
|
||||
|
||||
setFavoritesData(
|
||||
currentId,
|
||||
DataStoreHelper.FavoritesData(
|
||||
current?.favoritesTime ?: unixTimeMS,
|
||||
currentId,
|
||||
unixTimeMS,
|
||||
response.name,
|
||||
response.url,
|
||||
response.apiName,
|
||||
response.type,
|
||||
response.posterUrl,
|
||||
response.year,
|
||||
response.syncData
|
||||
)
|
||||
)
|
||||
|
||||
_favoriteStatus.postValue(true)
|
||||
|
||||
statusChangedCallback?.invoke(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun checkAndWarnDuplicates(
|
||||
context: Context?,
|
||||
listType: LibraryListType,
|
||||
checkDuplicateData: CheckDuplicateData,
|
||||
data: List<DataStoreHelper.LibrarySearchResponse>,
|
||||
checkDuplicatesCallback: (shouldContinue: Boolean, duplicateIds: List<Int?>) -> Unit
|
||||
) {
|
||||
val whitespaceRegex = "\\s+".toRegex()
|
||||
fun normalizeString(input: String): String {
|
||||
/**
|
||||
* Trim the input string and replace consecutive spaces with a single space.
|
||||
* This covers some edge-cases where the title does not match exactly across providers,
|
||||
* and one provider has the title with an extra whitespace. This is minor enough that
|
||||
* it should still match in this case.
|
||||
*/
|
||||
return input.trim().replace(whitespaceRegex, " ")
|
||||
}
|
||||
|
||||
_subscribeStatus.postValue(!isSubscribed)
|
||||
return !isSubscribed
|
||||
val syncData = checkDuplicateData.syncData
|
||||
|
||||
val imdbId = getImdbIdFromSyncData(syncData)
|
||||
val tmdbId = getTMDbIdFromSyncData(syncData)
|
||||
val malId = syncData?.get(AccountManager.malApi.idPrefix)
|
||||
val aniListId = syncData?.get(AccountManager.aniListApi.idPrefix)
|
||||
val normalizedName = normalizeString(checkDuplicateData.name)
|
||||
val year = checkDuplicateData.year
|
||||
|
||||
val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse ->
|
||||
val librarySyncData = it.syncData
|
||||
|
||||
val checks = listOf(
|
||||
{ imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId },
|
||||
{ tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId },
|
||||
{ malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId },
|
||||
{ aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId },
|
||||
{ normalizedName == normalizeString(it.name) && year == it.year }
|
||||
)
|
||||
|
||||
checks.any { it() }
|
||||
}
|
||||
|
||||
if (duplicateEntries.isEmpty() || context == null) {
|
||||
checkDuplicatesCallback.invoke(true, emptyList())
|
||||
return
|
||||
}
|
||||
|
||||
val replaceMessage = if (duplicateEntries.size > 1) {
|
||||
R.string.duplicate_replace_all
|
||||
} else R.string.duplicate_replace
|
||||
|
||||
val message = if (duplicateEntries.size == 1) {
|
||||
val list = when (listType) {
|
||||
LibraryListType.BOOKMARKS -> getResultWatchState(duplicateEntries[0].id ?: 0).stringRes
|
||||
LibraryListType.FAVORITES -> R.string.favorites_list_name
|
||||
LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name
|
||||
}
|
||||
|
||||
context.getString(R.string.duplicate_message_single,
|
||||
"${normalizeString(duplicateEntries[0].name)} (${context.getString(list)}) — ${duplicateEntries[0].apiName}"
|
||||
)
|
||||
} else {
|
||||
val bulletPoints = duplicateEntries.joinToString("\n") {
|
||||
val list = when (listType) {
|
||||
LibraryListType.BOOKMARKS -> getResultWatchState(it.id ?: 0).stringRes
|
||||
LibraryListType.FAVORITES -> R.string.favorites_list_name
|
||||
LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name
|
||||
}
|
||||
|
||||
"• ${it.apiName}: ${normalizeString(it.name)} (${context.getString(list)})"
|
||||
}
|
||||
|
||||
context.getString(R.string.duplicate_message_multiple, bulletPoints)
|
||||
}
|
||||
|
||||
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
|
||||
|
||||
val dialogClickListener =
|
||||
DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
checkDuplicatesCallback.invoke(true, emptyList())
|
||||
}
|
||||
DialogInterface.BUTTON_NEGATIVE -> {
|
||||
checkDuplicatesCallback.invoke(false, emptyList())
|
||||
}
|
||||
DialogInterface.BUTTON_NEUTRAL -> {
|
||||
checkDuplicatesCallback.invoke(true, duplicateEntries.map { it.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.setTitle(R.string.duplicate_title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.duplicate_add, dialogClickListener)
|
||||
.setNegativeButton(R.string.duplicate_cancel, dialogClickListener)
|
||||
.setNeutralButton(replaceMessage, dialogClickListener)
|
||||
.show().setDefaultFocus()
|
||||
}
|
||||
|
||||
private fun getImdbIdFromSyncData(syncData: Map<String, String>?): String? {
|
||||
return normalSafeApiCall {
|
||||
SimklApi.readIdFromString(
|
||||
syncData?.get(AccountManager.simklApi.idPrefix)
|
||||
)[SimklApi.Companion.SyncServices.Imdb]
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTMDbIdFromSyncData(syncData: Map<String, String>?): String? {
|
||||
return normalSafeApiCall {
|
||||
SimklApi.readIdFromString(
|
||||
syncData?.get(AccountManager.simklApi.idPrefix)
|
||||
)[SimklApi.Companion.SyncServices.Tmdb]
|
||||
}
|
||||
}
|
||||
|
||||
private fun startChromecast(
|
||||
|
@ -1219,7 +1512,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
// Do not add mark as watched on movies
|
||||
if (!listOf(TvType.Movie, TvType.AnimeMovie).contains(click.data.tvType)) {
|
||||
val isWatched =
|
||||
DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched
|
||||
getVideoWatchState(click.data.id) == VideoWatchState.Watched
|
||||
|
||||
val watchedText = if (isWatched) R.string.action_remove_from_watched
|
||||
else R.string.action_mark_as_watched
|
||||
|
@ -1468,12 +1761,12 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
ACTION_MARK_AS_WATCHED -> {
|
||||
val isWatched =
|
||||
DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched
|
||||
getVideoWatchState(click.data.id) == VideoWatchState.Watched
|
||||
|
||||
if (isWatched) {
|
||||
DataStoreHelper.setVideoWatchState(click.data.id, VideoWatchState.None)
|
||||
setVideoWatchState(click.data.id, VideoWatchState.None)
|
||||
} else {
|
||||
DataStoreHelper.setVideoWatchState(click.data.id, VideoWatchState.Watched)
|
||||
setVideoWatchState(click.data.id, VideoWatchState.Watched)
|
||||
}
|
||||
|
||||
// Kinda dirty to reload all episodes :(
|
||||
|
@ -1682,7 +1975,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
list.subList(start, end).map {
|
||||
val posDur = getViewPos(it.id)
|
||||
val watchState =
|
||||
DataStoreHelper.getVideoWatchState(it.id) ?: VideoWatchState.None
|
||||
getVideoWatchState(it.id) ?: VideoWatchState.None
|
||||
it.copy(
|
||||
position = posDur?.position ?: 0,
|
||||
duration = posDur?.duration ?: 0,
|
||||
|
@ -1743,13 +2036,19 @@ class ResultViewModel2 : ViewModel() {
|
|||
private fun postSubscription(loadResponse: LoadResponse) {
|
||||
if (loadResponse.isEpisodeBased()) {
|
||||
val id = loadResponse.getId()
|
||||
val data = DataStoreHelper.getSubscribedData(id)
|
||||
DataStoreHelper.updateSubscribedData(id, data, loadResponse as? EpisodeResponse)
|
||||
val data = getSubscribedData(id)
|
||||
updateSubscribedData(id, data, loadResponse as? EpisodeResponse)
|
||||
val isSubscribed = data != null
|
||||
_subscribeStatus.postValue(isSubscribed)
|
||||
}
|
||||
}
|
||||
|
||||
private fun postFavorites(loadResponse: LoadResponse) {
|
||||
val id = loadResponse.getId()
|
||||
val isFavorite = getFavoritesData(id) != null
|
||||
_favoriteStatus.postValue(isFavorite)
|
||||
}
|
||||
|
||||
private fun postEpisodeRange(indexer: EpisodeIndexer?, range: EpisodeRange?) {
|
||||
if (range == null || indexer == null) {
|
||||
return
|
||||
|
@ -1887,6 +2186,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
currentResponse = loadResponse
|
||||
postPage(loadResponse, apiRepository)
|
||||
postSubscription(loadResponse)
|
||||
postFavorites(loadResponse)
|
||||
if (updateEpisodes)
|
||||
postEpisodes(loadResponse, updateFillers)
|
||||
}
|
||||
|
@ -1915,6 +2215,10 @@ class ResultViewModel2 : ViewModel() {
|
|||
val id =
|
||||
mainId + episode + idIndex * 1_000_000 + (i.season?.times(10_000)
|
||||
?: 0)
|
||||
|
||||
val totalIndex =
|
||||
i.season?.let { season -> loadResponse.getTotalEpisodeIndex(episode, season) }
|
||||
|
||||
if (!existingEpisodes.contains(id)) {
|
||||
existingEpisodes.add(id)
|
||||
val seasonData = loadResponse.seasonNames.getSeason(i.season)
|
||||
|
@ -1934,7 +2238,8 @@ class ResultViewModel2 : ViewModel() {
|
|||
i.description,
|
||||
fillers.getOrDefault(episode, false),
|
||||
loadResponse.type,
|
||||
mainId
|
||||
mainId,
|
||||
totalIndex
|
||||
)
|
||||
|
||||
val season = eps.seasonIndex ?: 0
|
||||
|
@ -1963,6 +2268,9 @@ class ResultViewModel2 : ViewModel() {
|
|||
val seasonData =
|
||||
loadResponse.seasonNames.getSeason(episode.season)
|
||||
|
||||
val totalIndex =
|
||||
episode.season?.let { season -> loadResponse.getTotalEpisodeIndex(episodeIndex, season) }
|
||||
|
||||
val ep =
|
||||
buildResultEpisode(
|
||||
loadResponse.name,
|
||||
|
@ -1979,7 +2287,8 @@ class ResultViewModel2 : ViewModel() {
|
|||
episode.description,
|
||||
null,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
mainId,
|
||||
totalIndex
|
||||
)
|
||||
|
||||
val season = ep.seasonIndex ?: 0
|
||||
|
@ -2010,7 +2319,8 @@ class ResultViewModel2 : ViewModel() {
|
|||
null,
|
||||
null,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
mainId,
|
||||
null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -2032,7 +2342,8 @@ class ResultViewModel2 : ViewModel() {
|
|||
null,
|
||||
null,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
mainId,
|
||||
null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -2054,7 +2365,8 @@ class ResultViewModel2 : ViewModel() {
|
|||
null,
|
||||
null,
|
||||
loadResponse.type,
|
||||
mainId
|
||||
mainId,
|
||||
null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -2115,13 +2427,13 @@ class ResultViewModel2 : ViewModel() {
|
|||
postResume()
|
||||
}
|
||||
|
||||
fun postResume() {
|
||||
private fun postResume() {
|
||||
_resumeWatching.postValue(resume())
|
||||
}
|
||||
|
||||
private fun resume(): ResumeWatchingStatus? {
|
||||
val correctId = currentId ?: return null
|
||||
val resume = DataStoreHelper.getLastWatched(correctId)
|
||||
val resume = getLastWatched(correctId)
|
||||
val resumeParentId = resume?.parentId
|
||||
if (resumeParentId != correctId) return null // is null or smth went wrong with getLastWatched
|
||||
val resumeId = resume.episodeId ?: return null// invalid episode id
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui.search
|
|||
import android.content.DialogInterface
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -11,6 +12,7 @@ import android.widget.AbsListView
|
|||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isVisible
|
||||
|
@ -22,17 +24,22 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
|
||||
import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiSettings
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.AllLanguagesName
|
||||
import com.lagradost.cloudstream3.AnimeSearchResponse
|
||||
import com.lagradost.cloudstream3.HomePageList
|
||||
import com.lagradost.cloudstream3.MainAPI
|
||||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.databinding.FragmentSearchBinding
|
||||
import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
|
@ -53,8 +60,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.ownHide
|
|||
import com.lagradost.cloudstream3.utils.AppUtils.ownShow
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
|
@ -62,9 +69,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
const val SEARCH_PREF_TAGS = "search_pref_tags"
|
||||
const val SEARCH_PREF_PROVIDERS = "search_pref_providers"
|
||||
|
||||
class SearchFragment : Fragment() {
|
||||
companion object {
|
||||
fun List<SearchResponse>.filterSearchResponse(): List<SearchResponse> {
|
||||
|
@ -193,7 +197,7 @@ class SearchFragment : Fragment() {
|
|||
validAPIs.flatMap { api -> api.supportedTypes }.distinct()
|
||||
) { list ->
|
||||
if (selectedSearchTypes.toSet() != list.toSet()) {
|
||||
setKey(SEARCH_PREF_TAGS, selectedSearchTypes)
|
||||
DataStoreHelper.searchPreferenceTags = list
|
||||
selectedSearchTypes.clear()
|
||||
selectedSearchTypes.addAll(list)
|
||||
search(binding?.mainSearch?.query?.toString())
|
||||
|
@ -219,7 +223,7 @@ class SearchFragment : Fragment() {
|
|||
SearchHelper.handleSearchClickCallback(callback)
|
||||
}
|
||||
|
||||
|
||||
searchRoot.findViewById<TextView>(R.id.search_src_text)?.tag = "tv_no_focus_tag"
|
||||
searchAutofitResults.adapter = adapter
|
||||
searchLoadingBar.alpha = 0f
|
||||
}
|
||||
|
@ -228,17 +232,17 @@ class SearchFragment : Fragment() {
|
|||
val searchExitIcon =
|
||||
binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
|
||||
// val searchMagIcon =
|
||||
// main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
|
||||
//searchMagIcon.scaleX = 0.65f
|
||||
//searchMagIcon.scaleY = 0.65f
|
||||
// binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
|
||||
// searchMagIcon.scaleX = 0.65f
|
||||
// searchMagIcon.scaleY = 0.65f
|
||||
|
||||
context?.let { ctx ->
|
||||
val validAPIs = ctx.filterProviderByPreferredMedia()
|
||||
selectedApis = ctx.getKey(
|
||||
SEARCH_PREF_PROVIDERS,
|
||||
defVal = validAPIs.map { it.name }
|
||||
)!!.toMutableSet()
|
||||
}
|
||||
// Set the color for the search exit icon to the correct theme text color
|
||||
val searchExitIconColor = TypedValue()
|
||||
|
||||
activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true)
|
||||
searchExitIcon?.setColorFilter(searchExitIconColor.data)
|
||||
|
||||
selectedApis = DataStoreHelper.searchPreferenceProviders.toMutableSet()
|
||||
|
||||
binding?.searchFilter?.setOnClickListener { searchView ->
|
||||
searchView?.context?.let { ctx ->
|
||||
|
@ -286,7 +290,7 @@ class SearchFragment : Fragment() {
|
|||
}
|
||||
|
||||
fun updateList(types: List<TvType>) {
|
||||
setKey(SEARCH_PREF_TAGS, types.map { it.name })
|
||||
DataStoreHelper.searchPreferenceTags = types
|
||||
|
||||
arrayAdapter.clear()
|
||||
currentValidApis = validAPIs.filter { api ->
|
||||
|
@ -311,12 +315,7 @@ class SearchFragment : Fragment() {
|
|||
arrayAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
val selectedSearchTypes = getKey<List<String>>(SEARCH_PREF_TAGS)
|
||||
?.mapNotNull { listName ->
|
||||
TvType.values().firstOrNull { it.name == listName }
|
||||
}
|
||||
?.toMutableList()
|
||||
?: mutableListOf(TvType.Movie, TvType.TvSeries)
|
||||
val selectedSearchTypes = DataStoreHelper.searchPreferenceTags
|
||||
|
||||
bindChips(
|
||||
binding.tvtypesChipsScroll.tvtypesChips,
|
||||
|
@ -342,7 +341,7 @@ class SearchFragment : Fragment() {
|
|||
}
|
||||
|
||||
dialog.setOnDismissListener {
|
||||
context?.setKey(SEARCH_PREF_PROVIDERS, currentSelectedApis.toList())
|
||||
DataStoreHelper.searchPreferenceProviders = currentSelectedApis.toList()
|
||||
selectedApis = currentSelectedApis
|
||||
}
|
||||
updateList(selectedSearchTypes.toList())
|
||||
|
@ -353,10 +352,7 @@ class SearchFragment : Fragment() {
|
|||
val settingsManager = context?.let { PreferenceManager.getDefaultSharedPreferences(it) }
|
||||
val isAdvancedSearch = settingsManager?.getBoolean("advanced_search", true) ?: true
|
||||
|
||||
selectedSearchTypes = context?.getKey<List<String>>(SEARCH_PREF_TAGS)
|
||||
?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } }
|
||||
?.toMutableList()
|
||||
?: mutableListOf(TvType.Movie, TvType.TvSeries)
|
||||
selectedSearchTypes = DataStoreHelper.searchPreferenceTags.toMutableList()
|
||||
|
||||
if (isTrueTvSettings()) {
|
||||
binding?.searchFilter?.isFocusable = true
|
||||
|
@ -398,7 +394,7 @@ class SearchFragment : Fragment() {
|
|||
DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
removeKeys(SEARCH_HISTORY_KEY)
|
||||
removeKeys("$currentAccount/$SEARCH_HISTORY_KEY")
|
||||
searchViewModel.updateHistory()
|
||||
}
|
||||
DialogInterface.BUTTON_NEGATIVE -> {
|
||||
|
@ -510,7 +506,7 @@ class SearchFragment : Fragment() {
|
|||
binding?.mainSearch?.setQuery(searchItem.searchText, true)
|
||||
}
|
||||
SEARCH_HISTORY_REMOVE -> {
|
||||
removeKey(SEARCH_HISTORY_KEY, searchItem.key)
|
||||
removeKey("$currentAccount/$SEARCH_HISTORY_KEY", searchItem.key)
|
||||
searchViewModel.updateHistory()
|
||||
}
|
||||
else -> {
|
||||
|
@ -559,4 +555,4 @@ class SearchFragment : Fragment() {
|
|||
.commit()*/
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import com.lagradost.cloudstream3.mvvm.Resource
|
|||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||
import com.lagradost.cloudstream3.ui.APIRepository
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -64,7 +65,7 @@ class SearchViewModel : ViewModel() {
|
|||
|
||||
fun updateHistory() = viewModelScope.launch {
|
||||
ioSafe {
|
||||
val items = getKeys(SEARCH_HISTORY_KEY)?.mapNotNull {
|
||||
val items = getKeys("$currentAccount/$SEARCH_HISTORY_KEY")?.mapNotNull {
|
||||
getKey<SearchHistoryItem>(it)
|
||||
}?.sortedByDescending { it.searchedAt } ?: emptyList()
|
||||
_currentHistory.postValue(items)
|
||||
|
@ -87,7 +88,7 @@ class SearchViewModel : ViewModel() {
|
|||
if (!isQuickSearch) {
|
||||
val key = query.hashCode().toString()
|
||||
setKey(
|
||||
SEARCH_HISTORY_KEY,
|
||||
"$currentAccount/$SEARCH_HISTORY_KEY",
|
||||
key,
|
||||
SearchHistoryItem(
|
||||
searchedAt = System.currentTimeMillis(),
|
||||
|
@ -140,4 +141,4 @@ class SearchViewModel : ViewModel() {
|
|||
_searchResponse.postValue(Resource.Success(list))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -194,13 +194,13 @@ class SettingsFragment : Fragment() {
|
|||
}
|
||||
binding?.apply {
|
||||
listOf(
|
||||
settingsGeneral to R.id.action_navigation_settings_to_navigation_settings_general,
|
||||
settingsPlayer to R.id.action_navigation_settings_to_navigation_settings_player,
|
||||
settingsCredits to R.id.action_navigation_settings_to_navigation_settings_account,
|
||||
settingsUi to R.id.action_navigation_settings_to_navigation_settings_ui,
|
||||
settingsProviders to R.id.action_navigation_settings_to_navigation_settings_providers,
|
||||
settingsUpdates to R.id.action_navigation_settings_to_navigation_settings_updates,
|
||||
settingsExtensions to R.id.action_navigation_settings_to_navigation_settings_extensions,
|
||||
settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general,
|
||||
settingsPlayer to R.id.action_navigation_global_to_navigation_settings_player,
|
||||
settingsCredits to R.id.action_navigation_global_to_navigation_settings_account,
|
||||
settingsUi to R.id.action_navigation_global_to_navigation_settings_ui,
|
||||
settingsProviders to R.id.action_navigation_global_to_navigation_settings_providers,
|
||||
settingsUpdates to R.id.action_navigation_global_to_navigation_settings_updates,
|
||||
settingsExtensions to R.id.action_navigation_global_to_navigation_settings_extensions,
|
||||
).forEach { (view, navigationId) ->
|
||||
view.apply {
|
||||
setOnClickListener {
|
||||
|
@ -235,4 +235,4 @@ class SettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,6 +82,7 @@ val appLanguages = arrayListOf(
|
|||
Triple("", "日本語 (にほんご)", "ja"),
|
||||
Triple("", "ಕನ್ನಡ", "kn"),
|
||||
Triple("", "한국어", "ko"),
|
||||
Triple("", "lietuvių kalba", "lt"),
|
||||
Triple("", "latviešu valoda", "lv"),
|
||||
Triple("", "македонски", "mk"),
|
||||
Triple("", "മലയാളം", "ml"),
|
||||
|
|
|
@ -20,15 +20,17 @@ import com.lagradost.cloudstream3.databinding.LogcatBinding
|
|||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.network.initClient
|
||||
import com.lagradost.cloudstream3.utils.Scheduler.Companion.attachBackupListener
|
||||
import com.lagradost.cloudstream3.services.BackupWorkManager
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
import com.lagradost.cloudstream3.utils.BackupUtils.backup
|
||||
import com.lagradost.cloudstream3.utils.BackupUtils
|
||||
import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getSyncPrefs
|
||||
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||
|
@ -50,7 +52,30 @@ class SettingsUpdates : PreferenceFragmentCompat() {
|
|||
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
|
||||
getPref(R.string.backup_key)?.setOnPreferenceClickListener {
|
||||
activity?.backup()
|
||||
BackupUtils.backup(activity)
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
getPref(R.string.automatic_backup_key)?.setOnPreferenceClickListener {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
|
||||
val prefNames = resources.getStringArray(R.array.periodic_work_names)
|
||||
val prefValues = resources.getIntArray(R.array.periodic_work_values)
|
||||
val current = settingsManager.getInt(getString(R.string.automatic_backup_key), 0)
|
||||
|
||||
activity?.showDialog(
|
||||
prefNames.toList(),
|
||||
prefValues.indexOf(current),
|
||||
getString(R.string.backup_frequency),
|
||||
true,
|
||||
{}) { index ->
|
||||
settingsManager.edit()
|
||||
.putInt(getString(R.string.automatic_backup_key), prefValues[index]).apply()
|
||||
BackupWorkManager.enqueuePeriodicWork(
|
||||
context ?: AcraApplication.context,
|
||||
prefValues[index].toLong()
|
||||
)
|
||||
}
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
|
@ -67,7 +92,7 @@ class SettingsUpdates : PreferenceFragmentCompat() {
|
|||
val builder =
|
||||
AlertDialog.Builder(pref.context, R.style.AlertDialogCustom)
|
||||
|
||||
val binding = LogcatBinding.inflate(layoutInflater,null,false )
|
||||
val binding = LogcatBinding.inflate(layoutInflater, null, false)
|
||||
builder.setView(binding.root)
|
||||
|
||||
val dialog = builder.create()
|
||||
|
@ -179,7 +204,8 @@ class SettingsUpdates : PreferenceFragmentCompat() {
|
|||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context)
|
||||
|
||||
val prefNames = resources.getStringArray(R.array.auto_download_plugin)
|
||||
val prefValues = enumValues<AutoDownloadMode>().sortedBy { x -> x.value }.map { x -> x.value }
|
||||
val prefValues =
|
||||
enumValues<AutoDownloadMode>().sortedBy { x -> x.value }.map { x -> x.value }
|
||||
|
||||
val current = settingsManager.getInt(getString(R.string.auto_download_plugins_key), 0)
|
||||
|
||||
|
@ -189,7 +215,8 @@ class SettingsUpdates : PreferenceFragmentCompat() {
|
|||
getString(R.string.automatic_plugin_download_mode_title),
|
||||
true,
|
||||
{}) {
|
||||
settingsManager.edit().putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply()
|
||||
settingsManager.edit()
|
||||
.putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply()
|
||||
(context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) }
|
||||
}
|
||||
return@setOnPreferenceClickListener true
|
||||
|
|
|
@ -583,7 +583,7 @@ object AppUtils {
|
|||
//private val viewModel: ResultViewModel by activityViewModels()
|
||||
|
||||
private fun getResultsId(): Int {
|
||||
return if (isTrueTvSettings()) {
|
||||
return if (isTvSettings()) {
|
||||
R.id.global_to_navigation_results_tv
|
||||
} else {
|
||||
R.id.global_to_navigation_results_phone
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.annotation.WorkerThread
|
|||
import androidx.fragment.app.FragmentActivity
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterBackupRestoreEvent
|
||||
import com.lagradost.cloudstream3.R
|
||||
|
@ -150,10 +151,12 @@ object BackupUtils {
|
|||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun Context.getBackup(): BackupFile {
|
||||
private fun getBackup(context: Context?): BackupFile? {
|
||||
if (context == null) return null
|
||||
|
||||
val syncDataPrefs = getSyncPrefs().all.filter { it.key.isTransferable() }
|
||||
val allData = getSharedPrefs().all.filter { it.key.isTransferable() }
|
||||
val allSettings = getDefaultSharedPrefs().all.filter { it.key.isTransferable() }
|
||||
val allData = context.getSharedPrefs().all.filter { it.key.isTransferable() }
|
||||
val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() }
|
||||
|
||||
val syncData = BackupVars(
|
||||
syncDataPrefs.filter { it.value is Boolean } as? Map<String, Boolean>,
|
||||
|
@ -226,21 +229,23 @@ object BackupUtils {
|
|||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
fun FragmentActivity.backup() = ioSafe {
|
||||
fun backup(context: Context?) = ioSafe {
|
||||
if (context == null) return@ioSafe
|
||||
|
||||
var fileStream: OutputStream? = null
|
||||
var printStream: PrintWriter? = null
|
||||
try {
|
||||
if (!checkWrite()) {
|
||||
if (!context.checkWrite()) {
|
||||
showToast(R.string.backup_failed, Toast.LENGTH_LONG)
|
||||
requestRW()
|
||||
context.getActivity()?.requestRW()
|
||||
return@ioSafe
|
||||
}
|
||||
|
||||
val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis()))
|
||||
val ext = "txt"
|
||||
val displayName = "CS3_Backup_${date}"
|
||||
val backupFile = getBackup()
|
||||
val stream = setupStream(this@backup, displayName, null, ext, false)
|
||||
val backupFile = getBackup(context)
|
||||
val stream = setupStream(context, displayName, null, ext, false)
|
||||
|
||||
fileStream = stream.openNew()
|
||||
printStream = PrintWriter(fileStream)
|
||||
|
|
|
@ -6,11 +6,14 @@ import android.text.Editable
|
|||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
|
@ -24,19 +27,23 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager
|
|||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.WhoIsWatchingAdapter
|
||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||
import com.lagradost.cloudstream3.ui.account.AccountDialog.showPinInputDialog
|
||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||
import com.lagradost.cloudstream3.ui.result.UiImage
|
||||
import com.lagradost.cloudstream3.ui.result.VideoWatchState
|
||||
import com.lagradost.cloudstream3.ui.result.setImage
|
||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
const val VIDEO_POS_DUR = "video_pos_dur"
|
||||
const val VIDEO_WATCH_STATE = "video_watch_state"
|
||||
const val RESULT_WATCH_STATE = "result_watch_state"
|
||||
const val RESULT_WATCH_STATE_DATA = "result_watch_state_data"
|
||||
const val RESULT_SUBSCRIBED_STATE_DATA = "result_subscribed_state_data"
|
||||
const val RESULT_FAVORITES_STATE_DATA = "result_favorites_state_data"
|
||||
const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // changed due to id changes
|
||||
const val RESULT_RESUME_WATCHING_OLD = "result_resume_watching"
|
||||
const val RESULT_RESUME_WATCHING_HAS_MIGRATED = "result_resume_watching_migrated"
|
||||
|
@ -44,6 +51,28 @@ const val RESULT_EPISODE = "result_episode"
|
|||
const val RESULT_SEASON = "result_season"
|
||||
const val RESULT_DUB = "result_dub"
|
||||
|
||||
|
||||
class UserPreferenceDelegate<T : Any>(
|
||||
private val key: String, private val default: T //, private val klass: KClass<T>
|
||||
) {
|
||||
private val klass: KClass<out T> = default::class
|
||||
private val realKey get() = "${DataStoreHelper.currentAccount}/$key"
|
||||
operator fun getValue(self: Any?, property: KProperty<*>) =
|
||||
AcraApplication.getKeyClass(realKey, klass.java) ?: default
|
||||
|
||||
operator fun setValue(
|
||||
self: Any?,
|
||||
property: KProperty<*>,
|
||||
t: T?
|
||||
) {
|
||||
if (t == null) {
|
||||
removeKey(realKey)
|
||||
} else {
|
||||
AcraApplication.setKeyClass(realKey, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object DataStoreHelper {
|
||||
// be aware, don't change the index of these as Account uses the index for the art
|
||||
private val profileImages = arrayOf(
|
||||
|
@ -56,6 +85,49 @@ object DataStoreHelper {
|
|||
R.drawable.profile_bg_teal
|
||||
)
|
||||
|
||||
private var searchPreferenceProvidersStrings : List<String> by UserPreferenceDelegate(
|
||||
/** java moment right here, as listOf()::class.java != List(0) { "" }::class.java */
|
||||
"search_pref_providers", List(0) { "" }
|
||||
)
|
||||
|
||||
private fun serializeTv(data : List<TvType>) : List<String> = data.map { it.name }
|
||||
|
||||
private fun deserializeTv(data : List<String>) : List<TvType> {
|
||||
return data.mapNotNull { listName ->
|
||||
TvType.values().firstOrNull { it.name == listName }
|
||||
}
|
||||
}
|
||||
|
||||
var searchPreferenceProviders : List<String>
|
||||
get() {
|
||||
val ret = searchPreferenceProvidersStrings
|
||||
return ret.ifEmpty {
|
||||
context?.filterProviderByPreferredMedia()?.map { it.name } ?: emptyList()
|
||||
}
|
||||
} set(value) {
|
||||
searchPreferenceProvidersStrings = value
|
||||
}
|
||||
|
||||
private var searchPreferenceTagsStrings : List<String> by UserPreferenceDelegate("search_pref_tags", listOf(TvType.Movie, TvType.TvSeries).map { it.name })
|
||||
var searchPreferenceTags : List<TvType>
|
||||
get() = deserializeTv(searchPreferenceTagsStrings)
|
||||
set(value) {
|
||||
searchPreferenceTagsStrings = serializeTv(value)
|
||||
}
|
||||
|
||||
|
||||
private var homePreferenceStrings : List<String> by UserPreferenceDelegate("home_pref_homepage", listOf(TvType.Movie, TvType.TvSeries).map { it.name })
|
||||
var homePreference : List<TvType>
|
||||
get() = deserializeTv(homePreferenceStrings)
|
||||
set(value) {
|
||||
homePreferenceStrings = serializeTv(value)
|
||||
}
|
||||
|
||||
var homeBookmarkedList : IntArray by UserPreferenceDelegate("home_bookmarked_last_list", IntArray(0))
|
||||
var playBackSpeed : Float by UserPreferenceDelegate("playback_speed", 1.0f)
|
||||
var resizeMode : Int by UserPreferenceDelegate("resize_mode", 0)
|
||||
var librarySortingMode : Int by UserPreferenceDelegate("library_sorting_mode", ListSorting.AlphabeticalA.ordinal)
|
||||
|
||||
data class Account(
|
||||
@JsonProperty("keyIndex")
|
||||
val keyIndex: Int,
|
||||
|
@ -65,6 +137,8 @@ object DataStoreHelper {
|
|||
val customImage: String? = null,
|
||||
@JsonProperty("defaultImageIndex")
|
||||
val defaultImageIndex: Int,
|
||||
@JsonProperty("lockPin")
|
||||
val lockPin: String? = null,
|
||||
) {
|
||||
val image: UiImage
|
||||
get() = customImage?.let { UiImage.Image(it) } ?: UiImage.Drawable(
|
||||
|
@ -131,7 +205,6 @@ object DataStoreHelper {
|
|||
|
||||
// update UI
|
||||
setAccount(getDefaultAccount(context), true)
|
||||
MainActivity.bookmarksUpdatedEvent(true)
|
||||
dialog?.dismissSafe()
|
||||
}
|
||||
|
||||
|
@ -160,36 +233,86 @@ object DataStoreHelper {
|
|||
|
||||
binding.profilePic.setImage(account.image)
|
||||
binding.profilePic.setOnClickListener {
|
||||
// rolls the image forwards once
|
||||
// Roll the image forwards once
|
||||
currentEditAccount =
|
||||
currentEditAccount.copy(defaultImageIndex = (currentEditAccount.defaultImageIndex + 1) % profileImages.size)
|
||||
binding.profilePic.setImage(currentEditAccount.image)
|
||||
}
|
||||
|
||||
binding.applyBtt.setOnClickListener {
|
||||
val currentAccounts = accounts.toMutableList()
|
||||
|
||||
val overrideIndex =
|
||||
currentAccounts.indexOfFirst { it.keyIndex == currentEditAccount.keyIndex }
|
||||
|
||||
// if an account is found that has the same keyIndex then override that one, if not then append it
|
||||
if (overrideIndex != -1) {
|
||||
currentAccounts[overrideIndex] = currentEditAccount
|
||||
if (currentEditAccount.lockPin != null) {
|
||||
// Ask for the current PIN
|
||||
showPinInputDialog(context, currentEditAccount.lockPin, false) { pin ->
|
||||
if (pin == null) return@showPinInputDialog
|
||||
// PIN is correct, proceed to update the account
|
||||
performAccountUpdate(currentEditAccount)
|
||||
dialog.dismissSafe()
|
||||
}
|
||||
} else {
|
||||
currentAccounts.add(currentEditAccount)
|
||||
// No lock PIN set, proceed to update the account
|
||||
performAccountUpdate(currentEditAccount)
|
||||
dialog.dismissSafe()
|
||||
}
|
||||
|
||||
// Save the current homepage for new accounts
|
||||
val currentHomePage = DataStoreHelper.currentHomePage
|
||||
|
||||
// set the new default account as well as add the key for the new account
|
||||
setAccount(currentEditAccount, false)
|
||||
DataStoreHelper.currentHomePage = currentHomePage
|
||||
|
||||
accounts = currentAccounts.toTypedArray()
|
||||
|
||||
dialog.dismissSafe()
|
||||
}
|
||||
|
||||
// Handle setting or changing the PIN
|
||||
|
||||
if (currentEditAccount.keyIndex == getDefaultAccount(context).keyIndex) {
|
||||
binding.lockProfileCheckbox.isVisible = false
|
||||
if (currentEditAccount.lockPin != null) {
|
||||
currentEditAccount = currentEditAccount.copy(lockPin = null)
|
||||
}
|
||||
}
|
||||
|
||||
var canSetPin = true
|
||||
|
||||
binding.lockProfileCheckbox.isChecked = currentEditAccount.lockPin != null
|
||||
|
||||
binding.lockProfileCheckbox.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
if (canSetPin) {
|
||||
showPinInputDialog(context, null, true) { pin ->
|
||||
if (pin == null) {
|
||||
binding.lockProfileCheckbox.isChecked = false
|
||||
return@showPinInputDialog
|
||||
}
|
||||
|
||||
currentEditAccount = currentEditAccount.copy(lockPin = pin)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (currentEditAccount.lockPin != null) {
|
||||
// Ask for the current PIN
|
||||
showPinInputDialog(context, currentEditAccount.lockPin, true) { pin ->
|
||||
if (pin == null || pin != currentEditAccount.lockPin) {
|
||||
canSetPin = false
|
||||
binding.lockProfileCheckbox.isChecked = true
|
||||
} else {
|
||||
currentEditAccount = currentEditAccount.copy(lockPin = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canSetPin = true
|
||||
}
|
||||
|
||||
private fun performAccountUpdate(account: Account) {
|
||||
val currentAccounts = accounts.toMutableList()
|
||||
|
||||
val overrideIndex = currentAccounts.indexOfFirst { it.keyIndex == account.keyIndex }
|
||||
|
||||
if (overrideIndex != -1) {
|
||||
currentAccounts[overrideIndex] = account
|
||||
} else {
|
||||
currentAccounts.add(account)
|
||||
}
|
||||
|
||||
val currentHomePage = this.currentHomePage
|
||||
setAccount(account, false)
|
||||
this.currentHomePage = currentHomePage
|
||||
accounts = currentAccounts.toTypedArray()
|
||||
}
|
||||
|
||||
private fun getDefaultAccount(context: Context): Account {
|
||||
|
@ -202,10 +325,18 @@ object DataStoreHelper {
|
|||
}
|
||||
}
|
||||
|
||||
fun getAccounts(context: Context): List<Account> {
|
||||
return accounts.toMutableList().apply {
|
||||
val item = getDefaultAccount(context)
|
||||
remove(item)
|
||||
add(0, item)
|
||||
}
|
||||
}
|
||||
|
||||
fun showWhoIsWatching(context: Context) {
|
||||
val binding: WhoIsWatchingBinding = WhoIsWatchingBinding.inflate(
|
||||
LayoutInflater.from(context)
|
||||
)
|
||||
val binding: WhoIsWatchingBinding = WhoIsWatchingBinding.inflate(LayoutInflater.from(context))
|
||||
val builder = BottomSheetDialog(context)
|
||||
builder.setContentView(binding.root)
|
||||
|
||||
val showAccount = accounts.toMutableList().apply {
|
||||
val item = getDefaultAccount(context)
|
||||
|
@ -213,22 +344,25 @@ object DataStoreHelper {
|
|||
add(0, item)
|
||||
}
|
||||
|
||||
val builder =
|
||||
BottomSheetDialog(context)
|
||||
builder.setContentView(binding.root)
|
||||
val accountName = context.getString(R.string.account)
|
||||
|
||||
binding.profilesRecyclerview.setLinearListLayout(
|
||||
isHorizontal = true,
|
||||
nextUp = FOCUS_SELF,
|
||||
nextDown = FOCUS_SELF,
|
||||
nextLeft = FOCUS_SELF,
|
||||
nextRight = FOCUS_SELF
|
||||
)
|
||||
binding.profilesRecyclerview.setLinearListLayout(isHorizontal = true)
|
||||
binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter(
|
||||
selectCallBack = { account ->
|
||||
setAccount(account, true)
|
||||
builder.dismissSafe()
|
||||
// Check if the selected account has a lock PIN set
|
||||
if (account.lockPin != null) {
|
||||
// Prompt for the lock pin
|
||||
showPinInputDialog(context, account.lockPin, false) { pin ->
|
||||
if (pin == null) return@showPinInputDialog
|
||||
// Pin is correct, unlock the profile
|
||||
setAccount(account, true)
|
||||
builder.dismissSafe()
|
||||
}
|
||||
} else {
|
||||
// No lock PIN set, directly set the account
|
||||
setAccount(account, true)
|
||||
builder.dismissSafe()
|
||||
}
|
||||
},
|
||||
addAccountCallback = {
|
||||
val currentAccounts = accounts
|
||||
|
@ -264,7 +398,6 @@ object DataStoreHelper {
|
|||
builder.show()
|
||||
}
|
||||
|
||||
|
||||
data class PosDur(
|
||||
@JsonProperty("position") val position: Long,
|
||||
@JsonProperty("duration") val duration: Long
|
||||
|
@ -282,20 +415,35 @@ object DataStoreHelper {
|
|||
/**
|
||||
* Used to display notifications on new episodes and posters in library.
|
||||
**/
|
||||
data class SubscribedData(
|
||||
abstract class LibrarySearchResponse(
|
||||
@JsonProperty("id") override var id: Int?,
|
||||
@JsonProperty("subscribedTime") val bookmarkedTime: Long,
|
||||
@JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long,
|
||||
@JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map<DubStatus, Int?>,
|
||||
@JsonProperty("latestUpdatedTime") open val latestUpdatedTime: Long,
|
||||
@JsonProperty("name") override val name: String,
|
||||
@JsonProperty("url") override val url: String,
|
||||
@JsonProperty("apiName") override val apiName: String,
|
||||
@JsonProperty("type") override var type: TvType? = null,
|
||||
@JsonProperty("type") override var type: TvType?,
|
||||
@JsonProperty("posterUrl") override var posterUrl: String?,
|
||||
@JsonProperty("year") val year: Int?,
|
||||
@JsonProperty("quality") override var quality: SearchQuality? = null,
|
||||
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
|
||||
) : SearchResponse {
|
||||
@JsonProperty("year") open val year: Int?,
|
||||
@JsonProperty("syncData") open val syncData: Map<String, String>?,
|
||||
@JsonProperty("quality") override var quality: SearchQuality?,
|
||||
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>?
|
||||
) : SearchResponse
|
||||
|
||||
data class SubscribedData(
|
||||
@JsonProperty("subscribedTime") val subscribedTime: Long,
|
||||
@JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map<DubStatus, Int?>,
|
||||
override var id: Int?,
|
||||
override val latestUpdatedTime: Long,
|
||||
override val name: String,
|
||||
override val url: String,
|
||||
override val apiName: String,
|
||||
override var type: TvType?,
|
||||
override var posterUrl: String?,
|
||||
override val year: Int?,
|
||||
override val syncData: Map<String, String>? = null,
|
||||
override var quality: SearchQuality? = null,
|
||||
override var posterHeaders: Map<String, String>? = null
|
||||
) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) {
|
||||
fun toLibraryItem(): SyncAPI.LibraryItem? {
|
||||
return SyncAPI.LibraryItem(
|
||||
name,
|
||||
|
@ -311,18 +459,19 @@ object DataStoreHelper {
|
|||
}
|
||||
|
||||
data class BookmarkedData(
|
||||
@JsonProperty("id") override var id: Int?,
|
||||
@JsonProperty("bookmarkedTime") val bookmarkedTime: Long,
|
||||
@JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long,
|
||||
@JsonProperty("name") override val name: String,
|
||||
@JsonProperty("url") override val url: String,
|
||||
@JsonProperty("apiName") override val apiName: String,
|
||||
@JsonProperty("type") override var type: TvType? = null,
|
||||
@JsonProperty("posterUrl") override var posterUrl: String?,
|
||||
@JsonProperty("year") val year: Int?,
|
||||
@JsonProperty("quality") override var quality: SearchQuality? = null,
|
||||
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
|
||||
) : SearchResponse {
|
||||
override var id: Int?,
|
||||
override val latestUpdatedTime: Long,
|
||||
override val name: String,
|
||||
override val url: String,
|
||||
override val apiName: String,
|
||||
override var type: TvType?,
|
||||
override var posterUrl: String?,
|
||||
override val year: Int?,
|
||||
override val syncData: Map<String, String>? = null,
|
||||
override var quality: SearchQuality? = null,
|
||||
override var posterHeaders: Map<String, String>? = null
|
||||
) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) {
|
||||
fun toLibraryItem(id: String): SyncAPI.LibraryItem {
|
||||
return SyncAPI.LibraryItem(
|
||||
name,
|
||||
|
@ -337,6 +486,34 @@ object DataStoreHelper {
|
|||
}
|
||||
}
|
||||
|
||||
data class FavoritesData(
|
||||
@JsonProperty("favoritesTime") val favoritesTime: Long,
|
||||
override var id: Int?,
|
||||
override val latestUpdatedTime: Long,
|
||||
override val name: String,
|
||||
override val url: String,
|
||||
override val apiName: String,
|
||||
override var type: TvType?,
|
||||
override var posterUrl: String?,
|
||||
override val year: Int?,
|
||||
override val syncData: Map<String, String>? = null,
|
||||
override var quality: SearchQuality? = null,
|
||||
override var posterHeaders: Map<String, String>? = null
|
||||
) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) {
|
||||
fun toLibraryItem(): SyncAPI.LibraryItem? {
|
||||
return SyncAPI.LibraryItem(
|
||||
name,
|
||||
url,
|
||||
id?.toString() ?: return null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
latestUpdatedTime,
|
||||
apiName, type, posterUrl, posterHeaders, quality, this.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ResumeWatchingResult(
|
||||
@JsonProperty("name") override val name: String,
|
||||
@JsonProperty("url") override val url: String,
|
||||
|
@ -369,15 +546,9 @@ object DataStoreHelper {
|
|||
removeKeys(folder)
|
||||
}
|
||||
|
||||
fun deleteAllBookmarkedData() {
|
||||
val folder1 = "$currentAccount/$RESULT_WATCH_STATE"
|
||||
val folder2 = "$currentAccount/$RESULT_WATCH_STATE_DATA"
|
||||
removeKeys(folder1)
|
||||
removeKeys(folder2)
|
||||
}
|
||||
|
||||
fun deleteBookmarkedData(id: Int?) {
|
||||
if (id == null) return
|
||||
AccountManager.localListApi.requireLibraryRefresh = true
|
||||
removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString())
|
||||
removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString())
|
||||
}
|
||||
|
@ -475,6 +646,12 @@ object DataStoreHelper {
|
|||
return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString())
|
||||
}
|
||||
|
||||
fun getAllBookmarkedData(): List<BookmarkedData> {
|
||||
return getKeys("$currentAccount/$RESULT_WATCH_STATE_DATA")?.mapNotNull {
|
||||
getKey(it)
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
fun getAllSubscriptions(): List<SubscribedData> {
|
||||
return getKeys("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA")?.mapNotNull {
|
||||
getKey(it)
|
||||
|
@ -510,6 +687,29 @@ object DataStoreHelper {
|
|||
return getKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString())
|
||||
}
|
||||
|
||||
fun getAllFavorites(): List<FavoritesData> {
|
||||
return getKeys("$currentAccount/$RESULT_FAVORITES_STATE_DATA")?.mapNotNull {
|
||||
getKey(it)
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
fun removeFavoritesData(id: Int?) {
|
||||
if (id == null) return
|
||||
AccountManager.localListApi.requireLibraryRefresh = true
|
||||
removeKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString())
|
||||
}
|
||||
|
||||
fun setFavoritesData(id: Int?, data: FavoritesData) {
|
||||
if (id == null) return
|
||||
setKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString(), data)
|
||||
AccountManager.localListApi.requireLibraryRefresh = true
|
||||
}
|
||||
|
||||
fun getFavoritesData(id: Int?): FavoritesData? {
|
||||
if (id == null) return null
|
||||
return getKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString())
|
||||
}
|
||||
|
||||
fun setViewPos(id: Int?, pos: Long, dur: Long) {
|
||||
if (id == null) return
|
||||
if (dur < 30_000) return // too short
|
||||
|
|
|
@ -82,6 +82,7 @@ import com.lagradost.cloudstream3.extractors.Maxstream
|
|||
import com.lagradost.cloudstream3.extractors.Mcloud
|
||||
import com.lagradost.cloudstream3.extractors.Megacloud
|
||||
import com.lagradost.cloudstream3.extractors.Meownime
|
||||
import com.lagradost.cloudstream3.extractors.Minoplres
|
||||
import com.lagradost.cloudstream3.extractors.MixDrop
|
||||
import com.lagradost.cloudstream3.extractors.MixDropBz
|
||||
import com.lagradost.cloudstream3.extractors.MixDropCh
|
||||
|
@ -118,9 +119,6 @@ import com.lagradost.cloudstream3.extractors.Sbthe
|
|||
import com.lagradost.cloudstream3.extractors.Sendvid
|
||||
import com.lagradost.cloudstream3.extractors.ShaveTape
|
||||
import com.lagradost.cloudstream3.extractors.Solidfiles
|
||||
import com.lagradost.cloudstream3.extractors.SpeedoStream
|
||||
import com.lagradost.cloudstream3.extractors.SpeedoStream1
|
||||
import com.lagradost.cloudstream3.extractors.SpeedoStream2
|
||||
import com.lagradost.cloudstream3.extractors.Ssbstream
|
||||
import com.lagradost.cloudstream3.extractors.StreamM4u
|
||||
import com.lagradost.cloudstream3.extractors.StreamSB
|
||||
|
@ -380,6 +378,15 @@ open class ExtractorLink constructor(
|
|||
val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8
|
||||
val isDash : Boolean get() = type == ExtractorLinkType.DASH
|
||||
|
||||
fun getAllHeaders() : Map<String, String> {
|
||||
if (referer.isBlank()) {
|
||||
return headers
|
||||
} else if (headers.keys.none { it.equals("referer", ignoreCase = true) }) {
|
||||
return headers + mapOf("referer" to referer)
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
constructor(
|
||||
source: String,
|
||||
name: String,
|
||||
|
@ -748,9 +755,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
|||
Vido(),
|
||||
Linkbox(),
|
||||
Acefile(),
|
||||
SpeedoStream(),
|
||||
SpeedoStream1(),
|
||||
SpeedoStream2(),
|
||||
Minoplres(), // formerly SpeedoStream
|
||||
Zorofile(),
|
||||
Embedgram(),
|
||||
Mvidoo(),
|
||||
|
|
|
@ -8,6 +8,7 @@ import com.bumptech.glide.Registry
|
|||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
|
@ -27,6 +28,10 @@ class GlideModule : AppGlideModule() {
|
|||
RequestOptions()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.signature(ObjectKey(System.currentTimeMillis().toShort()))
|
||||
}.setDiskCache {
|
||||
// Possible to make this a setting in the future.
|
||||
val memoryCacheSizeBytes: Long = 1024 * 1024 * 100; // 100mb
|
||||
InternalCacheDiskCacheFactory(context, memoryCacheSizeBytes).build()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ object M3u8Helper2 {
|
|||
private val QUALITY_REGEX =
|
||||
Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""")
|
||||
private val TS_EXTENSION_REGEX =
|
||||
Regex("""#EXTINF:.*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways
|
||||
Regex("""#EXTINF:(([0-9]*[.])?[0-9]+|).*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways
|
||||
//Regex("""(.*\.(ts|jpg|html).*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts
|
||||
|
||||
private fun absoluteExtensionDetermination(url: String): String? {
|
||||
|
@ -115,13 +115,22 @@ object M3u8Helper2 {
|
|||
|
||||
private fun selectBest(qualities: List<M3u8Helper.M3u8Stream>): M3u8Helper.M3u8Stream? {
|
||||
val result = qualities.sortedBy {
|
||||
if (it.quality != null && it.quality <= 1080) it.quality else 0
|
||||
}.filter {
|
||||
it.quality ?: Qualities.Unknown.value //if (it.quality != null && it.quality <= 1080) else 0
|
||||
}/*.filter {
|
||||
listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl))
|
||||
}
|
||||
}*/
|
||||
return result.lastOrNull()
|
||||
}
|
||||
|
||||
private fun selectWorst(qualities: List<M3u8Helper.M3u8Stream>): M3u8Helper.M3u8Stream? {
|
||||
val result = qualities.sortedBy {
|
||||
it.quality ?: Qualities.Unknown.value //if (it.quality != null && it.quality <= 1080) else 0
|
||||
}/*.filter {
|
||||
listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl))
|
||||
}*/
|
||||
return result.firstOrNull()
|
||||
}
|
||||
|
||||
private fun getParentLink(uri: String): String {
|
||||
val split = uri.split("/").toMutableList()
|
||||
split.removeLast()
|
||||
|
@ -173,14 +182,20 @@ object M3u8Helper2 {
|
|||
return list
|
||||
}
|
||||
|
||||
data class TsLink(
|
||||
val url : String,
|
||||
val time : Double?,
|
||||
)
|
||||
|
||||
data class LazyHlsDownloadData(
|
||||
private val encryptionData: ByteArray,
|
||||
private val encryptionIv: ByteArray,
|
||||
private val isEncrypted: Boolean,
|
||||
private val allTsLinks: List<String>,
|
||||
private val relativeUrl: String,
|
||||
private val headers: Map<String, String>,
|
||||
val isEncrypted: Boolean,
|
||||
val allTsLinks: List<TsLink>,
|
||||
val relativeUrl: String,
|
||||
val headers: Map<String, String>,
|
||||
) {
|
||||
|
||||
val size get() = allTsLinks.size
|
||||
|
||||
suspend fun resolveLinkWhileSafe(
|
||||
|
@ -228,9 +243,9 @@ object M3u8Helper2 {
|
|||
@Throws
|
||||
suspend fun resolveLink(index: Int): ByteArray {
|
||||
if (index < 0 || index >= size) throw IllegalArgumentException("index must be in the bounds of the ts")
|
||||
val url = allTsLinks[index]
|
||||
val ts = allTsLinks[index]
|
||||
|
||||
val tsResponse = app.get(url, headers = headers, verify = false)
|
||||
val tsResponse = app.get(ts.url, headers = headers, verify = false)
|
||||
val tsData = tsResponse.body.bytes()
|
||||
if (tsData.isEmpty()) throw ErrorLoadingException("no data")
|
||||
|
||||
|
@ -244,15 +259,16 @@ object M3u8Helper2 {
|
|||
|
||||
@Throws
|
||||
suspend fun hslLazy(
|
||||
qualities: List<M3u8Helper.M3u8Stream>
|
||||
qualities: List<M3u8Helper.M3u8Stream>, selectBest : Boolean = true
|
||||
): LazyHlsDownloadData {
|
||||
if (qualities.isEmpty()) throw IllegalArgumentException("qualities must be non empty")
|
||||
val selected = selectBest(qualities) ?: qualities.first()
|
||||
val selected = if(selectBest) { selectBest(qualities) } else { selectWorst(qualities) } ?: qualities.first()
|
||||
val headers = selected.headers
|
||||
val streams = qualities.map { m3u8Generation(it, false) }.flatten()
|
||||
// this selects the best quality of the qualities offered,
|
||||
// due to the recursive nature of m3u8, we only go 2 depth
|
||||
val secondSelection = selectBest(streams.ifEmpty { listOf(selected) })
|
||||
val innerStreams = streams.ifEmpty { listOf(selected) }
|
||||
val secondSelection = if(selectBest) { selectBest(innerStreams) } else { selectWorst(innerStreams) }
|
||||
?: throw IllegalArgumentException("qualities has no streams")
|
||||
|
||||
val m3u8Response =
|
||||
|
@ -285,12 +301,14 @@ object M3u8Helper2 {
|
|||
}
|
||||
val relativeUrl = getParentLink(secondSelection.streamUrl)
|
||||
val allTsList = TS_EXTENSION_REGEX.findAll(m3u8Response + "\n").map { ts ->
|
||||
val value = ts.groupValues[1]
|
||||
if (isNotCompleteUrl(value)) {
|
||||
val time = ts.groupValues[1]
|
||||
val value = ts.groupValues[3]
|
||||
val url = if (isNotCompleteUrl(value)) {
|
||||
"$relativeUrl/${value}"
|
||||
} else {
|
||||
value
|
||||
}
|
||||
TsLink(url = url, time = time.toDoubleOrNull())
|
||||
}.toList()
|
||||
if (allTsList.isEmpty()) throw IllegalArgumentException("ts must be non empty")
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
|
@ -54,7 +54,11 @@ class PackageInstallerService : Service() {
|
|||
UPDATE_CHANNEL_NAME,
|
||||
UPDATE_CHANNEL_DESCRIPTION
|
||||
)
|
||||
startForeground(UPDATE_NOTIFICATION_ID, baseNotification.build())
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(UPDATE_NOTIFICATION_ID, baseNotification.build(), FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} else{
|
||||
startForeground(UPDATE_NOTIFICATION_ID, baseNotification.build())
|
||||
}
|
||||
}
|
||||
|
||||
private val updateLock = Mutex()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,14 +2,12 @@ package com.lagradost.cloudstream3.utils
|
|||
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.text.Spanned
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.AbsListView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isGone
|
||||
|
@ -19,7 +17,10 @@ import androidx.core.view.marginRight
|
|||
import androidx.core.view.marginTop
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.BottomInputDialogBinding
|
||||
import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding
|
||||
import com.lagradost.cloudstream3.databinding.BottomTextDialogBinding
|
||||
import com.lagradost.cloudstream3.databinding.OptionsPopupTvBinding
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
|
||||
|
@ -54,14 +55,14 @@ object SingleSelectionHelper {
|
|||
if (this == null) return
|
||||
|
||||
if (isTvSettings()) {
|
||||
val builder =
|
||||
AlertDialog.Builder(this, R.style.AlertDialogCustom)
|
||||
.setView(R.layout.options_popup_tv)
|
||||
val binding = OptionsPopupTvBinding.inflate(layoutInflater)
|
||||
val dialog = AlertDialog.Builder(this, R.style.AlertDialogCustom)
|
||||
.setView(binding.root)
|
||||
.create()
|
||||
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
|
||||
dialog.findViewById<ListView>(R.id.listview1)?.let { listView ->
|
||||
binding.listview1.let { listView ->
|
||||
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
listView.adapter =
|
||||
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice_color).apply {
|
||||
|
@ -74,7 +75,7 @@ object SingleSelectionHelper {
|
|||
}
|
||||
}
|
||||
|
||||
dialog.findViewById<ImageView>(R.id.imageView)?.apply {
|
||||
binding.imageView.apply {
|
||||
isGone = poster.isNullOrEmpty()
|
||||
setImage(poster)
|
||||
}
|
||||
|
@ -105,12 +106,12 @@ object SingleSelectionHelper {
|
|||
if (this == null) return
|
||||
|
||||
val realShowApply = showApply || isMultiSelect
|
||||
val listView = binding.listview1//.findViewById<ListView>(R.id.listview1)!!
|
||||
val textView = binding.text1//.findViewById<TextView>(R.id.text1)!!
|
||||
val applyButton = binding.applyBtt//.findViewById<TextView>(R.id.apply_btt)
|
||||
val cancelButton = binding.cancelBtt//findViewById<TextView>(R.id.cancel_btt)
|
||||
val listView = binding.listview1
|
||||
val textView = binding.text1
|
||||
val applyButton = binding.applyBtt
|
||||
val cancelButton = binding.cancelBtt
|
||||
val applyHolder =
|
||||
binding.applyBttHolder//.findViewById<LinearLayout>(R.id.apply_btt_holder)
|
||||
binding.applyBttHolder
|
||||
|
||||
applyHolder.isVisible = realShowApply
|
||||
if (!realShowApply) {
|
||||
|
@ -173,8 +174,8 @@ object SingleSelectionHelper {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private fun Activity?.showInputDialog(
|
||||
binding: BottomInputDialogBinding,
|
||||
dialog: Dialog,
|
||||
value: String,
|
||||
name: String,
|
||||
|
@ -184,11 +185,11 @@ object SingleSelectionHelper {
|
|||
) {
|
||||
if (this == null) return
|
||||
|
||||
val inputView = dialog.findViewById<EditText>(R.id.nginx_text_input)!!
|
||||
val textView = dialog.findViewById<TextView>(R.id.text1)!!
|
||||
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
|
||||
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!!
|
||||
val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!!
|
||||
val inputView = binding.nginxTextInput
|
||||
val textView = binding.text1
|
||||
val applyButton = binding.applyBtt
|
||||
val cancelButton = binding.cancelBtt
|
||||
val applyHolder = binding.applyBttHolder
|
||||
|
||||
applyHolder.isVisible = true
|
||||
textView.text = name
|
||||
|
@ -350,11 +351,17 @@ object SingleSelectionHelper {
|
|||
dismissCallback: () -> Unit,
|
||||
callback: (String) -> Unit,
|
||||
) {
|
||||
val builder = BottomSheetDialog(this) // probably the stuff at the bottom
|
||||
builder.setContentView(R.layout.bottom_input_dialog) // input layout
|
||||
val builder = BottomSheetDialog(this)
|
||||
|
||||
val binding: BottomInputDialogBinding = BottomInputDialogBinding.inflate(
|
||||
LayoutInflater.from(this)
|
||||
)
|
||||
|
||||
builder.setContentView(binding.root)
|
||||
|
||||
builder.show()
|
||||
showInputDialog(
|
||||
binding,
|
||||
builder,
|
||||
value,
|
||||
name,
|
||||
|
@ -363,4 +370,24 @@ object SingleSelectionHelper {
|
|||
dismissCallback
|
||||
)
|
||||
}
|
||||
|
||||
fun Activity.showBottomDialogText(
|
||||
title: String,
|
||||
text: Spanned,
|
||||
dismissCallback: () -> Unit
|
||||
) {
|
||||
val binding = BottomTextDialogBinding.inflate(layoutInflater)
|
||||
val dialog = BottomSheetDialog(this)
|
||||
|
||||
dialog.setContentView(binding.root)
|
||||
|
||||
binding.dialogTitle.text = title
|
||||
binding.dialogText.text = text
|
||||
|
||||
dialog.setOnDismissListener {
|
||||
dismissCallback.invoke()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ object UIHelper {
|
|||
val Int.toDp: Int get() = (this / Resources.getSystem().displayMetrics.density).toInt()
|
||||
val Float.toDp: Float get() = (this / Resources.getSystem().displayMetrics.density)
|
||||
|
||||
fun Activity.checkWrite(): Boolean {
|
||||
fun Context.checkWrite(): Boolean {
|
||||
return (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
|
@ -178,9 +178,10 @@ object UIHelper {
|
|||
fun Activity?.navigate(@IdRes navigation: Int, arguments: Bundle? = null) {
|
||||
try {
|
||||
if (this is FragmentActivity) {
|
||||
(supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment?)?.navController?.navigate(
|
||||
navigation, arguments
|
||||
)
|
||||
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment?
|
||||
navHostFragment?.navController?.let {
|
||||
it.navigate(navigation, arguments)
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
|
|
|
@ -2,11 +2,9 @@
|
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<stroke android:width="2dp"
|
||||
android:color="?attr/white"/>
|
||||
<!-- <corners
|
||||
<corners
|
||||
android:bottomLeftRadius="@dimen/rounded_image_radius"
|
||||
android:bottomRightRadius="@dimen/rounded_image_radius"
|
||||
android:topLeftRadius="@dimen/rounded_image_radius"
|
||||
android:topRightRadius="@dimen/rounded_image_radius" />
|
||||
-->
|
||||
|
||||
</shape>
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!--<item android:state_focused="true"
|
||||
android:drawable="@drawable/outline"/>--> <!-- focused -->
|
||||
<item android:state_focused="true"
|
||||
android:drawable="@drawable/outline"/> <!-- focused -->
|
||||
</selector>
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!--<item android:state_focused="true" android:drawable="@drawable/outline_less" />--> <!-- focused -->
|
||||
<item android:state_focused="true" android:drawable="@drawable/outline_less" /> <!-- focused -->
|
||||
</selector>
|
10
app/src/main/res/drawable/video_frame.xml
Normal file
10
app/src/main/res/drawable/video_frame.xml
Normal 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>
|
56
app/src/main/res/layout/account_list_item.xml
Normal file
56
app/src/main/res/layout/account_list_item.xml
Normal 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>
|
28
app/src/main/res/layout/activity_account_select.xml
Normal file
28
app/src/main/res/layout/activity_account_select.xml
Normal 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>
|
27
app/src/main/res/layout/activity_account_select_tv.xml
Normal file
27
app/src/main/res/layout/activity_account_select_tv.xml
Normal 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>
|
33
app/src/main/res/layout/bottom_text_dialog.xml
Normal file
33
app/src/main/res/layout/bottom_text_dialog.xml
Normal 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>
|
|
@ -9,7 +9,7 @@
|
|||
android:layout_margin="5dp"
|
||||
android:foreground="@drawable/outline_drawable"
|
||||
app:cardBackgroundColor="@color/transparent"
|
||||
|
||||
android:focusable="true"
|
||||
app:cardCornerRadius="@dimen/rounded_image_radius"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
|||
android:layout_width="100dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:focusable="true"
|
||||
android:padding="5dp">
|
||||
<!--app:cardCornerRadius="@dimen/roundedImageRadius"-->
|
||||
<FrameLayout
|
||||
|
@ -34,7 +35,6 @@
|
|||
|
||||
android:id="@+id/actor_image"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/episode_poster_img_des"
|
||||
android:scaleType="centerCrop"
|
||||
|
|
|
@ -137,6 +137,7 @@
|
|||
android:background="?attr/primaryBlackBackground"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:tag = "@string/tv_no_focus_tag"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:listitem="@layout/download_header_episode" />
|
||||
|
||||
|
|
|
@ -92,15 +92,7 @@
|
|||
android:layout_height="100dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="20dp">
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/home_preview_bookmark"
|
||||
|
@ -139,7 +131,7 @@
|
|||
app:drawableTint="?attr/white"
|
||||
app:drawableTopCompat="@drawable/ic_outline_info_24"
|
||||
app:tint="?attr/white" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
android:layout_gravity="end"
|
||||
android:background="@drawable/player_button_tv_attr_no_bg"
|
||||
android:contentDescription="@string/search"
|
||||
android:focusable="true"
|
||||
android:nextFocusLeft="@id/home_preview_change_api"
|
||||
android:nextFocusRight="@id/home_preview_switch_account"
|
||||
android:nextFocusDown="@id/home_preview_info_btt"
|
||||
|
@ -70,6 +71,7 @@
|
|||
android:layout_gravity="end"
|
||||
android:background="@drawable/player_button_tv_attr_no_bg"
|
||||
android:contentDescription="@string/account"
|
||||
android:focusable="true"
|
||||
android:nextFocusLeft="@id/home_preview_search_button"
|
||||
android:nextFocusRight="@id/home_preview_switch_account"
|
||||
android:nextFocusDown="@id/home_preview_info_btt"
|
||||
|
@ -230,7 +232,9 @@
|
|||
android:layout_marginStart="@dimen/navbar_width"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:padding="12dp"
|
||||
android:text="@string/continue_watching" />
|
||||
android:text="@string/continue_watching"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
app:drawableTint="?attr/white" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/home_watch_child_recyclerview"
|
||||
|
@ -256,7 +260,15 @@
|
|||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/home_bookmark_parent_item_title"
|
||||
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/horizontal_scroll_chips"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fadingEdge="horizontal"
|
||||
|
@ -342,6 +354,18 @@
|
|||
</com.google.android.material.chip.ChipGroup>
|
||||
</HorizontalScrollView>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/home_bookmark_parent_item_more_info"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:contentDescription="@string/home_more_info"
|
||||
android:src="@drawable/ic_baseline_arrow_forward_24"
|
||||
android:visibility="gone"
|
||||
app:drawableTint="?attr/white" />
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/home_bookmarked_child_recyclerview"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -140,6 +140,7 @@
|
|||
android:layout_gravity="end"
|
||||
android:background="@drawable/player_button_tv_attr_no_bg"
|
||||
android:contentDescription="@string/account"
|
||||
android:focusable="true"
|
||||
android:nextFocusLeft="@id/home_preview_search_button"
|
||||
android:nextFocusRight="@id/home_switch_account"
|
||||
android:nextFocusDown="@id/home_change_api"
|
||||
|
|
|
@ -41,6 +41,20 @@
|
|||
android:src="@drawable/ic_baseline_extension_24"
|
||||
app:tint="?attr/textColor" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/library_sort"
|
||||
android:layout_width="45dp"
|
||||
android:layout_height="45dp"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_marginStart="10dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/change_providers_img_des"
|
||||
android:padding="5dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:src="@drawable/ic_baseline_sort_24"
|
||||
app:tint="?attr/textColor" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
|
@ -54,6 +68,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="25dp"
|
||||
|
||||
android:iconifiedByDefault="false"
|
||||
android:imeOptions="actionSearch"
|
||||
|
@ -67,6 +82,7 @@
|
|||
app:queryBackground="@color/transparent"
|
||||
app:queryHint="@string/search_hint"
|
||||
app:searchIcon="@drawable/search_icon"
|
||||
app:closeIcon="@drawable/ic_baseline_close_24"
|
||||
tools:ignore="RtlSymmetry">
|
||||
|
||||
</androidx.appcompat.widget.SearchView>
|
||||
|
@ -160,8 +176,7 @@
|
|||
android:layout_height="40dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?attr/primaryGrayBackground"
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
android:focusable="false"
|
||||
android:focusable="true"
|
||||
android:paddingHorizontal="5dp"
|
||||
app:layout_scrollFlags="noScroll"
|
||||
app:tabGravity="center"
|
||||
|
|
204
app/src/main/res/layout/fragment_library_tv.xml
Normal file
204
app/src/main/res/layout/fragment_library_tv.xml
Normal 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>
|
|
@ -875,7 +875,7 @@
|
|||
android:descendantFocusability="afterDescendants"
|
||||
android:paddingBottom="100dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/result_episode_both_tv" />
|
||||
tools:listitem="@layout/result_episode" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
android:nextFocusUp="@id/result_back"
|
||||
android:nextFocusDown="@id/result_description"
|
||||
android:nextFocusLeft="@id/result_add_sync"
|
||||
android:nextFocusRight="@id/result_share"
|
||||
android:nextFocusRight="@id/result_favorite"
|
||||
|
||||
tools:visibility="visible"
|
||||
|
||||
|
@ -89,10 +89,27 @@
|
|||
android:layout_gravity="end|center_vertical"
|
||||
app:tint="?attr/textColor" />
|
||||
|
||||
<ImageView
|
||||
android:nextFocusUp="@id/result_back"
|
||||
android:nextFocusDown="@id/result_description"
|
||||
android:nextFocusLeft="@id/result_subscribe"
|
||||
android:nextFocusRight="@id/result_share"
|
||||
|
||||
android:id="@+id/result_favorite"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
android:layout_margin="5dp"
|
||||
android:elevation="10dp"
|
||||
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_baseline_favorite_border_24"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
app:tint="?attr/textColor" />
|
||||
|
||||
<ImageView
|
||||
android:nextFocusUp="@id/result_back"
|
||||
android:nextFocusDown="@id/result_description"
|
||||
android:nextFocusLeft="@id/result_subscribe"
|
||||
android:nextFocusLeft="@id/result_favorite"
|
||||
android:nextFocusRight="@id/result_open_in_browser"
|
||||
|
||||
android:id="@+id/result_share"
|
||||
|
|
|
@ -124,28 +124,27 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
|
|||
android:textColor="?attr/textColor" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/background_poster_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="150dp"
|
||||
android:layout_height="250dp"
|
||||
android:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
<com.lagradost.cloudstream3.utils.PercentageCropImageView
|
||||
android:id="@+id/background_poster"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="250dp"
|
||||
android:layout_height="275dp"
|
||||
android:layout_gravity="center"
|
||||
android:alpha="0.8"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/profile_bg_dark_blue" />
|
||||
android:scaleType="matrix"
|
||||
tools:src="@drawable/profile_bg_dark_blue" >
|
||||
</com.lagradost.cloudstream3.utils.PercentageCropImageView>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:layout_height="120dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:src="@drawable/background_shadow">
|
||||
|
||||
</ImageView>
|
||||
</FrameLayout>
|
||||
|
||||
|
@ -155,7 +154,6 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -166,8 +164,70 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
|
|||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="100dp"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="175dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="center_vertical"
|
||||
android:singleLine="true"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="The Perfect Run The Perfect Run" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/result_next_airing_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_episodes_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="normal"
|
||||
android:visibility="gone"
|
||||
tools:text="8 Episodes" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_next_airing"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center"
|
||||
android:textColor="?attr/grayTextColor"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="normal"
|
||||
tools:text="Episode 1022 will be released in" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_next_airing_time"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="start"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="normal"
|
||||
tools:text="5d 3h 30m" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="10dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="250dp"
|
||||
|
@ -176,67 +236,11 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
|
|||
android:layout_weight="0"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="center_vertical"
|
||||
android:singleLine="true"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="The Perfect Run The Perfect Run" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/result_next_airing_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_episodes_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="normal"
|
||||
android:visibility="gone"
|
||||
tools:text="8 Episodes" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_next_airing"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center"
|
||||
android:textColor="?attr/grayTextColor"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="normal"
|
||||
tools:text="Episode 1022 will be released in" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_next_airing_time"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="start"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="normal"
|
||||
tools:text="5d 3h 30m" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/result_movie_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:layout_marginTop="10dp"
|
||||
android:animateLayoutChanges="true"
|
||||
android:orientation="vertical"
|
||||
tools:visibility="visible">
|
||||
|
@ -310,19 +314,41 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
|
|||
style="@style/ResultButtonTV"
|
||||
android:nextFocusRight="@id/result_description"
|
||||
android:nextFocusUp="@id/result_play_trailer"
|
||||
android:nextFocusDown="@id/result_episodes_show"
|
||||
android:nextFocusDown="@id/result_favorite_button"
|
||||
|
||||
android:text="@string/type_none"
|
||||
android:visibility="visible"
|
||||
app:icon="@drawable/ic_baseline_bookmark_24" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/result_favorite_button"
|
||||
style="@style/ResultButtonTV"
|
||||
android:nextFocusRight="@id/result_description"
|
||||
android:nextFocusUp="@id/result_bookmark_button"
|
||||
android:nextFocusDown="@id/result_subscribe_button"
|
||||
|
||||
android:text="@string/action_add_to_favorites"
|
||||
android:visibility="visible"
|
||||
app:icon="@drawable/ic_baseline_favorite_border_24" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/result_subscribe_button"
|
||||
style="@style/ResultButtonTV"
|
||||
android:nextFocusRight="@id/result_description"
|
||||
android:nextFocusUp="@id/result_favorite_button"
|
||||
android:nextFocusDown="@id/result_episodes_show"
|
||||
|
||||
android:text="@string/action_subscribe"
|
||||
android:visibility="visible"
|
||||
app:icon="@drawable/ic_baseline_favorite_border_24" />
|
||||
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/result_episodes_show"
|
||||
style="@style/ResultButtonTV"
|
||||
|
||||
android:nextFocusRight="@id/redirect_to_episodes"
|
||||
android:nextFocusUp="@id/result_bookmark_button"
|
||||
android:nextFocusUp="@id/result_subscribe_button"
|
||||
android:nextFocusDown="@id/result_cast_items"
|
||||
|
||||
android:text="@string/episodes"
|
||||
|
@ -404,6 +430,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
|
|||
android:fadingEdgeLength="30dp"
|
||||
android:foreground="@drawable/outline_drawable"
|
||||
android:maxLines="7"
|
||||
android:focusable="true"
|
||||
android:nextFocusUp="@id/result_back"
|
||||
android:nextFocusDown="@id/result_bookmark_button"
|
||||
android:padding="5dp"
|
||||
|
@ -572,6 +599,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
|
|||
app:layout_constraintTop_toTopOf="@+id/shadow_space_1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/episodes_shadow_background"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
app:queryBackground="@color/transparent"
|
||||
app:queryHint="@string/search_hint"
|
||||
app:searchIcon="@drawable/search_icon"
|
||||
app:closeIcon="@drawable/ic_baseline_close_24"
|
||||
tools:ignore="RtlSymmetry">
|
||||
|
||||
<requestFocus />
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
app:queryBackground="@color/transparent"
|
||||
app:queryHint="@string/search_hint"
|
||||
app:searchIcon="@drawable/search_icon"
|
||||
app:closeIcon="@drawable/ic_baseline_close_24"
|
||||
tools:ignore="RtlSymmetry">
|
||||
|
||||
<requestFocus />
|
||||
|
@ -84,7 +85,8 @@
|
|||
android:nextFocusLeft="@id/main_search"
|
||||
android:nextFocusRight="@id/main_search"
|
||||
android:nextFocusUp="@id/nav_rail_view"
|
||||
android:nextFocusDown="@id/search_autofit_results"
|
||||
android:nextFocusDown="@id/tvtypes_chips_scroll"
|
||||
android:tag = "@string/tv_no_focus_tag"
|
||||
android:src="@drawable/ic_baseline_tune_24"
|
||||
app:tint="?attr/textColor" />
|
||||
</FrameLayout>
|
||||
|
@ -141,6 +143,7 @@
|
|||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusUp="@id/tvtypes_chips"
|
||||
android:nextFocusDown="@id/search_clear_call_history"
|
||||
android:tag = "@string/tv_no_focus_tag"
|
||||
android:paddingBottom="50dp"
|
||||
android:visibility="visible"
|
||||
tools:listitem="@layout/search_history_item" />
|
||||
|
|
35
app/src/main/res/layout/homepage_parent_emulator.xml
Normal file
35
app/src/main/res/layout/homepage_parent_emulator.xml
Normal 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>
|
|
@ -5,5 +5,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:focusable="false"
|
||||
android:tag="tv_no_focus_tag"
|
||||
tools:listitem="@layout/home_result_grid_expanded" />
|
||||
|
||||
|
|
25
app/src/main/res/layout/lock_pin_dialog.xml
Normal file
25
app/src/main/res/layout/lock_pin_dialog.xml
Normal 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>
|
|
@ -86,7 +86,6 @@
|
|||
</FrameLayout>
|
||||
<!-- atm this is useless, however it might be used for PIP one day? -->
|
||||
<ImageView
|
||||
android:visibility="gone"
|
||||
android:id="@+id/player_fullscreen"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
|
@ -94,7 +93,9 @@
|
|||
android:layout_marginEnd="20dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/baseline_fullscreen_24"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/white" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/player_intro_play"
|
||||
android:layout_width="0dp"
|
||||
|
@ -108,8 +109,8 @@
|
|||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:focusableInTouchMode="false"
|
||||
android:visibility="gone"
|
||||
android:importantForAccessibility="no" />
|
||||
android:importantForAccessibility="no"
|
||||
android:visibility="gone" />
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
@ -117,6 +118,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/player_top_holder"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -272,6 +274,7 @@
|
|||
app:tint="@color/white"
|
||||
tools:ignore="ContentDescription" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/player_pause_play_holder_holder"
|
||||
android:layout_width="100dp"
|
||||
|
@ -298,6 +301,7 @@
|
|||
app:tint="@color/white"
|
||||
tools:ignore="ContentDescription" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/player_ffwd_holder"
|
||||
android:layout_width="0dp"
|
||||
|
@ -345,14 +349,15 @@
|
|||
android:layout_width="150dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginEnd="100dp"
|
||||
android:layout_marginTop="60dp"
|
||||
android:backgroundTint="@color/skipOpTransparent"
|
||||
android:maxLines="1"
|
||||
android:padding="10dp"
|
||||
android:textColor="@color/white"
|
||||
android:visibility="gone"
|
||||
app:cornerRadius="@dimen/rounded_button_radius"
|
||||
app:layout_constraintBottom_toTopOf="@+id/bottom_player_bar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/player_top_holder"
|
||||
app:strokeColor="@color/white"
|
||||
app:strokeWidth="1dp"
|
||||
tools:text="Skip Opening"
|
||||
|
@ -427,20 +432,21 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/player_video_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:visibility="visible"
|
||||
android:layoutDirection="ltr"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@id/exo_position"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="20dp"
|
||||
android:gravity="end"
|
||||
android:gravity="end|center_vertical"
|
||||
android:includeFontPadding="false"
|
||||
android:minWidth="50dp"
|
||||
android:paddingLeft="4dp"
|
||||
|
@ -448,14 +454,46 @@
|
|||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="normal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="15:30" />
|
||||
<!--app:buffered_color="@color/videoCache"-->
|
||||
<androidx.media3.ui.DefaultTimeBar
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/previewFrameLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/video_frame"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toTopOf="@+id/exo_progress"
|
||||
app:layout_constraintDimensionRatio="16:9"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintWidth_default="percent"
|
||||
app:layout_constraintWidth_percent="0.25"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/previewImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="@dimen/video_frame_width"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="centerCrop" />
|
||||
</FrameLayout>
|
||||
|
||||
<com.github.rubensousa.previewseekbar.media3.PreviewTimeBar
|
||||
android:id="@id/exo_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_weight="1"
|
||||
app:bar_height="2dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/exo_position"
|
||||
app:layout_constraintEnd_toStartOf="@id/exo_duration"
|
||||
app:layout_constraintStart_toEndOf="@+id/exo_position"
|
||||
app:played_color="?attr/colorPrimary"
|
||||
|
||||
app:scrubber_color="?attr/colorPrimary"
|
||||
|
@ -463,12 +501,12 @@
|
|||
app:scrubber_enabled_size="24dp"
|
||||
app:unplayed_color="@color/videoProgress" />
|
||||
|
||||
<!-- exo_duration-->
|
||||
<TextView
|
||||
android:id="@id/exo_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center|center_vertical"
|
||||
|
||||
android:layout_marginEnd="20dp"
|
||||
android:includeFontPadding="false"
|
||||
android:minWidth="50dp"
|
||||
|
@ -477,9 +515,10 @@
|
|||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="normal"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:text="23:20" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -503,13 +503,12 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/player_video_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layoutDirection="ltr"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/player_pause_play"
|
||||
|
||||
|
@ -526,14 +525,17 @@
|
|||
|
||||
android:tag="@string/tv_no_focus_tag"
|
||||
app:tint="@color/player_button_tv"
|
||||
tools:ignore="ContentDescription" />
|
||||
tools:ignore="ContentDescription"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@id/exo_position"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="end"
|
||||
android:layout_marginStart="20dp"
|
||||
android:gravity="end|center_vertical"
|
||||
android:includeFontPadding="false"
|
||||
android:minWidth="50dp"
|
||||
android:paddingLeft="4dp"
|
||||
|
@ -541,29 +543,59 @@
|
|||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="normal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/player_pause_play"
|
||||
tools:text="15:30" />
|
||||
<!--app:buffered_color="@color/videoCache"-->
|
||||
<androidx.media3.ui.DefaultTimeBar
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/previewFrameLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/video_frame"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toTopOf="@+id/exo_progress"
|
||||
app:layout_constraintDimensionRatio="16:9"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintWidth_default="percent"
|
||||
app:layout_constraintWidth_percent="0.25"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/previewImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="@dimen/video_frame_width"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="centerCrop" />
|
||||
</FrameLayout>
|
||||
|
||||
<com.github.rubensousa.previewseekbar.media3.PreviewTimeBar
|
||||
android:id="@id/exo_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:focusable="false"
|
||||
android:focusableInTouchMode="false"
|
||||
app:bar_height="2dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/exo_position"
|
||||
app:layout_constraintEnd_toStartOf="@id/exo_duration"
|
||||
app:layout_constraintStart_toEndOf="@+id/exo_position"
|
||||
app:played_color="?attr/colorPrimary"
|
||||
|
||||
app:scrubber_color="?attr/colorPrimary"
|
||||
app:scrubber_dragged_size="26dp"
|
||||
app:scrubber_enabled_size="24dp"
|
||||
app:unplayed_color="@color/videoProgress" />
|
||||
|
||||
<!-- exo_duration-->
|
||||
<TextView
|
||||
android:id="@id/exo_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center|center_vertical"
|
||||
|
||||
android:layout_marginEnd="20dp"
|
||||
android:includeFontPadding="false"
|
||||
android:minWidth="50dp"
|
||||
|
@ -572,9 +604,11 @@
|
|||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="normal"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:text="23:20" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
android:layout_toStartOf="@id/priority_number"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="10dp"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/baseline_remove_24" />
|
||||
|
||||
<TextView
|
||||
|
@ -43,6 +44,7 @@
|
|||
android:layout_centerVertical="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="10dp"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_baseline_add_24" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
android:id="@+id/card_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:focusable="true"
|
||||
app:layout_constraintDimensionRatio="1"
|
||||
android:layout_marginStart="10dp"
|
||||
android:animateLayoutChanges="true"
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_marginTop="10dp"
|
||||
android:focusable="true"
|
||||
android:foreground="@drawable/outline_drawable_forced"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
@ -66,8 +67,9 @@
|
|||
android:layout_rowWeight="1"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:listSelector="@drawable/outline_drawable_forced"
|
||||
android:nextFocusLeft="@id/sort_subtitles"
|
||||
android:nextFocusRight="@id/apply_btt"
|
||||
android:nextFocusUp="@id/profiles_click_settings"
|
||||
android:nextFocusRight="@id/sort_subtitles"
|
||||
android:nextFocusDown="@id/apply_btt"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:layout_height="100dp"
|
||||
tools:listitem="@layout/sort_bottom_single_choice" />
|
||||
|
@ -94,6 +96,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_marginTop="10dp"
|
||||
android:focusable="true"
|
||||
android:foreground="@drawable/outline_drawable_forced"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="10dp"
|
||||
|
@ -139,8 +142,10 @@
|
|||
android:layout_rowWeight="1"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:listSelector="@drawable/outline_drawable_forced"
|
||||
android:nextFocusUp="@id/subtitles_click_settings"
|
||||
android:nextFocusLeft="@id/sort_providers"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:nextFocusRight="@id/apply_btt"
|
||||
android:nextFocusDown="@id/apply_btt"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:layout_height="200dp"
|
||||
tools:listfooter="@layout/sort_bottom_footer_add_choice"
|
||||
|
|
|
@ -42,8 +42,8 @@
|
|||
android:layout_rowWeight="1"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:listSelector="@drawable/outline_drawable_less"
|
||||
android:nextFocusLeft="@id/sort_subtitles"
|
||||
android:nextFocusRight="@id/apply_btt"
|
||||
android:nextFocusRight="@id/sort_subtitles"
|
||||
android:nextFocusDown="@id/profile_text_editable"
|
||||
android:requiresFadingEdge="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:layout_height="100dp"
|
||||
|
@ -92,6 +92,8 @@
|
|||
android:layout_height="50dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="12dp"
|
||||
android:focusable="true"
|
||||
android:nextFocusLeft="@id/sort_sources"
|
||||
android:src="@drawable/baseline_help_outline_24"
|
||||
android:contentDescription="@string/help" />
|
||||
|
||||
|
@ -115,8 +117,10 @@
|
|||
android:layout_rowWeight="1"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:listSelector="@drawable/outline_drawable_less"
|
||||
android:nextFocusLeft="@id/sort_providers"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:nextFocusLeft="@id/sort_sources"
|
||||
android:nextFocusRight="@id/apply_btt"
|
||||
android:nextFocusUp="@id/help_btt"
|
||||
android:nextFocusDown="@id/apply_btt"
|
||||
android:requiresFadingEdge="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:layout_height="200dp"
|
||||
|
|
|
@ -17,14 +17,15 @@
|
|||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/quick_search_back"
|
||||
android:layout_gravity="center"
|
||||
android:foregroundGravity="center"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_baseline_arrow_back_24"
|
||||
app:tint="@android:color/white"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="wrap_content">
|
||||
android:id="@+id/quick_search_back"
|
||||
android:layout_gravity="center"
|
||||
android:foregroundGravity="center"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_baseline_arrow_back_24"
|
||||
app:tint="@android:color/white"
|
||||
android:focusable="true"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<requestFocus />
|
||||
</ImageView>
|
||||
|
@ -48,6 +49,8 @@
|
|||
app:queryBackground="@color/transparent"
|
||||
|
||||
app:searchIcon="@drawable/search_icon"
|
||||
app:closeIcon="@drawable/ic_baseline_close_24"
|
||||
|
||||
android:paddingStart="-10dp"
|
||||
android:iconifiedByDefault="false"
|
||||
app:queryHint="@string/search_hint"
|
||||
|
|
|
@ -11,7 +11,9 @@
|
|||
android:nextFocusRight="@id/download_button"
|
||||
app:cardBackgroundColor="@color/transparent"
|
||||
app:cardCornerRadius="@dimen/rounded_image_radius"
|
||||
app:cardElevation="0dp">
|
||||
app:cardElevation="0dp"
|
||||
android:foreground="@drawable/outline_drawable"
|
||||
>
|
||||
<!--
|
||||
android:nextFocusLeft="@id/result_episode_download"
|
||||
-->
|
||||
|
|
|
@ -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>
|
14
app/src/main/res/layout/result_episode_both_old.xml
Normal file
14
app/src/main/res/layout/result_episode_both_old.xml
Normal 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>
|
|
@ -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>
|
21
app/src/main/res/layout/result_episode_both_tv_old.xml
Normal file
21
app/src/main/res/layout/result_episode_both_tv_old.xml
Normal 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>
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:foreground="@drawable/outline_drawable"
|
||||
android:nextFocusRight="@id/download_button"
|
||||
app:cardBackgroundColor="?attr/boxItemBackground"
|
||||
|
||||
|
|
|
@ -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>
|
112
app/src/main/res/layout/result_episode_large_tv_old.xml
Normal file
112
app/src/main/res/layout/result_episode_large_tv_old.xml
Normal 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>
|
|
@ -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>
|
59
app/src/main/res/layout/result_episode_tv_old.xml
Normal file
59
app/src/main/res/layout/result_episode_tv_old.xml
Normal 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>
|
|
@ -6,7 +6,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/outline_drawable_less"
|
||||
|
||||
android:focusable="true"
|
||||
android:nextFocusRight="@id/home_history_remove"
|
||||
android:orientation="horizontal">
|
||||
<!-- android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
|
@ -30,7 +30,7 @@
|
|||
android:layout_gravity="center_vertical|end"
|
||||
|
||||
android:background="@drawable/outline_drawable_less"
|
||||
|
||||
android:focusable="true"
|
||||
android:nextFocusLeft="@id/home_history_tab"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/ic_baseline_close_24"
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:focusable="true"
|
||||
android:nextFocusRight="@id/subtitle_offset_subtract"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/ic_baseline_keyboard_arrow_left_24"
|
||||
|
@ -48,6 +49,7 @@
|
|||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:focusable="true"
|
||||
android:nextFocusLeft="@id/subtitle_offset_subtract_more"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/baseline_remove_24"
|
||||
|
@ -70,6 +72,7 @@
|
|||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:focusable="true"
|
||||
android:nextFocusRight="@id/subtitle_offset_add_more"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/ic_baseline_add_24"
|
||||
|
@ -83,7 +86,9 @@
|
|||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:focusable="true"
|
||||
android:nextFocusLeft="@id/subtitle_offset_add"
|
||||
android:nextFocusDown="@id/apply_btt"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/ic_baseline_keyboard_arrow_right_24"
|
||||
app:tint="?attr/white"
|
||||
|
|
|
@ -212,7 +212,6 @@
|
|||
tools:visibility="visible" />
|
||||
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/player_center_menu"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -294,6 +293,7 @@
|
|||
app:tint="@color/white"
|
||||
tools:ignore="ContentDescription" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/player_ffwd_holder"
|
||||
android:layout_width="0dp"
|
||||
|
@ -420,20 +420,20 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/player_video_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layoutDirection="ltr"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@id/exo_position"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="20dp"
|
||||
android:gravity="end"
|
||||
android:gravity="end|center_vertical"
|
||||
android:includeFontPadding="false"
|
||||
android:minWidth="50dp"
|
||||
android:paddingLeft="4dp"
|
||||
|
@ -441,14 +441,46 @@
|
|||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="normal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="15:30" />
|
||||
<!--app:buffered_color="@color/videoCache"-->
|
||||
<androidx.media3.ui.DefaultTimeBar
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/previewFrameLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/video_frame"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toTopOf="@+id/exo_progress"
|
||||
app:layout_constraintDimensionRatio="16:9"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintWidth_default="percent"
|
||||
app:layout_constraintWidth_percent="0.25"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/previewImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="@dimen/video_frame_width"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="centerCrop" />
|
||||
</FrameLayout>
|
||||
|
||||
<com.github.rubensousa.previewseekbar.media3.PreviewTimeBar
|
||||
android:id="@id/exo_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_weight="1"
|
||||
app:bar_height="2dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/exo_position"
|
||||
app:layout_constraintEnd_toStartOf="@id/exo_duration"
|
||||
app:layout_constraintStart_toEndOf="@+id/exo_position"
|
||||
app:played_color="?attr/colorPrimary"
|
||||
|
||||
app:scrubber_color="?attr/colorPrimary"
|
||||
|
@ -456,13 +488,13 @@
|
|||
app:scrubber_enabled_size="24dp"
|
||||
app:unplayed_color="@color/videoProgress" />
|
||||
|
||||
<!-- exo_duration-->
|
||||
<TextView
|
||||
android:id="@id/exo_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center|center_vertical"
|
||||
|
||||
android:layout_marginEnd="30dp"
|
||||
android:includeFontPadding="false"
|
||||
android:minWidth="50dp"
|
||||
android:paddingLeft="4dp"
|
||||
|
@ -470,19 +502,23 @@
|
|||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="normal"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
|
||||
app:layout_constraintEnd_toEndOf="@id/player_fullscreen"
|
||||
tools:text="23:20" />
|
||||
|
||||
</LinearLayout>
|
||||
<ImageView
|
||||
android:id="@+id/player_fullscreen"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/baseline_fullscreen_24"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:tint="@color/white" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/player_fullscreen"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/baseline_fullscreen_24"
|
||||
app:tint="@color/white" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
android:foreground="?attr/selectableItemBackgroundBorderless"
|
||||
app:cardCornerRadius="@dimen/rounded_image_radius"
|
||||
android:layout_margin="5dp"
|
||||
android:focusable="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -34,6 +35,15 @@
|
|||
android:background="@drawable/outline_card"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/lock_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_margin="4dp"
|
||||
android:src="@drawable/video_locked"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/profile_text"
|
||||
tools:text="@string/mobile_data"
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
android:foreground="?attr/selectableItemBackgroundBorderless"
|
||||
app:cardCornerRadius="@dimen/rounded_image_radius"
|
||||
android:layout_margin="5dp"
|
||||
android:focusable="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
|
@ -77,6 +77,12 @@
|
|||
android:textColorHint="?attr/grayTextColor"
|
||||
tools:ignore="LabelFor" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/lockProfileCheckbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/lock_profile" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -88,6 +94,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:layout_gravity="center"
|
||||
android:focusable="true"
|
||||
android:contentDescription="@string/preview_background_img_des"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/profile_bg_blue" />
|
||||
|
|
|
@ -331,57 +331,56 @@
|
|||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim"
|
||||
tools:layout="@layout/main_settings">
|
||||
<action
|
||||
android:id="@+id/action_navigation_settings_to_navigation_settings_ui"
|
||||
app:destination="@id/navigation_settings_ui"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_navigation_settings_to_navigation_settings_providers"
|
||||
app:destination="@id/navigation_settings_providers"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_navigation_settings_to_navigation_settings_player"
|
||||
app:destination="@id/navigation_settings_player"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_navigation_settings_to_navigation_settings_updates"
|
||||
app:destination="@id/navigation_settings_updates"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_navigation_settings_to_navigation_settings_account"
|
||||
app:destination="@id/navigation_settings_account"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_navigation_settings_to_navigation_settings_general"
|
||||
app:destination="@id/navigation_settings_general"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_navigation_settings_to_navigation_settings_extensions"
|
||||
app:destination="@id/navigation_settings_extensions"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
</fragment>
|
||||
tools:layout="@layout/main_settings" />
|
||||
<action
|
||||
android:id="@+id/action_navigation_global_to_navigation_settings_ui"
|
||||
app:destination="@id/navigation_settings_ui"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_navigation_global_to_navigation_settings_providers"
|
||||
app:destination="@id/navigation_settings_providers"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_navigation_global_to_navigation_settings_player"
|
||||
app:destination="@id/navigation_settings_player"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_navigation_global_to_navigation_settings_updates"
|
||||
app:destination="@id/navigation_settings_updates"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_navigation_global_to_navigation_settings_account"
|
||||
app:destination="@id/navigation_settings_account"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_navigation_global_to_navigation_settings_general"
|
||||
app:destination="@id/navigation_settings_general"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_navigation_global_to_navigation_settings_extensions"
|
||||
app:destination="@id/navigation_settings_extensions"
|
||||
app:enterAnim="@anim/enter_anim"
|
||||
app:exitAnim="@anim/exit_anim"
|
||||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/navigation_subtitles"
|
||||
|
@ -638,4 +637,4 @@
|
|||
app:popEnterAnim="@anim/enter_anim"
|
||||
app:popExitAnim="@anim/exit_anim"
|
||||
tools:layout="@layout/fragment_setup_media" />
|
||||
</navigation>
|
||||
</navigation>
|
||||
|
|
|
@ -1,2 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources/>
|
||||
<resources>
|
||||
<string name="player_speed_text_format" formatted="true">السرعة (%.2fx)</string>
|
||||
<string name="home_change_provider_img_des">غير المصدر</string>
|
||||
<string name="next_episode_format" formatted="true">حتنزل الحلقة %d ب</string>
|
||||
<string name="title_downloads">التنزيلات</string>
|
||||
<string name="app_dub_sub_episode_text_format" formatted="true">%s الحلقة %d</string>
|
||||
</resources>
|
||||
|
|
|
@ -2,4 +2,110 @@
|
|||
<resources>
|
||||
<string name="app_dub_sub_episode_text_format" formatted="true">%s ክፍል %d</string>
|
||||
<string name="cast_format" formatted="true">ተዋናዮች: %s</string>
|
||||
<string name="player_speed_text_format" formatted="true">ፍጥነት(%.2fx)</string>
|
||||
<string name="home_next_random_img_des">ቀጣይ በዘፈቀደ</string>
|
||||
<string name="preview_background_img_des">ዳራ ቅድም እይታ</string>
|
||||
<string name="next_episode_time_hour_format" formatted="true">%dሰዓት %dደቂቃ</string>
|
||||
<string name="search_poster_img_des">ፖስተር</string>
|
||||
<string name="title_downloads">የወረዱ</string>
|
||||
<string name="new_update_format" formatted="true">አዲስ ማሻሻያ ተገኝቷል!
|
||||
\n%s -> %s</string>
|
||||
<string name="go_back_img_des">ተመለስ</string>
|
||||
<string name="episode_more_options_des">ተጨማሪ አማራጮች</string>
|
||||
<string name="type_watching">በማየት ላይ</string>
|
||||
<string name="result_tags">ዘውጎች</string>
|
||||
<string name="episode_poster_img_des">የክፍሉ ፖስተር</string>
|
||||
<string name="search_hint_site" formatted="true">%sን ፈልግ…</string>
|
||||
<string name="filler" formatted="true">መሙያ</string>
|
||||
<string name="home_change_provider_img_des">አቅራቢ ቀይር</string>
|
||||
<string name="search_hint">ፍለጋ…</string>
|
||||
<string name="rated_format" formatted="true">ተመዘነ: %.1f</string>
|
||||
<string name="browser">አሳሽ</string>
|
||||
<string name="next_episode_time_day_format" formatted="true">%dቀን %dሰዓት %dደቂቃ</string>
|
||||
<string name="next_episode">ቀጣይ ክፍል</string>
|
||||
<string name="next_episode_time_min_format" formatted="true">%dደቂቃ</string>
|
||||
<string name="duration_format" formatted="true">%d ደቂቃ</string>
|
||||
<string name="result_poster_img_des">ፖስተር</string>
|
||||
<string name="result_open_in_browser">በአሳሽ ውስጥ ይክፈቱ</string>
|
||||
<string name="no_data">ውሂብ የለም</string>
|
||||
<string name="title_home">መነሻ</string>
|
||||
<string name="skip_loading">መጫንን ዝለል</string>
|
||||
<string name="next_episode_format" formatted="true">ክፍል %d በ ይለቀቃል</string>
|
||||
<string name="result_share">ማጋሪያ</string>
|
||||
<string name="home_main_poster_img_des">ዋና ፖስተር</string>
|
||||
<string name="title_settings">ቅንብሮች</string>
|
||||
<string name="title_search">መፈለጊያ</string>
|
||||
<string name="loading">በመጫን ላይ…</string>
|
||||
<string name="app_name">CloudStream</string>
|
||||
<string name="play_with_app_name">በCloudStream አጫውት</string>
|
||||
<string name="action_remove_from_bookmarks">ያስወግዱ</string>
|
||||
<string name="error_bookmarks_text">ዕልባቶች</string>
|
||||
<string name="download_started">ማውረድ ተጀምሯል</string>
|
||||
<string name="play_movie_button">ሙቪ አጫውት</string>
|
||||
<string name="sort_clear">አጽዳ</string>
|
||||
<string name="subs_outline_color">የዳርቻ ቀለም</string>
|
||||
<string name="filter_bookmarks">ዕልባቶችን ማጣሪያ</string>
|
||||
<string name="type_plan_to_watch">በእቅድ ላይ</string>
|
||||
<string name="update_started">ማዘመን ተጀምሯል</string>
|
||||
<string name="sort_copy">ቅዳ</string>
|
||||
<string name="home_more_info">ተጨማሪ መረጃ</string>
|
||||
<string name="error_loading_links_toast">አገናኞችን መጫን ላይ ስህተት</string>
|
||||
<string name="subs_edge_type">የጠርዝ ዓይነት</string>
|
||||
<string name="download_done">ማውረድ ተከናውኗል</string>
|
||||
<string name="downloading">በማውረድ ላይ</string>
|
||||
<string name="play_episode">ክፍልን አጫውት</string>
|
||||
<string name="player_speed">የአጫዋች ፍጥነት</string>
|
||||
<string name="type_dropped">የተተወ</string>
|
||||
<string name="subs_background_color">የዳራ ቀለም</string>
|
||||
<string name="popup_pause_download">ማውረድ ለአፍታ አቁም</string>
|
||||
<string name="app_dubbed_text">ትርጉም ድምጽ</string>
|
||||
<string name="subtitles_settings">የትርጉም ጽሑፍ ቅንብሮች</string>
|
||||
<string name="subs_window_color">የዊንዶው ቀለም</string>
|
||||
<string name="play_torrent_button">Torrent አጫውት</string>
|
||||
<string name="download_canceled">ማውረድ ተቋርጧል</string>
|
||||
<string name="home_expanded_hide">ደብቅ</string>
|
||||
<string name="sort_apply">አጽድቅ</string>
|
||||
<string name="pick_subtitle">የትርጉም ጽሑፎች</string>
|
||||
<string name="download_paused">ማውረድ ቆሟል</string>
|
||||
<string name="download">ማውረጃ</string>
|
||||
<string name="reload_error">ግንኙነትን እንደገና ይሞሩ…</string>
|
||||
<string name="popup_delete_file">ፋይል አጥፋ</string>
|
||||
<string name="downloaded">ወርዷል</string>
|
||||
<string name="popup_resume_download">ማውረድ ቀጥል</string>
|
||||
<string name="subs_text_color">የጽሑፍ ቀለም</string>
|
||||
<string name="type_completed">የተጠናቀቀ</string>
|
||||
<string name="play_trailer_button">የፊልም ማስታወቂያ አጫውት</string>
|
||||
<string name="play_livestream_button">የቀጥታ ስርጭት አጫውት</string>
|
||||
<string name="popup_play_file">ፋይል አጫውት</string>
|
||||
<string name="type_re_watching">እንደገና በማየት ላይ</string>
|
||||
<string name="go_back">ወደ ኋላ መመለሻ</string>
|
||||
<string name="home_info">መረጃ</string>
|
||||
<string name="sort_save">ያስቀምጡ</string>
|
||||
<string name="download_failed">ማውረድ አልተሳካም</string>
|
||||
<string name="pick_source">ምንጮች</string>
|
||||
<string name="app_subbed_text">ትርጉም ጽሁፍ</string>
|
||||
<string name="stream">ዥረት</string>
|
||||
<string name="type_on_hold">በመቆየት ላይ</string>
|
||||
<string name="home_play">አጫውት</string>
|
||||
<string name="download_storage_text">ውስጣዊ ማከማቻ</string>
|
||||
<string name="sort_close">ዝጋ</string>
|
||||
<string name="action_add_to_bookmarks">የምልከታ ሁኔታን ያቀናብሩ</string>
|
||||
<string name="subs_hold_to_reset_to_default">ወደ ነባሪ ዳግም ለማስጀመር ጫን አድርገው ይያዙ</string>
|
||||
<string name="subs_font_size">የቅርጸ-ቁምፊ መጠን</string>
|
||||
<string name="subs_auto_select_language">ቋንቋን በራስ ይምረጥ</string>
|
||||
<string name="continue_watching">መመልከትዎን ይቀጥሉ</string>
|
||||
<string name="subs_download_languages">ቋንቋዎችን ያውርዱ</string>
|
||||
<string name="search_provider_text_providers">አቅራቢዎችን በመጠቀም ይፈልጉ</string>
|
||||
<string name="benene_count_text">%d ሙዝ ለዴቭሎፐሮቹ ተሰጥቷል</string>
|
||||
<string name="vpn_might_be_needed">ይህ አቅራቢ በትክክል እንዲሰራ ቪፒኤን ሊያስፈልግ ይችላል</string>
|
||||
<string name="benene_count_text_none">ምንም ሙዝ አልተሰጠም</string>
|
||||
<string name="subs_subtitle_languages">የትርጉም ጽሑፍ ቋንቋ</string>
|
||||
<string name="provider_info_meta">ሜታዳታ በድረ-ገጹ ላይ አይገኝም፣ ቪድዮ መጫን ሜታዳታ ድረ-ገጹ ላይ ከሌለ አይሳካም።</string>
|
||||
<string name="vpn_torrent">ይህ አቅራቢ ቶረንት ነው፣ ቪፒኤን ይመከራል</string>
|
||||
<string name="subs_font">ቅርጸ-ቁምፊ</string>
|
||||
<string name="torrent_plot">መግለጫ</string>
|
||||
<string name="action_remove_watching">ያስወግዱ</string>
|
||||
<string name="action_open_watching">ተጨማሪ መረጃ</string>
|
||||
<string name="search_provider_text_types">ዓይነቶችን በመጠቀም ይፈልጉ</string>
|
||||
<string name="subs_import_text" formatted="true">ቅርጸ-ቁምፊዎችን በ%s ውስጥ በማስቀመጥ ያጫኑ</string>
|
||||
</resources>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue