mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Compare commits
168 commits
revert-959
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d3ab40093 |
||
|
|
c4ccc5d351 |
||
|
|
fcac19737c |
||
|
|
77dc9f7484 |
||
|
|
f6a65f38db |
||
|
|
4d9a080341 |
||
|
|
7936ccf5d3 |
||
|
|
15b5013e28 | ||
|
|
6f522828a4 | ||
|
|
ad727b96cf |
||
|
|
67e278b2b7 | ||
|
|
7f1cba99e4 | ||
|
|
ff29fe6ee6 | ||
|
|
aac2311722 |
||
|
|
60e3c48aca |
||
|
|
14dd418652 |
||
|
|
5012821216 |
||
|
|
ab379ab31c |
||
|
|
8fcb3e3121 |
||
|
|
30adb1cd9d |
||
|
|
63e27c2ea5 |
||
|
|
b2f08847e1 |
||
|
|
150ad5fc9f |
||
|
|
82f8ab489e |
||
|
|
04dda008c4 |
||
|
|
0aa48f335a |
||
|
|
a28ee41368 |
||
|
|
2fc279f4ae |
||
|
|
15d2d21631 |
||
|
|
e3ff1cf455 |
||
|
|
dfd127265a |
||
|
|
c8a863e332 |
||
|
|
0c418fdf9b |
||
|
|
4c7379c766 |
||
|
|
bb8144a52e |
||
|
|
073af50f5f |
||
|
|
63465ed7a9 |
||
|
|
12de924559 |
||
|
|
627dd45309 |
||
|
|
a157115cfa |
||
|
|
694193fa3e |
||
|
|
febb843424 |
||
|
|
8be8e54746 |
||
|
|
e86c926c30 |
||
|
|
145c42f1c8 |
||
|
|
9b1ac5fc28 |
||
|
|
699a6979a5 |
||
|
|
e1d4a46309 |
||
|
|
c1b5f5c128 |
||
|
|
e5c9e96c83 |
||
|
|
02b956940a |
||
|
|
03b8b6e637 |
||
|
|
29ec554334 |
||
|
|
5f64e40a7e |
||
|
|
d17111c1c1 |
||
|
|
a5582a7a67 |
||
|
|
1a05651510 |
||
|
|
ad27eb3b0e |
||
|
|
6b93af5803 |
||
|
|
55a0eb66cb |
||
|
|
b776642775 | ||
|
|
09fe9873cf |
||
|
|
0d40b5ebe3 |
||
|
|
9ca1d02bdc |
||
|
|
b06d9f224d |
||
|
|
b9746c2b17 |
||
|
|
c71d5d8add |
||
|
|
afa178a63a |
||
|
|
b702b7b1ec |
||
|
|
bda6673cfd |
||
|
|
7a0cd07dc1 |
||
|
|
30d223cfe3 |
||
|
|
4c061edd7c |
||
|
|
4c95610238 |
||
|
|
3345326cb2 |
||
|
|
607a4510b6 |
||
|
|
f775c1725d |
||
|
|
7eec0eff02 |
||
|
|
358a20eb77 |
||
|
|
0391a3b89c |
||
|
|
9bebfe4590 |
||
|
|
b3e3dadc72 |
||
|
|
b87fdfbf85 |
||
|
|
dff56026de |
||
|
|
5502e478c4 |
||
|
|
960f8449b7 |
||
|
|
d0852449a5 |
||
|
|
e697bf7554 |
||
|
|
db2bf5e7be |
||
|
|
469a71236b |
||
|
|
4d5cd288ab |
||
|
|
af828de8d5 |
||
|
|
ee4d1dedc5 |
||
|
|
f1cc4db89c |
||
|
|
3874cb9f9d |
||
|
|
0a5399d9b6 |
||
|
|
71bd48f493 |
||
|
|
83c473d9f8 |
||
|
|
c28a3cb987 |
||
|
|
d3828eeafe |
||
|
|
c07e6d3222 |
||
|
|
949b5830b6 |
||
|
|
ff1ffbeb83 |
||
|
|
138e1a1f0e |
||
|
|
004c481a5e |
||
|
|
e2946cad6b |
||
|
|
e6b9d621f9 |
||
|
|
0019f85501 |
||
|
|
0744189020 |
||
|
|
4399a612df |
||
|
|
e01ff4d843 |
||
|
|
6cef9f7ea2 |
||
|
|
9a18ef6411 |
||
|
|
6df3ef14f6 |
||
|
|
5db541d7cc |
||
|
|
aa8972870c |
||
|
|
afdc4988ac |
||
|
|
e6c111532d |
||
|
|
ffa7b0248a | ||
|
|
c13d290377 |
||
|
|
1bf7e14eab |
||
|
|
145ceea50f |
||
|
|
2fad760426 |
||
|
|
ff0dea3fbb |
||
|
|
44e5b86176 |
||
|
|
d8f89df163 |
||
|
|
a74563d003 |
||
|
|
0a24661e4c |
||
|
|
ed2bdf44fb |
||
|
|
51d91bf9a7 |
||
|
|
fb89fd60b8 |
||
|
|
7db7742c73 | ||
|
|
b246d80861 |
||
|
|
d321aba3a7 |
||
|
|
22937424fa |
||
|
|
7f0034e872 |
||
|
|
35e38a53ad |
||
|
|
6d8a31809d |
||
|
|
650c7583af |
||
|
|
34af3a4b2f |
||
|
|
9ef1f1cc41 |
||
|
|
6ede44d85f |
||
|
|
2f03ca7de9 |
||
|
|
7ce2dfc4aa |
||
|
|
16510923d2 |
||
|
|
a9c2c0644a |
||
|
|
4468ce3d80 |
||
|
|
faeb71da2c |
||
|
|
86bc0b8345 |
||
|
|
1ff0b5dccd |
||
|
|
a2e63174be | ||
|
|
eb60be54ed | ||
|
|
8d5b73495d | ||
|
|
a3bb853691 |
||
|
|
375b3ec46e |
||
|
|
ad67b9ddab | ||
|
|
638cc4fee9 |
||
|
|
4817b29b9c |
||
|
|
040ac77b1a |
||
|
|
527046766a |
||
|
|
adc653943b |
||
|
|
81df68e137 |
||
|
|
a01bb9e55b |
||
|
|
807bd85fa9 |
||
|
|
510d11f705 |
||
|
|
bd69054f5d |
||
|
|
694e7abbdf |
||
|
|
e3f9f255c7 |
471 changed files with 15966 additions and 7252 deletions
4
.github/ISSUE_TEMPLATE/application-bug.yml
vendored
4
.github/ISSUE_TEMPLATE/application-bug.yml
vendored
|
|
@ -80,13 +80,13 @@ body:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Your issue will be closed if you haven't done these steps.
|
description: Your issue will be closed if you haven't done these steps.
|
||||||
options:
|
options:
|
||||||
|
- label: I am sure my issue is related to the app and **NOT some extension**.
|
||||||
|
required: true
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
- label: I have written a short but informative title.
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**.
|
- label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**.
|
||||||
required: true
|
required: true
|
||||||
- label: If related to a provider, I have checked the site and it works, but not the app.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
- label: I will fill out all of the requested information in this form.
|
||||||
required: true
|
required: true
|
||||||
|
|
|
||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -2,7 +2,7 @@ blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Request a new provider or report bug with an existing provider
|
- name: Request a new provider or report bug with an existing provider
|
||||||
url: https://github.com/recloudstream
|
url: https://github.com/recloudstream
|
||||||
about: Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord.
|
about: EXTREMELY IMPORTANT - Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord.
|
||||||
- name: Discord
|
- name: Discord
|
||||||
url: https://discord.gg/5Hus6fM
|
url: https://discord.gg/5Hus6fM
|
||||||
about: Join our discord for faster support on smaller issues.
|
about: Join our discord for faster support on smaller issues.
|
||||||
|
|
|
||||||
8
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
8
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
|
|
@ -27,9 +27,7 @@ body:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Your issue will be closed if you haven't done these steps.
|
description: Your issue will be closed if you haven't done these steps.
|
||||||
options:
|
options:
|
||||||
|
- label: My suggestion is **NOT** about adding a new provider
|
||||||
|
required: true
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
|
||||||
required: true
|
|
||||||
- label: I will fill out all of the requested information in this form.
|
|
||||||
required: true
|
|
||||||
6
.github/locales.py
vendored
6
.github/locales.py
vendored
|
|
@ -1,6 +1,7 @@
|
||||||
import re
|
import re
|
||||||
import glob
|
import glob
|
||||||
import requests
|
import requests
|
||||||
|
import os
|
||||||
import lxml.etree as ET # builtin library doesn't preserve comments
|
import lxml.etree as ET # builtin library doesn't preserve comments
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -53,11 +54,16 @@ for file in glob.glob(f"{XML_NAME}*/strings.xml"):
|
||||||
try:
|
try:
|
||||||
tree = ET.parse(file)
|
tree = ET.parse(file)
|
||||||
for child in tree.getroot():
|
for child in tree.getroot():
|
||||||
|
if not child.text:
|
||||||
|
continue
|
||||||
if child.text.startswith("\\@string/"):
|
if child.text.startswith("\\@string/"):
|
||||||
print(f"[{file}] fixing {child.attrib['name']}")
|
print(f"[{file}] fixing {child.attrib['name']}")
|
||||||
child.text = child.text.replace("\\@string/", "@string/")
|
child.text = child.text.replace("\\@string/", "@string/")
|
||||||
with open(file, 'wb') as fp:
|
with open(file, 'wb') as fp:
|
||||||
fp.write(b'<?xml version="1.0" encoding="utf-8"?>\n')
|
fp.write(b'<?xml version="1.0" encoding="utf-8"?>\n')
|
||||||
tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False)
|
tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False)
|
||||||
|
# Remove trailing new line to be consistent with weblate
|
||||||
|
fp.seek(-1, os.SEEK_END)
|
||||||
|
fp.truncate()
|
||||||
except ET.ParseError as ex:
|
except ET.ParseError as ex:
|
||||||
print(f"[{file}] {ex}")
|
print(f"[{file}] {ex}")
|
||||||
|
|
|
||||||
7
.idea/gradle.xml
generated
7
.idea/gradle.xml
generated
|
|
@ -4,17 +4,16 @@
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="delegatedBuild" value="true" />
|
|
||||||
<option name="testRunner" value="GRADLE" />
|
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="jbr-17" />
|
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
<option value="$PROJECT_DIR$/library" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
|
<option name="resolveExternalAnnotations" value="false" />
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
|
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
|
||||||
import org.jetbrains.dokka.gradle.DokkaTask
|
import org.jetbrains.dokka.gradle.DokkaTask
|
||||||
|
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
@ -41,8 +42,8 @@ android {
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("prerelease") {
|
if (prereleaseStoreFile != null) {
|
||||||
if (prereleaseStoreFile != null) {
|
create("prerelease") {
|
||||||
storeFile = file(prereleaseStoreFile)
|
storeFile = file(prereleaseStoreFile)
|
||||||
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
|
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
|
||||||
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
|
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
|
||||||
|
|
@ -59,25 +60,20 @@ android {
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 33 /* Android 14 is Fu*ked
|
targetSdk = 33 /* Android 14 is Fu*ked
|
||||||
^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/
|
^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/
|
||||||
versionCode = 63
|
versionCode = 64
|
||||||
versionName = "4.3.1"
|
versionName = "4.4.0"
|
||||||
|
|
||||||
// retrieve latest commit hash
|
|
||||||
val gitVersion = providers.exec {
|
|
||||||
commandLine("git", "rev-parse", "--short", "HEAD")
|
|
||||||
}.standardOutput.asText.get()
|
|
||||||
|
|
||||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||||
resValue("string", "commit_hash", gitVersion)
|
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
||||||
resValue("bool", "is_prerelease", "false")
|
resValue("bool", "is_prerelease", "false")
|
||||||
|
|
||||||
// Reads local.properties
|
// Reads local.properties
|
||||||
val localProperties = gradleLocalProperties(rootDir)
|
val localProperties = gradleLocalProperties(rootDir)
|
||||||
|
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"String",
|
"long",
|
||||||
"BUILDDATE",
|
"BUILD_DATE",
|
||||||
"new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
|
"${System.currentTimeMillis()}"
|
||||||
)
|
)
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"String",
|
"String",
|
||||||
|
|
@ -128,7 +124,11 @@ android {
|
||||||
resValue("bool", "is_prerelease", "true")
|
resValue("bool", "is_prerelease", "true")
|
||||||
buildConfigField("boolean", "BETA", "true")
|
buildConfigField("boolean", "BETA", "true")
|
||||||
applicationIdSuffix = ".prerelease"
|
applicationIdSuffix = ".prerelease"
|
||||||
signingConfig = signingConfigs.getByName("prerelease")
|
if (signingConfigs.names.contains("prerelease")) {
|
||||||
|
signingConfig = signingConfigs.getByName("prerelease")
|
||||||
|
} else {
|
||||||
|
logger.warn("No prerelease signing config!")
|
||||||
|
}
|
||||||
versionNameSuffix = "-PRE"
|
versionNameSuffix = "-PRE"
|
||||||
versionCode = (System.currentTimeMillis() / 60000).toInt()
|
versionCode = (System.currentTimeMillis() / 60000).toInt()
|
||||||
}
|
}
|
||||||
|
|
@ -144,7 +144,7 @@ android {
|
||||||
abortOnError = false
|
abortOnError = false
|
||||||
checkReleaseBuilds = false
|
checkReleaseBuilds = false
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
}
|
}
|
||||||
|
|
@ -159,24 +159,24 @@ repositories {
|
||||||
dependencies {
|
dependencies {
|
||||||
// Testing
|
// Testing
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
testImplementation("org.json:json:20231013")
|
testImplementation("org.json:json:20240303")
|
||||||
androidTestImplementation("androidx.test:core")
|
androidTestImplementation("androidx.test:core")
|
||||||
implementation("androidx.test.ext:junit-ktx:1.1.5")
|
implementation("androidx.test.ext:junit-ktx:1.2.1")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
|
||||||
|
|
||||||
// Android Core & Lifecycle
|
// Android Core & Lifecycle
|
||||||
implementation("androidx.core:core-ktx:1.12.0")
|
implementation("androidx.core:core-ktx:1.13.1")
|
||||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
|
implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
|
||||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.3")
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3")
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
|
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
|
||||||
|
|
||||||
// Design & UI
|
// Design & UI
|
||||||
implementation("jp.wasabeef:glide-transformations:4.3.0")
|
implementation("jp.wasabeef:glide-transformations:4.3.0")
|
||||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||||
implementation("com.google.android.material:material:1.10.0")
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
|
|
||||||
|
|
@ -186,9 +186,9 @@ dependencies {
|
||||||
implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
|
implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
|
||||||
|
|
||||||
// For KSP -> Official Annotation Processors are Not Yet Supported for KSP
|
// For KSP -> Official Annotation Processors are Not Yet Supported for KSP
|
||||||
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
|
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
|
||||||
implementation("com.google.guava:guava:33.0.0-android")
|
implementation("com.google.guava:guava:33.2.1-android")
|
||||||
implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
|
implementation("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
|
||||||
|
|
||||||
// Media 3 (ExoPlayer)
|
// Media 3 (ExoPlayer)
|
||||||
implementation("androidx.media3:media3-ui:1.1.1")
|
implementation("androidx.media3:media3-ui:1.1.1")
|
||||||
|
|
@ -204,9 +204,9 @@ dependencies {
|
||||||
// PlayBack
|
// PlayBack
|
||||||
implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker
|
implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker
|
||||||
implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs
|
implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs
|
||||||
implementation("com.github.teamnewpipe:NewPipeExtractor:6dc25f7") /* For Trailers
|
implementation("com.github.teamnewpipe:NewPipeExtractor:176da72") /* For Trailers
|
||||||
^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */
|
^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */
|
||||||
implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding
|
implementation("com.github.albfernandez:juniversalchardet:2.5.0") // Subtitle Decoding
|
||||||
|
|
||||||
// Crash Reports (AcraApplication.kt)
|
// Crash Reports (AcraApplication.kt)
|
||||||
implementation("ch.acra:acra-core:5.11.3")
|
implementation("ch.acra:acra-core:5.11.3")
|
||||||
|
|
@ -217,18 +217,17 @@ dependencies {
|
||||||
implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors
|
implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors
|
||||||
implementation("androidx.tvprovider:tvprovider:1.0.0")
|
implementation("androidx.tvprovider:tvprovider:1.0.0")
|
||||||
implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures
|
implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures
|
||||||
implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication
|
implementation("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication
|
||||||
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview
|
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview
|
||||||
|
implementation("io.github.g0dkar:qrcode-kotlin:4.2.0") // QR code for PIN Auth on TV
|
||||||
|
|
||||||
// Extensions & Other Libs
|
// Extensions & Other Libs
|
||||||
implementation("org.mozilla:rhino:1.7.13") /* run JavaScript
|
implementation("org.mozilla:rhino:1.7.15") // run JavaScript
|
||||||
^ Don't Bump RhinoJS to 1.7.14,`NoClassDefFoundError` Occurs and Trailers won't play (even with Desugaring)
|
|
||||||
NewPipeExtractor Issue */
|
|
||||||
implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance
|
implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance
|
||||||
implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery
|
implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery
|
||||||
implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9
|
implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9
|
||||||
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.10.0") // TMDB API v3 Wrapper Made with RetroFit
|
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.11.0") // TMDB API v3 Wrapper Made with RetroFit
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.0.4") //nio flavor needed for NewPipeExtractor
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser
|
||||||
^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API
|
^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API
|
||||||
Level 25 or Less. */
|
Level 25 or Less. */
|
||||||
|
|
@ -237,18 +236,46 @@ dependencies {
|
||||||
implementation("androidx.work:work-runtime:2.9.0")
|
implementation("androidx.work:work-runtime:2.9.0")
|
||||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||||
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
|
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
|
||||||
|
|
||||||
|
implementation(project(":library") {
|
||||||
|
// There does not seem to be a good way of getting the android flavor.
|
||||||
|
val isDebug = gradle.startParameter.taskRequests.any { task ->
|
||||||
|
task.args.any { arg ->
|
||||||
|
arg.contains("debug", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.extra.set("isDebug", isDebug)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("androidSourcesJar", Jar::class) {
|
tasks.register<Jar>("androidSourcesJar") {
|
||||||
archiveClassifier.set("sources")
|
archiveClassifier.set("sources")
|
||||||
from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources
|
from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources
|
||||||
}
|
}
|
||||||
|
|
||||||
// For GradLew Plugin
|
tasks.register<Copy>("copyJar") {
|
||||||
tasks.register("makeJar", Copy::class) {
|
from(
|
||||||
from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
|
"build/intermediates/compile_app_classes_jar/prereleaseDebug",
|
||||||
into("build")
|
"../library/build/libs"
|
||||||
include("classes.jar")
|
)
|
||||||
|
into("build/app-classes")
|
||||||
|
include("classes.jar", "library-jvm*.jar")
|
||||||
|
// Remove the version
|
||||||
|
rename("library-jvm.*.jar", "library-jvm.jar")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge the app classes and the library classes into classes.jar
|
||||||
|
tasks.register<Jar>("makeJar") {
|
||||||
|
// Duplicates cause hard to catch errors, better to fail at compile time.
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.FAIL
|
||||||
|
dependsOn(tasks.getByName("copyJar"))
|
||||||
|
from(
|
||||||
|
zipTree("build/app-classes/classes.jar"),
|
||||||
|
zipTree("build/app-classes/library-jvm.jar")
|
||||||
|
)
|
||||||
|
destinationDirectory.set(layout.buildDirectory)
|
||||||
|
archivesName = "classes"
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile> {
|
tasks.withType<KotlinCompile> {
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ class ExampleInstrumentedTest {
|
||||||
fun providerCorrectHomepage() {
|
fun providerCorrectHomepage() {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
getAllProviders().toList().amap { api ->
|
getAllProviders().toList().amap { api ->
|
||||||
TestingUtils.testHomepage(api, ::println)
|
TestingUtils.testHomepage(api, TestingUtils.Logger())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println("Done providerCorrectHomepage")
|
println("Done providerCorrectHomepage")
|
||||||
|
|
@ -166,7 +166,6 @@ class ExampleInstrumentedTest {
|
||||||
TestingUtils.getDeferredProviderTests(
|
TestingUtils.getDeferredProviderTests(
|
||||||
this,
|
this,
|
||||||
getAllProviders(),
|
getAllProviders(),
|
||||||
::println
|
|
||||||
) { _, _ -> }
|
) { _, _ -> }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@
|
||||||
-->
|
-->
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,14 @@ import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import com.lagradost.api.setContext
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
||||||
|
|
@ -31,8 +34,8 @@ import org.acra.sender.ReportSenderFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.PrintStream
|
import java.io.PrintStream
|
||||||
import java.lang.Exception
|
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.Locale
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
|
@ -79,14 +82,8 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
|
||||||
ACRA.errorReporter.handleException(error)
|
ACRA.errorReporter.handleException(error)
|
||||||
try {
|
try {
|
||||||
PrintStream(errorFile).use { ps ->
|
PrintStream(errorFile).use { ps ->
|
||||||
ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}"))
|
ps.println("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")
|
||||||
ps.println(
|
ps.println("Fatal exception on thread ${thread.name} (${thread.id})")
|
||||||
String.format(
|
|
||||||
"Fatal exception on thread %s (%d)",
|
|
||||||
thread.name,
|
|
||||||
thread.id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
error.printStackTrace(ps)
|
error.printStackTrace(ps)
|
||||||
}
|
}
|
||||||
} catch (ignored: FileNotFoundException) {
|
} catch (ignored: FileNotFoundException) {
|
||||||
|
|
@ -104,7 +101,6 @@ class AcraApplication : Application() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
//NativeCrashHandler.initCrashHandler()
|
|
||||||
ExceptionHandler(filesDir.resolve("last_error")) {
|
ExceptionHandler(filesDir.resolve("last_error")) {
|
||||||
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
||||||
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
||||||
|
|
@ -150,6 +146,7 @@ class AcraApplication : Application() {
|
||||||
get() = _context?.get()
|
get() = _context?.get()
|
||||||
private set(value) {
|
private set(value) {
|
||||||
_context = WeakReference(value)
|
_context = WeakReference(value)
|
||||||
|
setContext(WeakReference(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : Any> getKeyClass(path: String, valueType: Class<T>): T? {
|
fun <T : Any> getKeyClass(path: String, valueType: Class<T>): T? {
|
||||||
|
|
@ -211,7 +208,7 @@ class AcraApplication : Application() {
|
||||||
fun openBrowser(url: String, activity: FragmentActivity?) {
|
fun openBrowser(url: String, activity: FragmentActivity?) {
|
||||||
openBrowser(
|
openBrowser(
|
||||||
url,
|
url,
|
||||||
isTvSettings(),
|
isLayout(TV or EMULATOR),
|
||||||
activity?.supportFragmentManager?.fragments?.lastOrNull()
|
activity?.supportFragmentManager?.fragments?.lastOrNull()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,16 @@ import android.app.Activity
|
||||||
import android.app.PictureInPictureParams
|
import android.app.PictureInPictureParams
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.View.NO_ID
|
import android.view.View.NO_ID
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
|
@ -31,12 +30,14 @@ import com.google.android.material.chip.ChipGroup
|
||||||
import com.google.android.material.navigationrail.NavigationRailView
|
import com.google.android.material.navigationrail.NavigationRailView
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
|
import com.lagradost.cloudstream3.MainActivity.Companion.resumeApps
|
||||||
|
import com.lagradost.cloudstream3.databinding.ToastBinding
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.ui.player.PlayerEventType
|
import com.lagradost.cloudstream3.ui.player.PlayerEventType
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||||
import com.lagradost.cloudstream3.ui.result.UiText
|
import com.lagradost.cloudstream3.ui.result.UiText
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
|
import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
import com.lagradost.cloudstream3.utils.Event
|
import com.lagradost.cloudstream3.utils.Event
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper
|
import com.lagradost.cloudstream3.utils.UIHelper
|
||||||
|
|
@ -99,8 +100,7 @@ object CommonActivity {
|
||||||
var playerEventListener: ((PlayerEventType) -> Unit)? = null
|
var playerEventListener: ((PlayerEventType) -> Unit)? = null
|
||||||
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null
|
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null
|
||||||
|
|
||||||
|
private var currentToast: Toast? = null
|
||||||
var currentToast: Toast? = null
|
|
||||||
|
|
||||||
fun showToast(@StringRes message: Int, duration: Int? = null) {
|
fun showToast(@StringRes message: Int, duration: Int? = null) {
|
||||||
val act = activity ?: return
|
val act = activity ?: return
|
||||||
|
|
@ -156,25 +156,19 @@ object CommonActivity {
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val inflater =
|
val binding = ToastBinding.inflate(act.layoutInflater)
|
||||||
act.getSystemService(AppCompatActivity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
binding.text.text = message.trim()
|
||||||
|
|
||||||
val layout: View = inflater.inflate(
|
|
||||||
R.layout.toast,
|
|
||||||
act.findViewById<View>(R.id.toast_layout_root) as ViewGroup?
|
|
||||||
)
|
|
||||||
|
|
||||||
val text = layout.findViewById(R.id.text) as TextView
|
|
||||||
text.text = message.trim()
|
|
||||||
|
|
||||||
|
// custom toasts are deprecated and won't appear when cs3 sets minSDK to api30 (A11)
|
||||||
val toast = Toast(act)
|
val toast = Toast(act)
|
||||||
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
|
|
||||||
toast.duration = duration ?: Toast.LENGTH_SHORT
|
toast.duration = duration ?: Toast.LENGTH_SHORT
|
||||||
toast.view = layout
|
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
|
||||||
//https://github.com/PureWriter/ToastCompat
|
toast.view = binding.root //fixme Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version.
|
||||||
toast.show()
|
|
||||||
currentToast = toast
|
currentToast = toast
|
||||||
|
toast.show()
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
|
|
@ -283,12 +277,35 @@ object CommonActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateTheme(act: Activity) {
|
||||||
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
|
||||||
|
if (settingsManager
|
||||||
|
.getString(act.getString(R.string.app_theme_key), "AmoledLight") == "System"
|
||||||
|
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
loadThemes(act)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapSystemTheme(act: Activity): Int {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
val currentNightMode =
|
||||||
|
act.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||||
|
return when (currentNightMode) {
|
||||||
|
Configuration.UI_MODE_NIGHT_NO -> R.style.LightMode // Night mode is not active, we're using the light theme
|
||||||
|
else -> R.style.AppTheme // Night mode is active, we're using dark theme
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return R.style.AppTheme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun loadThemes(act: Activity?) {
|
fun loadThemes(act: Activity?) {
|
||||||
if (act == null) return
|
if (act == null) return
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
|
||||||
|
|
||||||
val currentTheme =
|
val currentTheme =
|
||||||
when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) {
|
when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) {
|
||||||
|
"System" -> mapSystemTheme(act)
|
||||||
"Black" -> R.style.AppTheme
|
"Black" -> R.style.AppTheme
|
||||||
"Light" -> R.style.LightMode
|
"Light" -> R.style.LightMode
|
||||||
"Amoled" -> R.style.AmoledMode
|
"Amoled" -> R.style.AmoledMode
|
||||||
|
|
@ -359,8 +376,8 @@ object CommonActivity {
|
||||||
currentLook = currentLook.parent as? View ?: break
|
currentLook = currentLook.parent as? View ?: break
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
private fun View.hasContent() : Boolean {
|
private fun View.hasContent(): Boolean {
|
||||||
return isShown && when(this) {
|
return isShown && when (this) {
|
||||||
//is RecyclerView -> this.childCount > 0
|
//is RecyclerView -> this.childCount > 0
|
||||||
is ViewGroup -> this.childCount > 0
|
is ViewGroup -> this.childCount > 0
|
||||||
else -> true
|
else -> true
|
||||||
|
|
@ -471,20 +488,6 @@ object CommonActivity {
|
||||||
|
|
||||||
|
|
||||||
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
|
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
|
||||||
//println("Keycode: $keyCode")
|
|
||||||
//showToast(
|
|
||||||
// this,
|
|
||||||
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
|
|
||||||
// Toast.LENGTH_LONG
|
|
||||||
//)
|
|
||||||
|
|
||||||
// Tested keycodes on remote:
|
|
||||||
// KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|
|
||||||
// KeyEvent.KEYCODE_MEDIA_REWIND
|
|
||||||
// KeyEvent.KEYCODE_MENU
|
|
||||||
// KeyEvent.KEYCODE_MEDIA_NEXT
|
|
||||||
// KeyEvent.KEYCODE_MEDIA_PREVIOUS
|
|
||||||
// KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|
|
||||||
|
|
||||||
// 149 keycode_numpad 5
|
// 149 keycode_numpad 5
|
||||||
when (keyCode) {
|
when (keyCode) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.lagradost.cloudstream3
|
||||||
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader
|
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||||
import org.schabi.newpipe.extractor.downloader.Request
|
import org.schabi.newpipe.extractor.downloader.Request
|
||||||
import org.schabi.newpipe.extractor.downloader.Response
|
import org.schabi.newpipe.extractor.downloader.Response
|
||||||
|
|
@ -10,7 +11,7 @@ import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
|
||||||
class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() {
|
class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() {
|
||||||
private val client: OkHttpClient
|
private val client: OkHttpClient = builder.readTimeout(30, TimeUnit.SECONDS).build()
|
||||||
override fun execute(request: Request): Response {
|
override fun execute(request: Request): Response {
|
||||||
val httpMethod: String = request.httpMethod()
|
val httpMethod: String = request.httpMethod()
|
||||||
val url: String = request.url()
|
val url: String = request.url()
|
||||||
|
|
@ -18,7 +19,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
|
||||||
val dataToSend: ByteArray? = request.dataToSend()
|
val dataToSend: ByteArray? = request.dataToSend()
|
||||||
var requestBody: RequestBody? = null
|
var requestBody: RequestBody? = null
|
||||||
if (dataToSend != null) {
|
if (dataToSend != null) {
|
||||||
requestBody = RequestBody.create(null, dataToSend)
|
requestBody = dataToSend.toRequestBody(null, 0, dataToSend.size)
|
||||||
}
|
}
|
||||||
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
||||||
.method(httpMethod, requestBody).url(url)
|
.method(httpMethod, requestBody).url(url)
|
||||||
|
|
@ -73,8 +74,4 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
|
||||||
client = builder.readTimeout(30, TimeUnit.SECONDS).build()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -19,7 +19,6 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import android.widget.Toast.LENGTH_LONG
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
|
|
@ -45,9 +44,6 @@ import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearSnapHelper
|
import androidx.recyclerview.widget.LinearSnapHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|
||||||
import com.google.android.gms.cast.framework.CastContext
|
import com.google.android.gms.cast.framework.CastContext
|
||||||
import com.google.android.gms.cast.framework.Session
|
import com.google.android.gms.cast.framework.Session
|
||||||
import com.google.android.gms.cast.framework.SessionManager
|
import com.google.android.gms.cast.framework.SessionManager
|
||||||
|
|
@ -60,9 +56,7 @@ import com.google.common.collect.Comparators.min
|
||||||
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
|
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
|
||||||
import com.lagradost.cloudstream3.APIHolder.allProviders
|
import com.lagradost.cloudstream3.APIHolder.allProviders
|
||||||
import com.lagradost.cloudstream3.APIHolder.apis
|
import com.lagradost.cloudstream3.APIHolder.apis
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
|
||||||
import com.lagradost.cloudstream3.APIHolder.initAll
|
import com.lagradost.cloudstream3.APIHolder.initAll
|
||||||
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
|
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
|
|
@ -74,6 +68,7 @@ import com.lagradost.cloudstream3.CommonActivity.screenHeight
|
||||||
import com.lagradost.cloudstream3.CommonActivity.setActivityInstance
|
import com.lagradost.cloudstream3.CommonActivity.setActivityInstance
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.CommonActivity.updateLocale
|
import com.lagradost.cloudstream3.CommonActivity.updateLocale
|
||||||
|
import com.lagradost.cloudstream3.CommonActivity.updateTheme
|
||||||
import com.lagradost.cloudstream3.databinding.ActivityMainBinding
|
import com.lagradost.cloudstream3.databinding.ActivityMainBinding
|
||||||
import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding
|
import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding
|
||||||
import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding
|
import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding
|
||||||
|
|
@ -87,20 +82,23 @@ import com.lagradost.cloudstream3.plugins.PluginManager
|
||||||
import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
|
import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
|
||||||
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
|
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
|
||||||
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
|
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
|
||||||
|
import com.lagradost.cloudstream3.services.SubscriptionWorkManager
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_PLAYER
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_REPO
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_RESUME_WATCHING
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_SEARCH
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringPlayer
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.localListApi
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository
|
import com.lagradost.cloudstream3.ui.APIRepository
|
||||||
import com.lagradost.cloudstream3.ui.SyncWatchType
|
import com.lagradost.cloudstream3.ui.SyncWatchType
|
||||||
import com.lagradost.cloudstream3.ui.WatchType
|
import com.lagradost.cloudstream3.ui.WatchType
|
||||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
|
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
|
||||||
import com.lagradost.cloudstream3.ui.home.HomeViewModel
|
import com.lagradost.cloudstream3.ui.home.HomeViewModel
|
||||||
|
import com.lagradost.cloudstream3.ui.library.LibraryViewModel
|
||||||
import com.lagradost.cloudstream3.ui.player.BasicLink
|
import com.lagradost.cloudstream3.ui.player.BasicLink
|
||||||
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
||||||
import com.lagradost.cloudstream3.ui.player.LinkGenerator
|
import com.lagradost.cloudstream3.ui.player.LinkGenerator
|
||||||
|
|
@ -110,32 +108,40 @@ import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
|
||||||
import com.lagradost.cloudstream3.ui.result.SyncViewModel
|
import com.lagradost.cloudstream3.ui.result.SyncViewModel
|
||||||
import com.lagradost.cloudstream3.ui.result.setImage
|
import com.lagradost.cloudstream3.ui.result.setImage
|
||||||
import com.lagradost.cloudstream3.ui.result.setText
|
import com.lagradost.cloudstream3.ui.result.setText
|
||||||
|
import com.lagradost.cloudstream3.ui.result.setTextHtml
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchFragment
|
import com.lagradost.cloudstream3.ui.search.SearchFragment
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTruePhone
|
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
|
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
|
||||||
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
|
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
|
||||||
import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
|
import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
|
||||||
import com.lagradost.cloudstream3.utils.ApkInstaller
|
import com.lagradost.cloudstream3.utils.ApkInstaller
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.html
|
import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
|
import com.lagradost.cloudstream3.utils.AppContextUtils.html
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isLtr
|
import com.lagradost.cloudstream3.utils.AppContextUtils.isCastApiAvailable
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isNetworkAvailable
|
import com.lagradost.cloudstream3.utils.AppContextUtils.isLtr
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
|
import com.lagradost.cloudstream3.utils.AppContextUtils.isNetworkAvailable
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
|
import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
|
import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
import com.lagradost.cloudstream3.utils.AppContextUtils.loadRepository
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
|
import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers
|
||||||
|
import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback
|
||||||
|
import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback
|
||||||
import com.lagradost.cloudstream3.utils.BackupUtils.backup
|
import com.lagradost.cloudstream3.utils.BackupUtils.backup
|
||||||
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
|
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
|
||||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator
|
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback
|
||||||
|
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt
|
||||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
|
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
|
||||||
|
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled
|
||||||
|
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo
|
||||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
|
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
|
|
@ -147,6 +153,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
|
||||||
import com.lagradost.cloudstream3.utils.Event
|
import com.lagradost.cloudstream3.utils.Event
|
||||||
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
|
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||||
|
import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
|
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
|
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||||
|
|
@ -158,8 +165,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.requestRW
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
||||||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
||||||
import com.lagradost.nicehttp.Requests
|
import com.lagradost.cloudstream3.utils.fcast.FcastManager
|
||||||
import com.lagradost.nicehttp.ResponseParser
|
|
||||||
import com.lagradost.safefile.SafeFile
|
import com.lagradost.safefile.SafeFile
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
@ -170,7 +176,6 @@ import java.net.URLDecoder
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.reflect.KClass
|
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
//https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
|
//https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
|
||||||
|
|
@ -183,116 +188,92 @@ import kotlin.system.exitProcess
|
||||||
|
|
||||||
//https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225
|
//https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225
|
||||||
|
|
||||||
const val VLC_PACKAGE = "org.videolan.vlc"
|
class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCallback {
|
||||||
const val MPV_PACKAGE = "is.xyz.mpv"
|
|
||||||
const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo"
|
|
||||||
|
|
||||||
val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity")
|
|
||||||
val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
|
|
||||||
|
|
||||||
//TODO REFACTOR AF
|
|
||||||
open class ResultResume(
|
|
||||||
val packageString: String,
|
|
||||||
val action: String = Intent.ACTION_VIEW,
|
|
||||||
val position: String? = null,
|
|
||||||
val duration: String? = null,
|
|
||||||
var launcher: ActivityResultLauncher<Intent>? = null,
|
|
||||||
) {
|
|
||||||
val defaultTime = -1L
|
|
||||||
|
|
||||||
val lastId get() = "${packageString}_last_open_id"
|
|
||||||
suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
|
|
||||||
val intent = Intent(action)
|
|
||||||
|
|
||||||
if (id != null)
|
|
||||||
setKey(lastId, id)
|
|
||||||
else
|
|
||||||
removeKey(lastId)
|
|
||||||
|
|
||||||
intent.setPackage(packageString)
|
|
||||||
callback.invoke(intent)
|
|
||||||
launcher?.launch(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun getPosition(intent: Intent?): Long {
|
|
||||||
return defaultTime
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun getDuration(intent: Intent?): Long {
|
|
||||||
return defaultTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val VLC = object : ResultResume(
|
|
||||||
VLC_PACKAGE,
|
|
||||||
// Android 13 intent restrictions fucks up specifically launching the VLC player
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
"org.videolan.vlc.player.result"
|
|
||||||
} else {
|
|
||||||
Intent.ACTION_VIEW
|
|
||||||
},
|
|
||||||
"extra_position",
|
|
||||||
"extra_duration",
|
|
||||||
) {
|
|
||||||
override fun getPosition(intent: Intent?): Long {
|
|
||||||
return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDuration(intent: Intent?): Long {
|
|
||||||
return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val MPV = object : ResultResume(
|
|
||||||
MPV_PACKAGE,
|
|
||||||
//"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
|
|
||||||
position = "position",
|
|
||||||
duration = "duration",
|
|
||||||
) {
|
|
||||||
override fun getPosition(intent: Intent?): Long {
|
|
||||||
return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDuration(intent: Intent?): Long {
|
|
||||||
return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
|
|
||||||
|
|
||||||
val resumeApps = arrayOf(
|
|
||||||
VLC, MPV, WEB_VIDEO
|
|
||||||
)
|
|
||||||
|
|
||||||
// Short name for requests client to make it nicer to use
|
|
||||||
|
|
||||||
var app = Requests(responseParser = object : ResponseParser {
|
|
||||||
val mapper: ObjectMapper = jacksonObjectMapper().configure(
|
|
||||||
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun <T : Any> parse(text: String, kClass: KClass<T>): T {
|
|
||||||
return mapper.readValue(text, kClass.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun <T : Any> parseSafe(text: String, kClass: KClass<T>): T? {
|
|
||||||
return try {
|
|
||||||
mapper.readValue(text, kClass.java)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun writeValueAsString(obj: Any): String {
|
|
||||||
return mapper.writeValueAsString(obj)
|
|
||||||
}
|
|
||||||
}).apply {
|
|
||||||
defaultHeaders = mapOf("user-agent" to USER_AGENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAuthenticator.BiometricAuthCallback {
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val VLC_PACKAGE = "org.videolan.vlc"
|
||||||
|
const val MPV_PACKAGE = "is.xyz.mpv"
|
||||||
|
const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo"
|
||||||
|
|
||||||
|
val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity")
|
||||||
|
val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
|
||||||
|
|
||||||
|
//TODO REFACTOR AF
|
||||||
|
open class ResultResume(
|
||||||
|
val packageString: String,
|
||||||
|
val action: String = Intent.ACTION_VIEW,
|
||||||
|
val position: String? = null,
|
||||||
|
val duration: String? = null,
|
||||||
|
var launcher: ActivityResultLauncher<Intent>? = null,
|
||||||
|
) {
|
||||||
|
val defaultTime = -1L
|
||||||
|
|
||||||
|
val lastId get() = "${packageString}_last_open_id"
|
||||||
|
suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
|
||||||
|
val intent = Intent(action)
|
||||||
|
|
||||||
|
if (id != null)
|
||||||
|
setKey(lastId, id)
|
||||||
|
else
|
||||||
|
removeKey(lastId)
|
||||||
|
|
||||||
|
intent.setPackage(packageString)
|
||||||
|
callback.invoke(intent)
|
||||||
|
launcher?.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getPosition(intent: Intent?): Long {
|
||||||
|
return defaultTime
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getDuration(intent: Intent?): Long {
|
||||||
|
return defaultTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val VLC = object : ResultResume(
|
||||||
|
VLC_PACKAGE,
|
||||||
|
// Android 13 intent restrictions fucks up specifically launching the VLC player
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
"org.videolan.vlc.player.result"
|
||||||
|
} else {
|
||||||
|
Intent.ACTION_VIEW
|
||||||
|
},
|
||||||
|
"extra_position",
|
||||||
|
"extra_duration",
|
||||||
|
) {
|
||||||
|
override fun getPosition(intent: Intent?): Long {
|
||||||
|
return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDuration(intent: Intent?): Long {
|
||||||
|
return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val MPV = object : ResultResume(
|
||||||
|
MPV_PACKAGE,
|
||||||
|
//"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
|
||||||
|
position = "position",
|
||||||
|
duration = "duration",
|
||||||
|
) {
|
||||||
|
override fun getPosition(intent: Intent?): Long {
|
||||||
|
return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong()
|
||||||
|
?: defaultTime
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDuration(intent: Intent?): Long {
|
||||||
|
return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong()
|
||||||
|
?: defaultTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
|
||||||
|
|
||||||
|
val resumeApps = arrayOf(
|
||||||
|
VLC, MPV, WEB_VIDEO
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
const val TAG = "MAINACT"
|
const val TAG = "MAINACT"
|
||||||
const val ANIMATED_OUTLINE: Boolean = false
|
const val ANIMATED_OUTLINE: Boolean = false
|
||||||
var lastError: String? = null
|
var lastError: String? = null
|
||||||
|
|
@ -337,10 +318,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
|
|
||||||
// kinda shitty solution, but cant com main->home otherwise for popups
|
// kinda shitty solution, but cant com main->home otherwise for popups
|
||||||
val bookmarksUpdatedEvent = Event<Boolean>()
|
val bookmarksUpdatedEvent = Event<Boolean>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used by DataStoreHelper to fully reload home when switching accounts
|
* Used by DataStoreHelper to fully reload home when switching accounts
|
||||||
*/
|
*/
|
||||||
val reloadHomeEvent = Event<Boolean>()
|
val reloadHomeEvent = Event<Boolean>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used by DataStoreHelper to fully reload library when switching accounts
|
* Used by DataStoreHelper to fully reload library when switching accounts
|
||||||
*/
|
*/
|
||||||
|
|
@ -368,7 +351,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
println("Repository url: $realUrl")
|
println("Repository url: $realUrl")
|
||||||
loadRepository(realUrl)
|
loadRepository(realUrl)
|
||||||
return true
|
return true
|
||||||
} else if (str.contains(appString)) {
|
} else if (str.contains(APP_STRING)) {
|
||||||
for (api in OAuth2Apis) {
|
for (api in OAuth2Apis) {
|
||||||
if (str.contains("/${api.redirectUrl}")) {
|
if (str.contains("/${api.redirectUrl}")) {
|
||||||
ioSafe {
|
ioSafe {
|
||||||
|
|
@ -398,15 +381,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
}
|
}
|
||||||
// This specific intent is used for the gradle deployWithAdb
|
// This specific intent is used for the gradle deployWithAdb
|
||||||
// https://github.com/recloudstream/gradle/blob/master/src/main/kotlin/com/lagradost/cloudstream3/gradle/tasks/DeployWithAdbTask.kt#L46
|
// https://github.com/recloudstream/gradle/blob/master/src/main/kotlin/com/lagradost/cloudstream3/gradle/tasks/DeployWithAdbTask.kt#L46
|
||||||
if (str == "$appString:") {
|
if (str == "$APP_STRING:") {
|
||||||
PluginManager.hotReloadAllLocalPlugins(activity)
|
PluginManager.hotReloadAllLocalPlugins(activity)
|
||||||
}
|
}
|
||||||
} else if (safeURI(str)?.scheme == appStringRepo) {
|
} else if (safeURI(str)?.scheme == APP_STRING_REPO) {
|
||||||
val url = str.replaceFirst(appStringRepo, "https")
|
val url = str.replaceFirst(APP_STRING_REPO, "https")
|
||||||
loadRepository(url)
|
loadRepository(url)
|
||||||
return true
|
return true
|
||||||
} else if (safeURI(str)?.scheme == appStringSearch) {
|
} else if (safeURI(str)?.scheme == APP_STRING_SEARCH) {
|
||||||
val query = str.substringAfter("$appStringSearch://")
|
val query = str.substringAfter("$APP_STRING_SEARCH://")
|
||||||
nextSearchQuery =
|
nextSearchQuery =
|
||||||
try {
|
try {
|
||||||
URLDecoder.decode(query, "UTF-8")
|
URLDecoder.decode(query, "UTF-8")
|
||||||
|
|
@ -420,7 +403,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
R.id.navigation_search
|
R.id.navigation_search
|
||||||
activity?.findViewById<NavigationRailView>(R.id.nav_rail_view)?.selectedItemId =
|
activity?.findViewById<NavigationRailView>(R.id.nav_rail_view)?.selectedItemId =
|
||||||
R.id.navigation_search
|
R.id.navigation_search
|
||||||
} else if (safeURI(str)?.scheme == appStringPlayer) {
|
} else if (safeURI(str)?.scheme == APP_STRING_PLAYER) {
|
||||||
val uri = Uri.parse(str)
|
val uri = Uri.parse(str)
|
||||||
val name = uri.getQueryParameter("name")
|
val name = uri.getQueryParameter("name")
|
||||||
val url = URLDecoder.decode(uri.authority, "UTF-8")
|
val url = URLDecoder.decode(uri.authority, "UTF-8")
|
||||||
|
|
@ -434,9 +417,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else if (safeURI(str)?.scheme == appStringResumeWatching) {
|
} else if (safeURI(str)?.scheme == APP_STRING_RESUME_WATCHING) {
|
||||||
val id =
|
val id =
|
||||||
str.substringAfter("$appStringResumeWatching://").toIntOrNull()
|
str.substringAfter("$APP_STRING_RESUME_WATCHING://").toIntOrNull()
|
||||||
?: return false
|
?: return false
|
||||||
ioSafe {
|
ioSafe {
|
||||||
val resumeWatchingCard =
|
val resumeWatchingCard =
|
||||||
|
|
@ -468,7 +451,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastPopup: SearchResponse? = null
|
var lastPopup: SearchResponse? = null
|
||||||
fun loadPopup(result: SearchResponse, load : Boolean = true) {
|
fun loadPopup(result: SearchResponse, load: Boolean = true) {
|
||||||
lastPopup = result
|
lastPopup = result
|
||||||
val syncName = syncViewModel.syncName(result.apiName)
|
val syncName = syncViewModel.syncName(result.apiName)
|
||||||
|
|
||||||
|
|
@ -489,8 +472,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
.contains(DubStatus.Dubbed)
|
.contains(DubStatus.Dubbed)
|
||||||
) DubStatus.Dubbed else DubStatus.Subbed, null
|
) DubStatus.Dubbed else DubStatus.Subbed, null
|
||||||
)
|
)
|
||||||
}else {
|
} else {
|
||||||
viewModel.loadSmall(this,result)
|
viewModel.loadSmall(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -505,6 +488,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
updateLocale() // android fucks me by chaining lang when rotating the phone
|
updateLocale() // android fucks me by chaining lang when rotating the phone
|
||||||
|
updateTheme(this) // Update if system theme
|
||||||
|
|
||||||
val navHostFragment =
|
val navHostFragment =
|
||||||
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
||||||
|
|
@ -555,7 +539,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
binding?.navHostFragment?.apply {
|
binding?.navHostFragment?.apply {
|
||||||
val params = layoutParams as ConstraintLayout.LayoutParams
|
val params = layoutParams as ConstraintLayout.LayoutParams
|
||||||
val push =
|
val push =
|
||||||
if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0
|
if (!dontPush && isLayout(TV or EMULATOR)) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0
|
||||||
|
|
||||||
if (!this.isLtr()) {
|
if (!this.isLtr()) {
|
||||||
params.setMargins(
|
params.setMargins(
|
||||||
|
|
@ -582,30 +566,51 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
}
|
}
|
||||||
|
|
||||||
Configuration.ORIENTATION_PORTRAIT -> {
|
Configuration.ORIENTATION_PORTRAIT -> {
|
||||||
isTvSettings()
|
isLayout(TV or EMULATOR)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding?.apply {
|
binding?.apply {
|
||||||
navView.isVisible = isNavVisible && !landscape
|
|
||||||
navRailView.isVisible = isNavVisible && landscape
|
navRailView.isVisible = isNavVisible && landscape
|
||||||
|
navView.isVisible = isNavVisible && !landscape
|
||||||
|
|
||||||
// Hide library on TV since it is not supported yet :(
|
/**
|
||||||
//val isTrueTv = isTrueTvSettings()
|
* We need to make sure if we return to a sub-fragment,
|
||||||
//navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
|
* the correct navigation item is selected so that it does not
|
||||||
//navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
|
* highlight the wrong one in UI.
|
||||||
|
*/
|
||||||
// Hide downloads on TV
|
when (destination.id) {
|
||||||
//navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv
|
in listOf(R.id.navigation_downloads, R.id.navigation_download_child) -> {
|
||||||
//navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv
|
navRailView.menu.findItem(R.id.navigation_downloads).isChecked = true
|
||||||
|
navView.menu.findItem(R.id.navigation_downloads).isChecked = true
|
||||||
|
}
|
||||||
|
in listOf(
|
||||||
|
R.id.navigation_settings,
|
||||||
|
R.id.navigation_subtitles,
|
||||||
|
R.id.navigation_chrome_subtitles,
|
||||||
|
R.id.navigation_settings_player,
|
||||||
|
R.id.navigation_settings_updates,
|
||||||
|
R.id.navigation_settings_ui,
|
||||||
|
R.id.navigation_settings_account,
|
||||||
|
R.id.navigation_settings_providers,
|
||||||
|
R.id.navigation_settings_general,
|
||||||
|
R.id.navigation_settings_extensions,
|
||||||
|
R.id.navigation_settings_plugins,
|
||||||
|
R.id.navigation_test_providers
|
||||||
|
) -> {
|
||||||
|
navRailView.menu.findItem(R.id.navigation_settings).isChecked = true
|
||||||
|
navView.menu.findItem(R.id.navigation_settings).isChecked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//private var mCastSession: CastSession? = null
|
//private var mCastSession: CastSession? = null
|
||||||
lateinit var mSessionManager: SessionManager
|
var mSessionManager: SessionManager? = null
|
||||||
private val mSessionManagerListener: SessionManagerListener<Session> by lazy { SessionManagerListenerImpl() }
|
private val mSessionManagerListener: SessionManagerListener<Session> by lazy { SessionManagerListenerImpl() }
|
||||||
|
|
||||||
private inner class SessionManagerListenerImpl : SessionManagerListener<Session> {
|
private inner class SessionManagerListenerImpl : SessionManagerListener<Session> {
|
||||||
|
|
@ -645,8 +650,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
setActivityInstance(this)
|
setActivityInstance(this)
|
||||||
try {
|
try {
|
||||||
if (isCastApiAvailable()) {
|
if (isCastApiAvailable()) {
|
||||||
//mCastSession = mSessionManager.currentCastSession
|
mSessionManager?.addSessionManagerListener(mSessionManagerListener)
|
||||||
mSessionManager.addSessionManagerListener(mSessionManagerListener)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
|
|
@ -662,7 +666,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (isCastApiAvailable()) {
|
if (isCastApiAvailable()) {
|
||||||
mSessionManager.removeSessionManagerListener(mSessionManagerListener)
|
mSessionManager?.removeSessionManagerListener(mSessionManagerListener)
|
||||||
//mCastSession = null
|
//mCastSession = null
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
@ -670,7 +674,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
val response = CommonActivity.dispatchKeyEvent(this, event)
|
val response = CommonActivity.dispatchKeyEvent(this, event)
|
||||||
if (response != null)
|
if (response != null)
|
||||||
return response
|
return response
|
||||||
|
|
@ -766,7 +770,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
list.forEach { custom ->
|
list.forEach { custom ->
|
||||||
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
|
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
|
||||||
?.let {
|
?.let {
|
||||||
allProviders.add(it.javaClass.newInstance().apply {
|
allProviders.add(it.javaClass.getDeclaredConstructor().newInstance().apply {
|
||||||
name = custom.name
|
name = custom.name
|
||||||
lang = custom.lang
|
lang = custom.lang
|
||||||
mainUrl = custom.url.trimEnd('/')
|
mainUrl = custom.url.trimEnd('/')
|
||||||
|
|
@ -788,14 +792,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var viewModel: ResultViewModel2
|
lateinit var viewModel: ResultViewModel2
|
||||||
lateinit var syncViewModel : SyncViewModel
|
lateinit var syncViewModel: SyncViewModel
|
||||||
|
private var libraryViewModel: LibraryViewModel? = null
|
||||||
|
|
||||||
/** kinda dirty, however it signals that we should use the watch status as sync or not*/
|
/** kinda dirty, however it signals that we should use the watch status as sync or not*/
|
||||||
var isLocalList : Boolean = false
|
var isLocalList: Boolean = false
|
||||||
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
|
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
|
||||||
viewModel =
|
|
||||||
ViewModelProvider(this)[ResultViewModel2::class.java]
|
viewModel = ViewModelProvider(this)[ResultViewModel2::class.java]
|
||||||
syncViewModel =
|
syncViewModel = ViewModelProvider(this)[SyncViewModel::class.java]
|
||||||
ViewModelProvider(this)[SyncViewModel::class.java]
|
|
||||||
|
|
||||||
return super.onCreateView(name, context, attrs)
|
return super.onCreateView(name, context, attrs)
|
||||||
}
|
}
|
||||||
|
|
@ -1106,8 +1111,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun centerView(view : View?) {
|
private fun centerView(view: View?) {
|
||||||
if(view == null) return
|
if (view == null) return
|
||||||
try {
|
try {
|
||||||
Log.v(TAG, "centerView: $view")
|
Log.v(TAG, "centerView: $view")
|
||||||
val r = Rect(0, 0, 0, 0)
|
val r = Rect(0, 0, 0, 0)
|
||||||
|
|
@ -1146,7 +1151,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
try {
|
try {
|
||||||
if (isCastApiAvailable()) {
|
if (isCastApiAvailable()) {
|
||||||
mSessionManager = CastContext.getSharedInstance(this).sessionManager
|
CastContext.getSharedInstance(this) {it.run()}.addOnSuccessListener { mSessionManager = it.sessionManager }
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
logError(t)
|
logError(t)
|
||||||
|
|
@ -1173,11 +1178,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
|
|
||||||
// just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH
|
// just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH
|
||||||
binding = try {
|
binding = try {
|
||||||
if (isTvSettings()) {
|
if (isLayout(TV or EMULATOR)) {
|
||||||
val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false)
|
val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false)
|
||||||
setContentView(newLocalBinding.root)
|
setContentView(newLocalBinding.root)
|
||||||
|
|
||||||
if (isTrueTvSettings() && ANIMATED_OUTLINE) {
|
if (isLayout(TV) && ANIMATED_OUTLINE) {
|
||||||
TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
|
TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
|
||||||
newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
|
newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
|
||||||
TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
|
TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
|
||||||
|
|
@ -1189,8 +1194,26 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
newLocalBinding.focusOutline.isVisible = false
|
newLocalBinding.focusOutline.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTrueTvSettings()) {
|
if (isLayout(TV)) {
|
||||||
|
// Put here any button you don't want focusing it to center the view
|
||||||
|
val exceptionButtons = listOf(
|
||||||
|
R.id.home_preview_play_btt,
|
||||||
|
R.id.home_preview_info_btt,
|
||||||
|
R.id.home_preview_hidden_next_focus,
|
||||||
|
R.id.home_preview_hidden_prev_focus,
|
||||||
|
R.id.result_play_movie_button,
|
||||||
|
R.id.result_play_series_button,
|
||||||
|
R.id.result_resume_series_button,
|
||||||
|
R.id.result_play_trailer_button,
|
||||||
|
R.id.result_bookmark_Button,
|
||||||
|
R.id.result_favorite_Button,
|
||||||
|
R.id.result_subscribe_Button,
|
||||||
|
R.id.result_search_Button,
|
||||||
|
R.id.result_episodes_show_button,
|
||||||
|
)
|
||||||
|
|
||||||
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
|
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
|
||||||
|
if (exceptionButtons.contains(newFocus?.id)) return@addOnGlobalFocusChangeListener
|
||||||
centerView(newFocus)
|
centerView(newFocus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1206,24 +1229,24 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
changeStatusBarState(isEmulatorSettings())
|
changeStatusBarState(isLayout(EMULATOR))
|
||||||
|
|
||||||
/** Biometric stuff for users without accounts **/
|
/** Biometric stuff for users without accounts **/
|
||||||
val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false)
|
val noAccounts = settingsManager.getBoolean(
|
||||||
val noAccounts = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false) || accounts.count() <= 1
|
getString(R.string.skip_startup_account_select_key),
|
||||||
|
false
|
||||||
|
) || accounts.count() <= 1
|
||||||
|
|
||||||
if (isTruePhone() && authEnabled && noAccounts) {
|
if (isLayout(PHONE) && isAuthEnabled(this) && noAccounts) {
|
||||||
if (deviceHasPasswordPinLock(this)) {
|
if (deviceHasPasswordPinLock(this)) {
|
||||||
startBiometricAuthentication(this, R.string.biometric_authentication_title, false)
|
startBiometricAuthentication(this, R.string.biometric_authentication_title, false)
|
||||||
|
|
||||||
BiometricAuthenticator.promptInfo?.let {
|
promptInfo?.let { prompt ->
|
||||||
BiometricAuthenticator.biometricPrompt?.authenticate(it)
|
biometricPrompt?.authenticate(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// hide background while authenticating, Sorry moms & dads 🙏
|
// hide background while authenticating, Sorry moms & dads 🙏
|
||||||
binding?.navHostFragment?.isInvisible = true
|
binding?.navHostFragment?.isInvisible = true
|
||||||
} else {
|
|
||||||
showToast(R.string.phone_not_secured, LENGTH_LONG)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1234,17 +1257,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
this.setKey(getString(R.string.jsdelivr_proxy_key), false)
|
this.setKey(getString(R.string.jsdelivr_proxy_key), false)
|
||||||
} else {
|
} else {
|
||||||
this.setKey(getString(R.string.jsdelivr_proxy_key), true)
|
this.setKey(getString(R.string.jsdelivr_proxy_key), true)
|
||||||
val parentView: View = findViewById(android.R.id.content)
|
showSnackbar(
|
||||||
Snackbar.make(parentView, R.string.jsdelivr_enabled, Snackbar.LENGTH_LONG)
|
this@MainActivity,
|
||||||
.let { snackbar ->
|
R.string.jsdelivr_enabled,
|
||||||
snackbar.setAction(R.string.revert) {
|
Snackbar.LENGTH_LONG,
|
||||||
setKey(getString(R.string.jsdelivr_proxy_key), false)
|
R.string.revert
|
||||||
}
|
) { setKey(getString(R.string.jsdelivr_proxy_key), false) }
|
||||||
snackbar.setBackgroundTint(colorFromAttribute(R.attr.primaryGrayBackground))
|
|
||||||
snackbar.setTextColor(colorFromAttribute(R.attr.textColor))
|
|
||||||
snackbar.setActionTextColor(colorFromAttribute(R.attr.colorPrimary))
|
|
||||||
snackbar.show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1311,7 +1329,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun setUserData(status : Resource<SyncAPI.AbstractSyncStatus>?) {
|
fun setUserData(status: Resource<SyncAPI.AbstractSyncStatus>?) {
|
||||||
if (isLocalList) return
|
if (isLocalList) return
|
||||||
bottomPreviewBinding?.apply {
|
bottomPreviewBinding?.apply {
|
||||||
when (status) {
|
when (status) {
|
||||||
|
|
@ -1336,7 +1354,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setWatchStatus(state : WatchType?) {
|
fun setWatchStatus(state: WatchType?) {
|
||||||
if (!isLocalList || state == null) return
|
if (!isLocalList || state == null) return
|
||||||
|
|
||||||
bottomPreviewBinding?.resultviewPreviewBookmark?.apply {
|
bottomPreviewBinding?.resultviewPreviewBookmark?.apply {
|
||||||
|
|
@ -1345,13 +1363,42 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
observe(viewModel.watchStatus) { state ->
|
fun setSubscribeStatus(state: Boolean?) {
|
||||||
setWatchStatus(state)
|
bottomPreviewBinding?.resultviewPreviewSubscribe?.apply {
|
||||||
}
|
if (state != null) {
|
||||||
observe(syncViewModel.userData) { status ->
|
val drawable = if (state) {
|
||||||
setUserData(status)
|
R.drawable.ic_baseline_notifications_active_24
|
||||||
|
} else {
|
||||||
|
R.drawable.baseline_notifications_none_24
|
||||||
|
}
|
||||||
|
setImageResource(drawable)
|
||||||
|
}
|
||||||
|
isVisible = state != null
|
||||||
|
|
||||||
|
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) ?: ""
|
||||||
|
showToast(txt(message, name), Toast.LENGTH_SHORT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observe(viewModel.watchStatus, ::setWatchStatus)
|
||||||
|
observe(syncViewModel.userData, ::setUserData)
|
||||||
|
observeNullable(viewModel.subscribeStatus, ::setSubscribeStatus)
|
||||||
|
|
||||||
observeNullable(viewModel.page) { resource ->
|
observeNullable(viewModel.page) { resource ->
|
||||||
if (resource == null) {
|
if (resource == null) {
|
||||||
hidePreviewPopupDialog()
|
hidePreviewPopupDialog()
|
||||||
|
|
@ -1386,13 +1433,14 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
resultviewPreviewMetaDuration.setText(d.durationText)
|
resultviewPreviewMetaDuration.setText(d.durationText)
|
||||||
resultviewPreviewMetaRating.setText(d.ratingText)
|
resultviewPreviewMetaRating.setText(d.ratingText)
|
||||||
|
|
||||||
resultviewPreviewDescription.setText(d.plotText)
|
resultviewPreviewDescription.setTextHtml(d.plotText)
|
||||||
resultviewPreviewPoster.setImage(
|
resultviewPreviewPoster.setImage(
|
||||||
d.posterImage ?: d.posterBackgroundImage
|
d.posterImage ?: d.posterBackgroundImage
|
||||||
)
|
)
|
||||||
|
|
||||||
setUserData(syncViewModel.userData.value)
|
setUserData(syncViewModel.userData.value)
|
||||||
setWatchStatus(viewModel.watchStatus.value)
|
setWatchStatus(viewModel.watchStatus.value)
|
||||||
|
setSubscribeStatus(viewModel.subscribeStatus.value)
|
||||||
|
|
||||||
resultviewPreviewBookmark.setOnClickListener {
|
resultviewPreviewBookmark.setOnClickListener {
|
||||||
//viewModel.updateWatchStatus(WatchType.PLANTOWATCH)
|
//viewModel.updateWatchStatus(WatchType.PLANTOWATCH)
|
||||||
|
|
@ -1400,26 +1448,28 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
val value = viewModel.watchStatus.value ?: WatchType.NONE
|
val value = viewModel.watchStatus.value ?: WatchType.NONE
|
||||||
|
|
||||||
this@MainActivity.showBottomDialog(
|
this@MainActivity.showBottomDialog(
|
||||||
WatchType.values().map { getString(it.stringRes) }.toList(),
|
WatchType.entries.map { getString(it.stringRes) }.toList(),
|
||||||
value.ordinal,
|
value.ordinal,
|
||||||
this@MainActivity.getString(R.string.action_add_to_bookmarks),
|
this@MainActivity.getString(R.string.action_add_to_bookmarks),
|
||||||
showApply = false,
|
showApply = false,
|
||||||
{}) {
|
{}) {
|
||||||
viewModel.updateWatchStatus(
|
viewModel.updateWatchStatus(
|
||||||
WatchType.values()[it],
|
WatchType.entries[it],
|
||||||
this@MainActivity
|
this@MainActivity
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val value = (syncViewModel.userData.value as? Resource.Success)?.value?.status ?: SyncWatchType.NONE
|
val value =
|
||||||
|
(syncViewModel.userData.value as? Resource.Success)?.value?.status
|
||||||
|
?: SyncWatchType.NONE
|
||||||
|
|
||||||
this@MainActivity.showBottomDialog(
|
this@MainActivity.showBottomDialog(
|
||||||
SyncWatchType.values().map { getString(it.stringRes) }.toList(),
|
SyncWatchType.entries.map { getString(it.stringRes) }.toList(),
|
||||||
value.ordinal,
|
value.ordinal,
|
||||||
this@MainActivity.getString(R.string.action_add_to_bookmarks),
|
this@MainActivity.getString(R.string.action_add_to_bookmarks),
|
||||||
showApply = false,
|
showApply = false,
|
||||||
{}) {
|
{}) {
|
||||||
syncViewModel.setStatus(SyncWatchType.values()[it].internalId)
|
syncViewModel.setStatus(SyncWatchType.entries[it].internalId)
|
||||||
syncViewModel.publishUserData()
|
syncViewModel.publishUserData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1438,7 +1488,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
resultviewPreviewFavorite.setImageResource(drawable)
|
resultviewPreviewFavorite.setImageResource(drawable)
|
||||||
}
|
}
|
||||||
|
|
||||||
resultviewPreviewFavorite.setOnClickListener{
|
resultviewPreviewFavorite.setOnClickListener {
|
||||||
viewModel.toggleFavoriteStatus(this@MainActivity) { newStatus: Boolean? ->
|
viewModel.toggleFavoriteStatus(this@MainActivity) { newStatus: Boolean? ->
|
||||||
if (newStatus == null) return@toggleFavoriteStatus
|
if (newStatus == null) return@toggleFavoriteStatus
|
||||||
|
|
||||||
|
|
@ -1454,7 +1504,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isTvSettings()) // dont want this clickable on tv layout
|
if (isLayout(PHONE)) // dont want this clickable on tv layout
|
||||||
resultviewPreviewDescription.setOnClickListener { view ->
|
resultviewPreviewDescription.setOnClickListener { view ->
|
||||||
view.context?.let { ctx ->
|
view.context?.let { ctx ->
|
||||||
val builder: AlertDialog.Builder =
|
val builder: AlertDialog.Builder =
|
||||||
|
|
@ -1500,6 +1550,26 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we need to run this after we init all apis, otherwise currentSyncApi will fuck itself
|
||||||
|
this@MainActivity.runOnUiThread {
|
||||||
|
// Change library icon with logo of current api in sync
|
||||||
|
libraryViewModel = ViewModelProvider(this@MainActivity)[LibraryViewModel::class.java]
|
||||||
|
libraryViewModel?.currentApiName?.observe(this@MainActivity) {
|
||||||
|
val syncAPI = libraryViewModel?.currentSyncApi
|
||||||
|
Log.i("SYNC_API", "${syncAPI?.name}, ${syncAPI?.idPrefix}")
|
||||||
|
val icon = if (syncAPI?.idPrefix == localListApi.idPrefix) {
|
||||||
|
R.drawable.library_icon
|
||||||
|
} else {
|
||||||
|
syncAPI?.icon ?: R.drawable.library_icon
|
||||||
|
}
|
||||||
|
|
||||||
|
binding?.apply {
|
||||||
|
navRailView.menu.findItem(R.id.navigation_library)?.setIcon(icon)
|
||||||
|
navView.menu.findItem(R.id.navigation_library)?.setIcon(icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchResultBuilder.updateCache(this)
|
SearchResultBuilder.updateCache(this)
|
||||||
|
|
@ -1529,9 +1599,14 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTvSettings()) {
|
if (isLayout(TV or EMULATOR)) {
|
||||||
if (navDestination.matchDestination(R.id.navigation_home)) {
|
if (navDestination.matchDestination(R.id.navigation_home)) {
|
||||||
attachBackPressedCallback()
|
attachBackPressedCallback {
|
||||||
|
showConfirmExitDialog()
|
||||||
|
window?.navigationBarColor =
|
||||||
|
colorFromAttribute(R.attr.primaryGrayBackground)
|
||||||
|
updateLocale()
|
||||||
|
}
|
||||||
} else detachBackPressedCallback()
|
} else detachBackPressedCallback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1565,7 +1640,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
itemRippleColor = rippleColor
|
itemRippleColor = rippleColor
|
||||||
itemActiveIndicatorColor = rippleColor
|
itemActiveIndicatorColor = rippleColor
|
||||||
setupWithNavController(navController)
|
setupWithNavController(navController)
|
||||||
if (isTvSettings()) {
|
if (isLayout(TV or EMULATOR)) {
|
||||||
background?.alpha = 200
|
background?.alpha = 200
|
||||||
} else {
|
} else {
|
||||||
background?.alpha = 255
|
background?.alpha = 255
|
||||||
|
|
@ -1699,6 +1774,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
runAutoUpdate()
|
runAutoUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FcastManager().init(this, false)
|
||||||
|
|
||||||
APIRepository.dubStatusActive = getApiDubstatusSettings()
|
APIRepository.dubStatusActive = getApiDubstatusSettings()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -1735,8 +1812,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
} finally {
|
|
||||||
setKey(HAS_DONE_SETUP_KEY, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to check current focus for TV
|
// Used to check current focus for TV
|
||||||
|
|
@ -1772,26 +1847,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
binding?.navHostFragment?.isInvisible = false
|
binding?.navHostFragment?.isInvisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private var backPressedCallback: OnBackPressedCallback? = null
|
override fun onAuthenticationError() {
|
||||||
|
finish()
|
||||||
private fun attachBackPressedCallback() {
|
|
||||||
if (backPressedCallback == null) {
|
|
||||||
backPressedCallback = object : OnBackPressedCallback(true) {
|
|
||||||
override fun handleOnBackPressed() {
|
|
||||||
showConfirmExitDialog()
|
|
||||||
window?.navigationBarColor =
|
|
||||||
colorFromAttribute(R.attr.primaryGrayBackground)
|
|
||||||
updateLocale()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
backPressedCallback?.isEnabled = true
|
|
||||||
onBackPressedDispatcher.addCallback(this, backPressedCallback ?: return)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun detachBackPressedCallback() {
|
|
||||||
backPressedCallback?.isEnabled = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun checkGithubConnectivity(): Boolean {
|
suspend fun checkGithubConnectivity(): Boolean {
|
||||||
|
|
@ -1804,4 +1861,4 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
package com.lagradost.cloudstream3
|
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.MainActivity.Companion.lastError
|
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
|
||||||
import com.lagradost.cloudstream3.plugins.PluginManager.checkSafeModeFile
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
object NativeCrashHandler {
|
|
||||||
// external fun triggerNativeCrash()
|
|
||||||
/*private external fun initNativeCrashHandler()
|
|
||||||
private external fun getSignalStatus(): Int
|
|
||||||
|
|
||||||
private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
|
|
||||||
//launch {
|
|
||||||
// delay(10000)
|
|
||||||
// triggerNativeCrash()
|
|
||||||
//}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
delay(10_000)
|
|
||||||
val signal = getSignalStatus()
|
|
||||||
// Signal is initialized to zero
|
|
||||||
if (signal == 0) continue
|
|
||||||
|
|
||||||
// Do not crash in safe mode!
|
|
||||||
if (lastError != null) continue
|
|
||||||
if (checkSafeModeFile()) continue
|
|
||||||
|
|
||||||
AcraApplication.exceptionHandler?.uncaughtException(
|
|
||||||
Thread.currentThread(),
|
|
||||||
RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun initCrashHandler() {
|
|
||||||
try {
|
|
||||||
System.loadLibrary("native-lib")
|
|
||||||
initNativeCrashHandler()
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
// Make debug crash.
|
|
||||||
if (BuildConfig.DEBUG) throw t
|
|
||||||
logError(t)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
initSignalPolling()
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
|
||||||
|
|
||||||
package com.lagradost.cloudstream3.extractors
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.lagradost.cloudstream3.*
|
|
||||||
import com.lagradost.cloudstream3.utils.*
|
|
||||||
|
|
||||||
open class ContentX : ExtractorApi() {
|
|
||||||
override val name = "ContentX"
|
|
||||||
override val mainUrl = "https://contentx.me"
|
|
||||||
override val requiresReferer = true
|
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
|
||||||
val ext_ref = referer ?: ""
|
|
||||||
Log.d("Kekik_${this.name}", "url » ${url}")
|
|
||||||
|
|
||||||
val i_source = app.get(url, referer=ext_ref).text
|
|
||||||
val i_extract = Regex("""window\.openPlayer\('([^']+)'""").find(i_source)!!.groups[1]?.value ?: throw ErrorLoadingException("i_extract is null")
|
|
||||||
|
|
||||||
val sub_urls = mutableSetOf<String>()
|
|
||||||
Regex("""\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(i_source).forEach {
|
|
||||||
val (sub_url, sub_lang) = it.destructured
|
|
||||||
|
|
||||||
if (sub_url in sub_urls) { return@forEach }
|
|
||||||
sub_urls.add(sub_url)
|
|
||||||
|
|
||||||
subtitleCallback.invoke(
|
|
||||||
SubtitleFile(
|
|
||||||
lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"),
|
|
||||||
url = fixUrl(sub_url.replace("\\", ""))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val vid_source = app.get("${mainUrl}/source2.php?v=${i_extract}", referer=ext_ref).text
|
|
||||||
val vid_extract = Regex("""file\":\"([^\"]+)""").find(vid_source)!!.groups[1]?.value ?: throw ErrorLoadingException("vid_extract is null")
|
|
||||||
val m3u_link = vid_extract.replace("\\", "")
|
|
||||||
|
|
||||||
callback.invoke(
|
|
||||||
ExtractorLink(
|
|
||||||
source = this.name,
|
|
||||||
name = this.name,
|
|
||||||
url = m3u_link,
|
|
||||||
referer = url,
|
|
||||||
quality = Qualities.Unknown.value,
|
|
||||||
isM3u8 = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val i_dublaj = Regex(""",\"([^']+)\",\"Türkçe""").find(i_source)!!.groups[1]?.value
|
|
||||||
if (i_dublaj != null) {
|
|
||||||
val dublaj_source = app.get("${mainUrl}/source2.php?v=${i_dublaj}", referer=ext_ref).text
|
|
||||||
val dublaj_extract = Regex("""file\":\"([^\"]+)""").find(dublaj_source)!!.groups[1]?.value ?: throw ErrorLoadingException("dublaj_extract is null")
|
|
||||||
val dublaj_link = dublaj_extract.replace("\\", "")
|
|
||||||
|
|
||||||
callback.invoke(
|
|
||||||
ExtractorLink(
|
|
||||||
source = "${this.name} Türkçe Dublaj",
|
|
||||||
name = "${this.name} Türkçe Dublaj",
|
|
||||||
url = dublaj_link,
|
|
||||||
referer = url,
|
|
||||||
quality = Qualities.Unknown.value,
|
|
||||||
isM3u8 = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
|
||||||
|
|
||||||
package com.lagradost.cloudstream3.extractors
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.lagradost.cloudstream3.*
|
|
||||||
import com.lagradost.cloudstream3.utils.*
|
|
||||||
|
|
||||||
open class RapidVid : ExtractorApi() {
|
|
||||||
override val name = "RapidVid"
|
|
||||||
override val mainUrl = "https://rapidvid.net"
|
|
||||||
override val requiresReferer = true
|
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
|
||||||
val ext_ref = referer ?: ""
|
|
||||||
val video_req = app.get(url, referer=ext_ref).text
|
|
||||||
|
|
||||||
val sub_urls = mutableSetOf<String>()
|
|
||||||
Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(video_req).forEach {
|
|
||||||
val (sub_url, sub_lang) = it.destructured
|
|
||||||
|
|
||||||
if (sub_url in sub_urls) { return@forEach }
|
|
||||||
sub_urls.add(sub_url)
|
|
||||||
|
|
||||||
subtitleCallback.invoke(
|
|
||||||
SubtitleFile(
|
|
||||||
lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"),
|
|
||||||
url = fixUrl(sub_url.replace("\\", ""))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val extracted_value = Regex("""file": "(.*)",""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found")
|
|
||||||
|
|
||||||
val bytes = extracted_value.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray()
|
|
||||||
val decoded = String(bytes, Charsets.UTF_8)
|
|
||||||
Log.d("Kekik_${this.name}", "decoded » ${decoded}")
|
|
||||||
|
|
||||||
callback.invoke(
|
|
||||||
ExtractorLink(
|
|
||||||
source = this.name,
|
|
||||||
name = this.name,
|
|
||||||
url = decoded,
|
|
||||||
referer = ext_ref,
|
|
||||||
quality = Qualities.Unknown.value,
|
|
||||||
isM3u8 = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.app
|
|
||||||
import com.lagradost.cloudstream3.network.WebViewResolver
|
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
|
||||||
|
|
||||||
open class StreamWishExtractor : ExtractorApi() {
|
|
||||||
override var name = "StreamWish"
|
|
||||||
override var mainUrl = "https://streamwish.to"
|
|
||||||
override val requiresReferer = false
|
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
|
||||||
val response = app.get(
|
|
||||||
url, referer = referer ?: "$mainUrl/", interceptor = WebViewResolver(
|
|
||||||
Regex("""master\.m3u8""")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val sources = mutableListOf<ExtractorLink>()
|
|
||||||
if (response.url.contains("m3u8"))
|
|
||||||
sources.add(
|
|
||||||
ExtractorLink(
|
|
||||||
source = name,
|
|
||||||
name = name,
|
|
||||||
url = response.url,
|
|
||||||
referer = referer ?: "$mainUrl/",
|
|
||||||
quality = Qualities.Unknown.value,
|
|
||||||
isM3u8 = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return sources
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır.
|
|
||||||
|
|
||||||
package com.lagradost.cloudstream3.extractors
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.lagradost.cloudstream3.*
|
|
||||||
import com.lagradost.cloudstream3.utils.*
|
|
||||||
|
|
||||||
open class VidMoxy : ExtractorApi() {
|
|
||||||
override val name = "VidMoxy"
|
|
||||||
override val mainUrl = "https://vidmoxy.com"
|
|
||||||
override val requiresReferer = true
|
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
|
||||||
val ext_ref = referer ?: ""
|
|
||||||
val video_req = app.get(url, referer=ext_ref).text
|
|
||||||
|
|
||||||
val sub_urls = mutableSetOf<String>()
|
|
||||||
Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(video_req).forEach {
|
|
||||||
val (sub_url, sub_lang) = it.destructured
|
|
||||||
|
|
||||||
if (sub_url in sub_urls) { return@forEach }
|
|
||||||
sub_urls.add(sub_url)
|
|
||||||
|
|
||||||
subtitleCallback.invoke(
|
|
||||||
SubtitleFile(
|
|
||||||
lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"),
|
|
||||||
url = fixUrl(sub_url.replace("\\", ""))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val extracted_value = Regex("""file": "(.*)",""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found")
|
|
||||||
|
|
||||||
val bytes = extracted_value.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray()
|
|
||||||
val decoded = String(bytes, Charsets.UTF_8)
|
|
||||||
Log.d("Kekik_${this.name}", "decoded » ${decoded}")
|
|
||||||
|
|
||||||
callback.invoke(
|
|
||||||
ExtractorLink(
|
|
||||||
source = this.name,
|
|
||||||
name = this.name,
|
|
||||||
url = decoded,
|
|
||||||
referer = ext_ref,
|
|
||||||
quality = Qualities.Unknown.value,
|
|
||||||
isM3u8 = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
|
||||||
import com.lagradost.cloudstream3.amap
|
|
||||||
import com.lagradost.cloudstream3.app
|
|
||||||
import com.lagradost.cloudstream3.utils.*
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
class VidSrcExtractor2 : VidSrcExtractor() {
|
|
||||||
override val mainUrl = "https://vidsrc.me/embed"
|
|
||||||
override suspend fun getUrl(
|
|
||||||
url: String,
|
|
||||||
referer: String?,
|
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
|
||||||
callback: (ExtractorLink) -> Unit
|
|
||||||
) {
|
|
||||||
val newUrl = url.lowercase().replace(mainUrl, super.mainUrl)
|
|
||||||
super.getUrl(newUrl, referer, subtitleCallback, callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open class VidSrcExtractor : ExtractorApi() {
|
|
||||||
override val name = "VidSrc"
|
|
||||||
private val absoluteUrl = "https://v2.vidsrc.me"
|
|
||||||
override val mainUrl = "$absoluteUrl/embed"
|
|
||||||
override val requiresReferer = false
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/** Infinite function to validate the vidSrc pass */
|
|
||||||
suspend fun validatePass(url: String) {
|
|
||||||
val uri = URI(url)
|
|
||||||
val host = uri.host
|
|
||||||
|
|
||||||
// Basically turn https://tm3p.vidsrc.stream/ -> https://vidsrc.stream/
|
|
||||||
val referer = host.split(".").let {
|
|
||||||
val size = it.size
|
|
||||||
"https://" + it.subList(maxOf(0, size - 2), size).joinToString(".") + "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
app.get(url, referer = referer)
|
|
||||||
delay(60_000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getUrl(
|
|
||||||
url: String,
|
|
||||||
referer: String?,
|
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
|
||||||
callback: (ExtractorLink) -> Unit
|
|
||||||
) {
|
|
||||||
val iframedoc = app.get(url).document
|
|
||||||
|
|
||||||
val serverslist =
|
|
||||||
iframedoc.select("div#sources.button_content div#content div#list div").map {
|
|
||||||
val datahash = it.attr("data-hash")
|
|
||||||
if (datahash.isNotBlank()) {
|
|
||||||
val links = try {
|
|
||||||
app.get(
|
|
||||||
"$absoluteUrl/srcrcp/$datahash",
|
|
||||||
referer = "https://rcp.vidsrc.me/"
|
|
||||||
).url
|
|
||||||
} catch (e: Exception) {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
links
|
|
||||||
} else ""
|
|
||||||
}
|
|
||||||
|
|
||||||
serverslist.amap { server ->
|
|
||||||
val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/")
|
|
||||||
if (linkfixed.contains("/prorcp")) {
|
|
||||||
val srcresponse = app.get(server, referer = absoluteUrl).text
|
|
||||||
val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)")
|
|
||||||
val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap
|
|
||||||
val passRegex = Regex("""['"](.*set_pass[^"']*)""")
|
|
||||||
val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace(
|
|
||||||
Regex("""^//"""), "https://"
|
|
||||||
)
|
|
||||||
|
|
||||||
callback.invoke(
|
|
||||||
ExtractorLink(
|
|
||||||
this.name,
|
|
||||||
this.name,
|
|
||||||
srcm3u8,
|
|
||||||
"https://vidsrc.stream/",
|
|
||||||
Qualities.Unknown.value,
|
|
||||||
extractorData = pass,
|
|
||||||
isM3u8 = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
loadExtractor(linkfixed, url, subtitleCallback, callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
|
||||||
import com.lagradost.cloudstream3.app
|
|
||||||
import com.lagradost.cloudstream3.base64Encode
|
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
// Code found in https://github.com/KillerDogeEmpire/vidplay-keys
|
|
||||||
// special credits to @KillerDogeEmpire for providing key
|
|
||||||
|
|
||||||
class MyCloud : Vidplay() {
|
|
||||||
override val name = "MyCloud"
|
|
||||||
override val mainUrl = "https://mcloud.bz"
|
|
||||||
}
|
|
||||||
|
|
||||||
class VidplayOnline : Vidplay() {
|
|
||||||
override val mainUrl = "https://vidplay.online"
|
|
||||||
}
|
|
||||||
|
|
||||||
open class Vidplay : ExtractorApi() {
|
|
||||||
override val name = "Vidplay"
|
|
||||||
override val mainUrl = "https://vidplay.site"
|
|
||||||
override val requiresReferer = true
|
|
||||||
open val key =
|
|
||||||
"https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json"
|
|
||||||
|
|
||||||
override suspend fun getUrl(
|
|
||||||
url: String,
|
|
||||||
referer: String?,
|
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
|
||||||
callback: (ExtractorLink) -> Unit
|
|
||||||
) {
|
|
||||||
val id = url.substringBefore("?").substringAfterLast("/")
|
|
||||||
val encodeId = encodeId(id, getKeys())
|
|
||||||
val mediaUrl = callFutoken(encodeId, url)
|
|
||||||
val res = app.get(
|
|
||||||
"$mediaUrl", headers = mapOf(
|
|
||||||
"Accept" to "application/json, text/javascript, */*; q=0.01",
|
|
||||||
"X-Requested-With" to "XMLHttpRequest",
|
|
||||||
), referer = url
|
|
||||||
).parsedSafe<Response>()?.result
|
|
||||||
|
|
||||||
res?.sources?.map {
|
|
||||||
M3u8Helper.generateM3u8(
|
|
||||||
this.name,
|
|
||||||
it.file ?: return@map,
|
|
||||||
"$mainUrl/"
|
|
||||||
).forEach(callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
res?.tracks?.filter { it.kind == "captions" }?.map {
|
|
||||||
subtitleCallback.invoke(
|
|
||||||
SubtitleFile(it.label ?: return@map, it.file ?: return@map)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getKeys(): List<String> {
|
|
||||||
return app.get(key).parsed()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun callFutoken(id: String, url: String): String? {
|
|
||||||
val script = app.get("$mainUrl/futoken").text
|
|
||||||
val k = "k='(\\S+)'".toRegex().find(script)?.groupValues?.get(1) ?: return null
|
|
||||||
val a = mutableListOf(k)
|
|
||||||
for (i in id.indices) {
|
|
||||||
a.add((k[i % k.length].code + id[i].code).toString())
|
|
||||||
}
|
|
||||||
return "$mainUrl/mediainfo/${a.joinToString(",")}?${url.substringAfter("?")}"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun encodeId(id: String, keyList: List<String>): String {
|
|
||||||
val cipher1 = Cipher.getInstance("RC4")
|
|
||||||
val cipher2 = Cipher.getInstance("RC4")
|
|
||||||
cipher1.init(
|
|
||||||
Cipher.DECRYPT_MODE,
|
|
||||||
SecretKeySpec(keyList[0].toByteArray(), "RC4"),
|
|
||||||
cipher1.parameters
|
|
||||||
)
|
|
||||||
cipher2.init(
|
|
||||||
Cipher.DECRYPT_MODE,
|
|
||||||
SecretKeySpec(keyList[1].toByteArray(), "RC4"),
|
|
||||||
cipher2.parameters
|
|
||||||
)
|
|
||||||
var input = id.toByteArray()
|
|
||||||
input = cipher1.doFinal(input)
|
|
||||||
input = cipher2.doFinal(input)
|
|
||||||
return base64Encode(input).replace("/", "_")
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Tracks(
|
|
||||||
@JsonProperty("file") val file: String? = null,
|
|
||||||
@JsonProperty("label") val label: String? = null,
|
|
||||||
@JsonProperty("kind") val kind: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Sources(
|
|
||||||
@JsonProperty("file") val file: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Result(
|
|
||||||
@JsonProperty("sources") val sources: ArrayList<Sources>? = arrayListOf(),
|
|
||||||
@JsonProperty("tracks") val tracks: ArrayList<Tracks>? = arrayListOf(),
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Response(
|
|
||||||
@JsonProperty("result") val result: Result? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
|
||||||
import com.lagradost.cloudstream3.app
|
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
|
||||||
|
|
||||||
class Tubeless : Voe() {
|
|
||||||
override var mainUrl = "https://tubelessceliolymph.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
open class Voe : ExtractorApi() {
|
|
||||||
override val name = "Voe"
|
|
||||||
override val mainUrl = "https://voe.sx"
|
|
||||||
override val requiresReferer = true
|
|
||||||
|
|
||||||
override suspend fun getUrl(
|
|
||||||
url: String,
|
|
||||||
referer: String?,
|
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
|
||||||
callback: (ExtractorLink) -> Unit
|
|
||||||
) {
|
|
||||||
val res = app.get(url, referer = referer).document
|
|
||||||
val script = res.select("script").find { it.data().contains("sources =") }?.data()
|
|
||||||
val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1)
|
|
||||||
|
|
||||||
M3u8Helper.generateM3u8(
|
|
||||||
name,
|
|
||||||
link ?: return,
|
|
||||||
"$mainUrl/",
|
|
||||||
headers = mapOf("Origin" to "$mainUrl/")
|
|
||||||
).forEach(callback)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,15 +2,13 @@ package com.lagradost.cloudstream3.metaproviders
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.MainAPI
|
import com.lagradost.cloudstream3.MainAPI
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||||
|
|
||||||
object SyncRedirector {
|
object SyncRedirector {
|
||||||
val syncApis = SyncApis
|
|
||||||
private val syncIds =
|
private val syncIds =
|
||||||
listOf(
|
listOf(
|
||||||
SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""),
|
SyncIdName.MyAnimeList to Regex("""myanimelist\.net/anime/(\d+)"""),
|
||||||
SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""")
|
SyncIdName.Anilist to Regex("""anilist\.co/anime/(\d+)""")
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun redirect(
|
suspend fun redirect(
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ open class TmdbProvider : MainAPI() {
|
||||||
this.id,
|
this.id,
|
||||||
episode.episode_number,
|
episode.episode_number,
|
||||||
episode.season_number,
|
episode.season_number,
|
||||||
|
this.name ?: this.original_name,
|
||||||
).toJson(),
|
).toJson(),
|
||||||
episode.name,
|
episode.name,
|
||||||
episode.season_number,
|
episode.season_number,
|
||||||
|
|
@ -122,6 +123,7 @@ open class TmdbProvider : MainAPI() {
|
||||||
this.id,
|
this.id,
|
||||||
episodeNum,
|
episodeNum,
|
||||||
season.season_number,
|
season.season_number,
|
||||||
|
this.name ?: this.original_name,
|
||||||
).toJson(),
|
).toJson(),
|
||||||
season = season.season_number
|
season = season.season_number
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,471 @@
|
||||||
|
package com.lagradost.cloudstream3.metaproviders
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAlias
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.APIHolder
|
||||||
|
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||||
|
import com.lagradost.cloudstream3.Actor
|
||||||
|
import com.lagradost.cloudstream3.ActorData
|
||||||
|
import com.lagradost.cloudstream3.Episode
|
||||||
|
import com.lagradost.cloudstream3.HomePageResponse
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||||
|
import com.lagradost.cloudstream3.MainAPI
|
||||||
|
import com.lagradost.cloudstream3.MainPageRequest
|
||||||
|
import com.lagradost.cloudstream3.NextAiring
|
||||||
|
import com.lagradost.cloudstream3.ProviderType
|
||||||
|
import com.lagradost.cloudstream3.SearchResponse
|
||||||
|
import com.lagradost.cloudstream3.ShowStatus
|
||||||
|
import com.lagradost.cloudstream3.TvType
|
||||||
|
import com.lagradost.cloudstream3.addDate
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.base64Decode
|
||||||
|
import com.lagradost.cloudstream3.mainPageOf
|
||||||
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
import com.lagradost.cloudstream3.newHomePageResponse
|
||||||
|
import com.lagradost.cloudstream3.newMovieLoadResponse
|
||||||
|
import com.lagradost.cloudstream3.newMovieSearchResponse
|
||||||
|
import com.lagradost.cloudstream3.newTvSeriesLoadResponse
|
||||||
|
import com.lagradost.cloudstream3.newTvSeriesSearchResponse
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
open class TraktProvider : MainAPI() {
|
||||||
|
override var name = "Trakt"
|
||||||
|
override val hasMainPage = true
|
||||||
|
override val providerType = ProviderType.MetaProvider
|
||||||
|
override val supportedTypes = setOf(
|
||||||
|
TvType.Movie,
|
||||||
|
TvType.TvSeries,
|
||||||
|
TvType.Anime,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val traktClientId =
|
||||||
|
base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==")
|
||||||
|
private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2")
|
||||||
|
|
||||||
|
override val mainPage = mainPageOf(
|
||||||
|
"$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now
|
||||||
|
"$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time
|
||||||
|
"$traktApiUrl/shows/trending" to "Trending Shows", //Most watched Shows right now
|
||||||
|
"$traktApiUrl/shows/popular" to "Popular Shows", //The most popular Shows for all time
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
||||||
|
|
||||||
|
val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page")
|
||||||
|
|
||||||
|
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
|
||||||
|
element.toSearchResponse()
|
||||||
|
}
|
||||||
|
return newHomePageResponse(request.name, results)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MediaDetails.toSearchResponse(): SearchResponse {
|
||||||
|
|
||||||
|
val media = this.media ?: this
|
||||||
|
val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries
|
||||||
|
val poster = media.images?.poster?.firstOrNull()
|
||||||
|
|
||||||
|
if (mediaType == TvType.Movie) {
|
||||||
|
return newMovieSearchResponse(
|
||||||
|
name = media.title!!,
|
||||||
|
url = Data(
|
||||||
|
type = mediaType,
|
||||||
|
mediaDetails = media,
|
||||||
|
).toJson(),
|
||||||
|
type = TvType.Movie,
|
||||||
|
) {
|
||||||
|
posterUrl = fixPath(poster)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return newTvSeriesSearchResponse(
|
||||||
|
name = media.title!!,
|
||||||
|
url = Data(
|
||||||
|
type = mediaType,
|
||||||
|
mediaDetails = media,
|
||||||
|
).toJson(),
|
||||||
|
type = TvType.TvSeries,
|
||||||
|
) {
|
||||||
|
this.posterUrl = fixPath(poster)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse>? {
|
||||||
|
val apiResponse =
|
||||||
|
getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query")
|
||||||
|
|
||||||
|
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
|
||||||
|
element.toSearchResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse {
|
||||||
|
|
||||||
|
val data = parseJson<Data>(url)
|
||||||
|
val mediaDetails = data.mediaDetails
|
||||||
|
val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows"
|
||||||
|
|
||||||
|
val posterUrl = mediaDetails?.images?.poster?.firstOrNull()
|
||||||
|
val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull()
|
||||||
|
|
||||||
|
val resActor =
|
||||||
|
getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full")
|
||||||
|
|
||||||
|
val actors = parseJson<People>(resActor).cast?.map {
|
||||||
|
ActorData(
|
||||||
|
Actor(
|
||||||
|
name = it.person?.name!!,
|
||||||
|
image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500")
|
||||||
|
),
|
||||||
|
roleString = it.character
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val resRelated =
|
||||||
|
getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20")
|
||||||
|
|
||||||
|
val relatedMedia = parseJson<List<MediaDetails>>(resRelated).map { it.toSearchResponse() }
|
||||||
|
|
||||||
|
val isCartoon =
|
||||||
|
mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true
|
||||||
|
val isAnime =
|
||||||
|
isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja")
|
||||||
|
val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko")
|
||||||
|
val isBollywood = mediaDetails?.country == "in"
|
||||||
|
|
||||||
|
if (data.type == TvType.Movie) {
|
||||||
|
|
||||||
|
val linkData = LinkData(
|
||||||
|
id = mediaDetails?.ids?.tmdb,
|
||||||
|
traktId = mediaDetails?.ids?.trakt,
|
||||||
|
traktSlug = mediaDetails?.ids?.slug,
|
||||||
|
tmdbId = mediaDetails?.ids?.tmdb,
|
||||||
|
imdbId = mediaDetails?.ids?.imdb.toString(),
|
||||||
|
tvdbId = mediaDetails?.ids?.tvdb,
|
||||||
|
tvrageId = mediaDetails?.ids?.tvrage,
|
||||||
|
type = data.type.toString(),
|
||||||
|
title = mediaDetails?.title,
|
||||||
|
year = mediaDetails?.year,
|
||||||
|
orgTitle = mediaDetails?.title,
|
||||||
|
isAnime = isAnime,
|
||||||
|
//jpTitle = later if needed as it requires another network request,
|
||||||
|
airedDate = mediaDetails?.released
|
||||||
|
?: mediaDetails?.firstAired,
|
||||||
|
isAsian = isAsian,
|
||||||
|
isBollywood = isBollywood,
|
||||||
|
).toJson()
|
||||||
|
|
||||||
|
return newMovieLoadResponse(
|
||||||
|
name = mediaDetails?.title!!,
|
||||||
|
url = data.toJson(),
|
||||||
|
dataUrl = linkData.toJson(),
|
||||||
|
type = if (isAnime) TvType.AnimeMovie else TvType.Movie,
|
||||||
|
) {
|
||||||
|
this.name = mediaDetails.title
|
||||||
|
this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie
|
||||||
|
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
||||||
|
this.year = mediaDetails.year
|
||||||
|
this.plot = mediaDetails.overview
|
||||||
|
this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
|
||||||
|
this.tags = mediaDetails.genres
|
||||||
|
this.duration = mediaDetails.runtime
|
||||||
|
this.recommendations = relatedMedia
|
||||||
|
this.actors = actors
|
||||||
|
this.comingSoon = isUpcoming(mediaDetails.released)
|
||||||
|
//posterHeaders
|
||||||
|
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
|
||||||
|
this.contentRating = mediaDetails.certification
|
||||||
|
addTrailer(mediaDetails.trailer)
|
||||||
|
addImdbId(mediaDetails.ids?.imdb)
|
||||||
|
addTMDbId(mediaDetails.ids?.tmdb.toString())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
val resSeasons =
|
||||||
|
getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes")
|
||||||
|
val episodes = mutableListOf<Episode>()
|
||||||
|
val seasons = parseJson<List<Seasons>>(resSeasons)
|
||||||
|
var nextAir: NextAiring? = null
|
||||||
|
|
||||||
|
seasons.forEach { season ->
|
||||||
|
|
||||||
|
season.episodes?.map { episode ->
|
||||||
|
|
||||||
|
val linkData = LinkData(
|
||||||
|
id = mediaDetails?.ids?.tmdb,
|
||||||
|
traktId = mediaDetails?.ids?.trakt,
|
||||||
|
traktSlug = mediaDetails?.ids?.slug,
|
||||||
|
tmdbId = mediaDetails?.ids?.tmdb,
|
||||||
|
imdbId = mediaDetails?.ids?.imdb.toString(),
|
||||||
|
tvdbId = mediaDetails?.ids?.tvdb,
|
||||||
|
tvrageId = mediaDetails?.ids?.tvrage,
|
||||||
|
type = data.type.toString(),
|
||||||
|
season = episode.season,
|
||||||
|
episode = episode.number,
|
||||||
|
title = mediaDetails?.title,
|
||||||
|
year = mediaDetails?.year,
|
||||||
|
orgTitle = mediaDetails?.title,
|
||||||
|
isAnime = isAnime,
|
||||||
|
airedYear = mediaDetails?.year,
|
||||||
|
lastSeason = seasons.size,
|
||||||
|
epsTitle = episode.title,
|
||||||
|
//jpTitle = later if needed as it requires another network request,
|
||||||
|
date = episode.firstAired,
|
||||||
|
airedDate = episode.firstAired,
|
||||||
|
isAsian = isAsian,
|
||||||
|
isBollywood = isBollywood,
|
||||||
|
isCartoon = isCartoon
|
||||||
|
).toJson()
|
||||||
|
|
||||||
|
episodes.add(
|
||||||
|
Episode(
|
||||||
|
data = linkData.toJson(),
|
||||||
|
name = episode.title,
|
||||||
|
season = episode.season,
|
||||||
|
episode = episode.number,
|
||||||
|
posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()),
|
||||||
|
rating = episode.rating?.times(10)?.roundToInt(),
|
||||||
|
description = episode.overview,
|
||||||
|
runTime = episode.runtime
|
||||||
|
).apply {
|
||||||
|
this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
|
||||||
|
if (nextAir == null && this.date != null && this.date!! > unixTimeMS && this.season != 0) {
|
||||||
|
nextAir = NextAiring(
|
||||||
|
episode = this.episode!!,
|
||||||
|
unixTime = this.date!!.div(1000L),
|
||||||
|
season = if (this.season == 1) null else this.season,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newTvSeriesLoadResponse(
|
||||||
|
name = mediaDetails?.title!!,
|
||||||
|
url = data.toJson(),
|
||||||
|
type = if (isAnime) TvType.Anime else TvType.TvSeries,
|
||||||
|
episodes = episodes
|
||||||
|
) {
|
||||||
|
this.name = mediaDetails.title
|
||||||
|
this.type = if (isAnime) TvType.Anime else TvType.TvSeries
|
||||||
|
this.episodes = episodes
|
||||||
|
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
||||||
|
this.year = mediaDetails.year
|
||||||
|
this.plot = mediaDetails.overview
|
||||||
|
this.showStatus = getStatus(mediaDetails.status)
|
||||||
|
this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
|
||||||
|
this.tags = mediaDetails.genres
|
||||||
|
this.duration = mediaDetails.runtime
|
||||||
|
this.recommendations = relatedMedia
|
||||||
|
this.actors = actors
|
||||||
|
this.comingSoon = isUpcoming(mediaDetails.released)
|
||||||
|
//posterHeaders
|
||||||
|
this.nextAiring = nextAir
|
||||||
|
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
|
||||||
|
this.contentRating = mediaDetails.certification
|
||||||
|
addTrailer(mediaDetails.trailer)
|
||||||
|
addImdbId(mediaDetails.ids?.imdb)
|
||||||
|
addTMDbId(mediaDetails.ids?.tmdb.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getApi(url: String): String {
|
||||||
|
return app.get(
|
||||||
|
url = url,
|
||||||
|
headers = mapOf(
|
||||||
|
"Content-Type" to "application/json",
|
||||||
|
"trakt-api-version" to "2",
|
||||||
|
"trakt-api-key" to traktClientId,
|
||||||
|
)
|
||||||
|
).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isUpcoming(dateString: String?): Boolean {
|
||||||
|
return try {
|
||||||
|
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||||
|
val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
|
||||||
|
unixTimeMS < dateTime
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
logError(t)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getStatus(t: String?): ShowStatus {
|
||||||
|
return when (t) {
|
||||||
|
"returning series" -> ShowStatus.Ongoing
|
||||||
|
"continuing" -> ShowStatus.Ongoing
|
||||||
|
else -> ShowStatus.Completed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fixPath(url: String?): String? {
|
||||||
|
url ?: return null
|
||||||
|
return "https://$url"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getWidthImageUrl(path: String?, width: String): String? {
|
||||||
|
if (path == null) return null
|
||||||
|
if (!path.contains("image.tmdb.org")) return fixPath(path)
|
||||||
|
val fileName = Uri.parse(path).lastPathSegment ?: return null
|
||||||
|
return "https://image.tmdb.org/t/p/${width}/${fileName}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOriginalWidthImageUrl(path: String?): String? {
|
||||||
|
if (path == null) return null
|
||||||
|
if (!path.contains("image.tmdb.org")) return fixPath(path)
|
||||||
|
return getWidthImageUrl(path, "original")
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Data(
|
||||||
|
val type: TvType? = null,
|
||||||
|
val mediaDetails: MediaDetails? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MediaDetails(
|
||||||
|
@JsonProperty("title") val title: String? = null,
|
||||||
|
@JsonProperty("year") val year: Int? = null,
|
||||||
|
@JsonProperty("ids") val ids: Ids? = null,
|
||||||
|
@JsonProperty("tagline") val tagline: String? = null,
|
||||||
|
@JsonProperty("overview") val overview: String? = null,
|
||||||
|
@JsonProperty("released") val released: String? = null,
|
||||||
|
@JsonProperty("runtime") val runtime: Int? = null,
|
||||||
|
@JsonProperty("country") val country: String? = null,
|
||||||
|
@JsonProperty("updatedAt") val updatedAt: String? = null,
|
||||||
|
@JsonProperty("trailer") val trailer: String? = null,
|
||||||
|
@JsonProperty("homepage") val homepage: String? = null,
|
||||||
|
@JsonProperty("status") val status: String? = null,
|
||||||
|
@JsonProperty("rating") val rating: Double? = null,
|
||||||
|
@JsonProperty("votes") val votes: Long? = null,
|
||||||
|
@JsonProperty("comment_count") val commentCount: Long? = null,
|
||||||
|
@JsonProperty("language") val language: String? = null,
|
||||||
|
@JsonProperty("languages") val languages: List<String>? = null,
|
||||||
|
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
|
||||||
|
@JsonProperty("genres") val genres: List<String>? = null,
|
||||||
|
@JsonProperty("certification") val certification: String? = null,
|
||||||
|
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
|
||||||
|
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||||
|
@JsonProperty("airs") val airs: Airs? = null,
|
||||||
|
@JsonProperty("network") val network: String? = null,
|
||||||
|
@JsonProperty("images") val images: Images? = null,
|
||||||
|
@JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Airs(
|
||||||
|
@JsonProperty("day") val day: String? = null,
|
||||||
|
@JsonProperty("time") val time: String? = null,
|
||||||
|
@JsonProperty("timezone") val timezone: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Ids(
|
||||||
|
@JsonProperty("trakt") val trakt: Int? = null,
|
||||||
|
@JsonProperty("slug") val slug: String? = null,
|
||||||
|
@JsonProperty("tvdb") val tvdb: Int? = null,
|
||||||
|
@JsonProperty("imdb") val imdb: String? = null,
|
||||||
|
@JsonProperty("tmdb") val tmdb: Int? = null,
|
||||||
|
@JsonProperty("tvrage") val tvrage: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Images(
|
||||||
|
@JsonProperty("fanart") val fanart: List<String>? = null,
|
||||||
|
@JsonProperty("poster") val poster: List<String>? = null,
|
||||||
|
@JsonProperty("logo") val logo: List<String>? = null,
|
||||||
|
@JsonProperty("clearart") val clearart: List<String>? = null,
|
||||||
|
@JsonProperty("banner") val banner: List<String>? = null,
|
||||||
|
@JsonProperty("thumb") val thumb: List<String>? = null,
|
||||||
|
@JsonProperty("screenshot") val screenshot: List<String>? = null,
|
||||||
|
@JsonProperty("headshot") val headshot: List<String>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class People(
|
||||||
|
@JsonProperty("cast") val cast: List<Cast>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Cast(
|
||||||
|
@JsonProperty("character") val character: String? = null,
|
||||||
|
@JsonProperty("characters") val characters: List<String>? = null,
|
||||||
|
@JsonProperty("episode_count") val episodeCount: Long? = null,
|
||||||
|
@JsonProperty("person") val person: Person? = null,
|
||||||
|
@JsonProperty("images") val images: Images? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Person(
|
||||||
|
@JsonProperty("name") val name: String? = null,
|
||||||
|
@JsonProperty("ids") val ids: Ids? = null,
|
||||||
|
@JsonProperty("images") val images: Images? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Seasons(
|
||||||
|
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
|
||||||
|
@JsonProperty("episode_count") val episodeCount: Int? = null,
|
||||||
|
@JsonProperty("episodes") val episodes: List<TraktEpisode>? = null,
|
||||||
|
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||||
|
@JsonProperty("ids") val ids: Ids? = null,
|
||||||
|
@JsonProperty("images") val images: Images? = null,
|
||||||
|
@JsonProperty("network") val network: String? = null,
|
||||||
|
@JsonProperty("number") val number: Int? = null,
|
||||||
|
@JsonProperty("overview") val overview: String? = null,
|
||||||
|
@JsonProperty("rating") val rating: Double? = null,
|
||||||
|
@JsonProperty("title") val title: String? = null,
|
||||||
|
@JsonProperty("updated_at") val updatedAt: String? = null,
|
||||||
|
@JsonProperty("votes") val votes: Int? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TraktEpisode(
|
||||||
|
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
|
||||||
|
@JsonProperty("comment_count") val commentCount: Int? = null,
|
||||||
|
@JsonProperty("episode_type") val episodeType: String? = null,
|
||||||
|
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||||
|
@JsonProperty("ids") val ids: Ids? = null,
|
||||||
|
@JsonProperty("images") val images: Images? = null,
|
||||||
|
@JsonProperty("number") val number: Int? = null,
|
||||||
|
@JsonProperty("number_abs") val numberAbs: Int? = null,
|
||||||
|
@JsonProperty("overview") val overview: String? = null,
|
||||||
|
@JsonProperty("rating") val rating: Double? = null,
|
||||||
|
@JsonProperty("runtime") val runtime: Int? = null,
|
||||||
|
@JsonProperty("season") val season: Int? = null,
|
||||||
|
@JsonProperty("title") val title: String? = null,
|
||||||
|
@JsonProperty("updated_at") val updatedAt: String? = null,
|
||||||
|
@JsonProperty("votes") val votes: Int? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LinkData(
|
||||||
|
val id: Int? = null,
|
||||||
|
val traktId: Int? = null,
|
||||||
|
val traktSlug: String? = null,
|
||||||
|
val tmdbId: Int? = null,
|
||||||
|
val imdbId: String? = null,
|
||||||
|
val tvdbId: Int? = null,
|
||||||
|
val tvrageId: String? = null,
|
||||||
|
val type: String? = null,
|
||||||
|
val season: Int? = null,
|
||||||
|
val episode: Int? = null,
|
||||||
|
val aniId: String? = null,
|
||||||
|
val animeId: String? = null,
|
||||||
|
val title: String? = null,
|
||||||
|
val year: Int? = null,
|
||||||
|
val orgTitle: String? = null,
|
||||||
|
val isAnime: Boolean = false,
|
||||||
|
val airedYear: Int? = null,
|
||||||
|
val lastSeason: Int? = null,
|
||||||
|
val epsTitle: String? = null,
|
||||||
|
val jpTitle: String? = null,
|
||||||
|
val date: String? = null,
|
||||||
|
val airedDate: String? = null,
|
||||||
|
val isAsian: Boolean = false,
|
||||||
|
val isBollywood: Boolean = false,
|
||||||
|
val isCartoon: Boolean = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.lagradost.cloudstream3.mvvm
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
|
||||||
|
/** NOTE: Only one observer at a time per value */
|
||||||
|
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
||||||
|
liveData.removeObservers(this)
|
||||||
|
liveData.observe(this) { it?.let { t -> action(t) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** NOTE: Only one observer at a time per value */
|
||||||
|
fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
||||||
|
liveData.removeObservers(this)
|
||||||
|
liveData.observe(this) { action(it) }
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,10 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.nicehttp.Requests.Companion.await
|
import com.lagradost.nicehttp.Requests.Companion.await
|
||||||
import com.lagradost.nicehttp.cookies
|
import com.lagradost.nicehttp.cookies
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import okhttp3.*
|
import okhttp3.Headers
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,4 @@ package com.lagradost.cloudstream3.plugins
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
annotation class CloudstreamPlugin(
|
annotation class CloudstreamPlugin
|
||||||
)
|
|
||||||
|
|
@ -34,7 +34,7 @@ abstract class Plugin {
|
||||||
*/
|
*/
|
||||||
fun registerMainAPI(element: MainAPI) {
|
fun registerMainAPI(element: MainAPI) {
|
||||||
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI")
|
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI")
|
||||||
element.sourcePlugin = this.__filename
|
element.sourcePlugin = this.filename
|
||||||
// Race condition causing which would case duplicates if not for distinctBy
|
// Race condition causing which would case duplicates if not for distinctBy
|
||||||
synchronized(APIHolder.allProviders) {
|
synchronized(APIHolder.allProviders) {
|
||||||
APIHolder.allProviders.add(element)
|
APIHolder.allProviders.add(element)
|
||||||
|
|
@ -48,7 +48,7 @@ abstract class Plugin {
|
||||||
*/
|
*/
|
||||||
fun registerExtractorAPI(element: ExtractorApi) {
|
fun registerExtractorAPI(element: ExtractorApi) {
|
||||||
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi")
|
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi")
|
||||||
element.sourcePlugin = this.__filename
|
element.sourcePlugin = this.filename
|
||||||
extractorApis.add(element)
|
extractorApis.add(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,7 +67,12 @@ abstract class Plugin {
|
||||||
* This will contain your resources if you specified requiresResources in gradle
|
* This will contain your resources if you specified requiresResources in gradle
|
||||||
*/
|
*/
|
||||||
var resources: Resources? = null
|
var resources: Resources? = null
|
||||||
var __filename: String? = null
|
/** Full file path to the plugin. */
|
||||||
|
@Deprecated("Renamed to `filename` to follow conventions", replaceWith = ReplaceWith("filename"))
|
||||||
|
var __filename: String?
|
||||||
|
get() = filename
|
||||||
|
set(value) {filename = value}
|
||||||
|
var filename: String? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will add a button in the settings allowing you to add custom settings
|
* This will add a button in the settings allowing you to add custom settings
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,25 @@
|
||||||
package com.lagradost.cloudstream3.plugins
|
package com.lagradost.cloudstream3.plugins
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.*
|
import android.app.*
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
|
||||||
import com.lagradost.cloudstream3.APIHolder.removePluginMapping
|
import com.lagradost.cloudstream3.APIHolder.removePluginMapping
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider
|
import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider
|
||||||
|
|
@ -34,6 +35,7 @@ import com.lagradost.cloudstream3.ui.result.UiText
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
|
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
|
||||||
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
|
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||||
|
|
@ -164,7 +166,7 @@ object PluginManager {
|
||||||
|
|
||||||
private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins"
|
private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins"
|
||||||
|
|
||||||
public var currentlyLoading: String? = null
|
var currentlyLoading: String? = null
|
||||||
|
|
||||||
// Maps filepath to plugin
|
// Maps filepath to plugin
|
||||||
val plugins: MutableMap<String, Plugin> =
|
val plugins: MutableMap<String, Plugin> =
|
||||||
|
|
@ -340,7 +342,7 @@ object PluginManager {
|
||||||
|
|
||||||
//Omit non-NSFW if mode is set to NSFW only
|
//Omit non-NSFW if mode is set to NSFW only
|
||||||
if (mode == AutoDownloadMode.NsfwOnly) {
|
if (mode == AutoDownloadMode.NsfwOnly) {
|
||||||
if (tvtypes.contains(TvType.NSFW.name) == false) {
|
if (!tvtypes.contains(TvType.NSFW.name)) {
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -429,7 +431,6 @@ object PluginManager {
|
||||||
**/
|
**/
|
||||||
fun loadAllLocalPlugins(context: Context, forceReload: Boolean) {
|
fun loadAllLocalPlugins(context: Context, forceReload: Boolean) {
|
||||||
val dir = File(LOCAL_PLUGINS_PATH)
|
val dir = File(LOCAL_PLUGINS_PATH)
|
||||||
removeKey(PLUGINS_KEY_LOCAL)
|
|
||||||
|
|
||||||
if (!dir.exists()) {
|
if (!dir.exists()) {
|
||||||
val res = dir.mkdirs()
|
val res = dir.mkdirs()
|
||||||
|
|
@ -506,10 +507,12 @@ object PluginManager {
|
||||||
val version: Int = manifest.version ?: PLUGIN_VERSION_NOT_SET.also {
|
val version: Int = manifest.version ?: PLUGIN_VERSION_NOT_SET.also {
|
||||||
Log.d(TAG, "No manifest version for ${data.internalName}")
|
Log.d(TAG, "No manifest version for ${data.internalName}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val pluginClass: Class<*> =
|
val pluginClass: Class<*> =
|
||||||
loader.loadClass(manifest.pluginClassName) as Class<out Plugin?>
|
loader.loadClass(manifest.pluginClassName) as Class<out Plugin?>
|
||||||
val pluginInstance: Plugin =
|
val pluginInstance: Plugin =
|
||||||
pluginClass.newInstance() as Plugin
|
pluginClass.getDeclaredConstructor().newInstance() as Plugin
|
||||||
|
|
||||||
// Sets with the proper version
|
// Sets with the proper version
|
||||||
setPluginData(data.copy(version = version))
|
setPluginData(data.copy(version = version))
|
||||||
|
|
@ -519,14 +522,16 @@ object PluginManager {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginInstance.__filename = fileName
|
pluginInstance.filename = file.absolutePath
|
||||||
if (manifest.requiresResources) {
|
if (manifest.requiresResources) {
|
||||||
Log.d(TAG, "Loading resources for ${data.internalName}")
|
Log.d(TAG, "Loading resources for ${data.internalName}")
|
||||||
// based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk
|
// based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk
|
||||||
val assets = AssetManager::class.java.newInstance()
|
val assets = AssetManager::class.java.getDeclaredConstructor().newInstance()
|
||||||
val addAssetPath =
|
val addAssetPath =
|
||||||
AssetManager::class.java.getMethod("addAssetPath", String::class.java)
|
AssetManager::class.java.getMethod("addAssetPath", String::class.java)
|
||||||
addAssetPath.invoke(assets, file.absolutePath)
|
addAssetPath.invoke(assets, file.absolutePath)
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
pluginInstance.resources = Resources(
|
pluginInstance.resources = Resources(
|
||||||
assets,
|
assets,
|
||||||
context.resources.displayMetrics,
|
context.resources.displayMetrics,
|
||||||
|
|
@ -568,14 +573,14 @@ object PluginManager {
|
||||||
|
|
||||||
// remove all registered apis
|
// remove all registered apis
|
||||||
synchronized(APIHolder.apis) {
|
synchronized(APIHolder.apis) {
|
||||||
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach {
|
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
|
||||||
removePluginMapping(it)
|
removePluginMapping(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
synchronized(APIHolder.allProviders) {
|
synchronized(APIHolder.allProviders) {
|
||||||
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename }
|
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename }
|
||||||
}
|
}
|
||||||
extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename }
|
extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename }
|
||||||
|
|
||||||
classLoaders.values.removeIf { v -> v == plugin }
|
classLoaders.values.removeIf { v -> v == plugin }
|
||||||
|
|
||||||
|
|
@ -722,9 +727,14 @@ object PluginManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
val notification = builder.build()
|
val notification = builder.build()
|
||||||
with(NotificationManagerCompat.from(context)) {
|
// notificationId is a unique int for each notification that you must define
|
||||||
// notificationId is a unique int for each notification that you must define
|
if (ActivityCompat.checkSelfPermission(
|
||||||
notify((System.currentTimeMillis() / 1000).toInt(), notification)
|
context,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
NotificationManagerCompat.from(context)
|
||||||
|
.notify((System.currentTimeMillis() / 1000).toInt(), notification)
|
||||||
}
|
}
|
||||||
return notification
|
return notification
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ object RepositoryManager {
|
||||||
val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy {
|
val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy {
|
||||||
getKey("PREBUILT_REPOSITORIES") ?: emptyArray()
|
getKey("PREBUILT_REPOSITORIES") ?: emptyArray()
|
||||||
}
|
}
|
||||||
val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$")
|
private val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$")
|
||||||
|
|
||||||
/* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */
|
/* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */
|
||||||
fun convertRawGitUrl(url: String): String {
|
fun convertRawGitUrl(url: String): String {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import kotlinx.coroutines.sync.withLock
|
||||||
object VotingApi { // please do not cheat the votes lol
|
object VotingApi { // please do not cheat the votes lol
|
||||||
private const val LOGKEY = "VotingApi"
|
private const val LOGKEY = "VotingApi"
|
||||||
|
|
||||||
private const val apiDomain = "https://counterapi.com/api"
|
private const val API_DOMAIN = "https://counterapi.com/api"
|
||||||
|
|
||||||
private fun transformUrl(url: String): String = // dont touch or all votes get reset
|
private fun transformUrl(url: String): String = // dont touch or all votes get reset
|
||||||
MessageDigest
|
MessageDigest
|
||||||
|
|
@ -49,13 +49,13 @@ object VotingApi { // please do not cheat the votes lol
|
||||||
.joinToString("-")
|
.joinToString("-")
|
||||||
|
|
||||||
private suspend fun readVote(pluginUrl: String): Int {
|
private suspend fun readVote(pluginUrl: String): Int {
|
||||||
var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true"
|
val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true"
|
||||||
Log.d(LOGKEY, "Requesting: $url")
|
Log.d(LOGKEY, "Requesting: $url")
|
||||||
return app.get(url).parsedSafe<Result>()?.value ?: 0
|
return app.get(url).parsedSafe<Result>()?.value ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun writeVote(pluginUrl: String): Boolean {
|
private suspend fun writeVote(pluginUrl: String): Boolean {
|
||||||
var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}"
|
val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}"
|
||||||
Log.d(LOGKEY, "Requesting: $url")
|
Log.d(LOGKEY, "Requesting: $url")
|
||||||
return app.get(url).parsedSafe<Result>()?.value != null
|
return app.get(url).parsedSafe<Result>()?.value != null
|
||||||
}
|
}
|
||||||
|
|
@ -69,8 +69,7 @@ object VotingApi { // please do not cheat the votes lol
|
||||||
getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false
|
getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false
|
||||||
|
|
||||||
fun canVote(pluginUrl: String): Boolean {
|
fun canVote(pluginUrl: String): Boolean {
|
||||||
if (!PluginManager.urlPlugins.contains(pluginUrl)) return false
|
return PluginManager.urlPlugins.contains(pluginUrl)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val voteLock = Mutex()
|
private val voteLock = Mutex()
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import androidx.work.PeriodicWorkRequest
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel
|
import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel
|
||||||
import com.lagradost.cloudstream3.utils.BackupUtils
|
import com.lagradost.cloudstream3.utils.BackupUtils
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.services
|
package com.lagradost.cloudstream3.services
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -9,13 +10,13 @@ import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel
|
import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
|
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
|
||||||
|
|
@ -97,128 +98,138 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnspecifiedImmutableFlag")
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
|
try {
|
||||||
// println("Update subscriptions!")
|
// println("Update subscriptions!")
|
||||||
context.createNotificationChannel(
|
context.createNotificationChannel(
|
||||||
SUBSCRIPTION_CHANNEL_ID,
|
SUBSCRIPTION_CHANNEL_ID,
|
||||||
SUBSCRIPTION_CHANNEL_NAME,
|
SUBSCRIPTION_CHANNEL_NAME,
|
||||||
SUBSCRIPTION_CHANNEL_DESCRIPTION
|
SUBSCRIPTION_CHANNEL_DESCRIPTION
|
||||||
)
|
|
||||||
|
|
||||||
setForeground(
|
|
||||||
ForegroundInfo(
|
|
||||||
SUBSCRIPTION_NOTIFICATION_ID,
|
|
||||||
progressNotificationBuilder.build()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
val subscriptions = getAllSubscriptions()
|
setForeground(
|
||||||
|
ForegroundInfo(
|
||||||
|
SUBSCRIPTION_NOTIFICATION_ID,
|
||||||
|
progressNotificationBuilder.build()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if (subscriptions.isEmpty()) {
|
val subscriptions = getAllSubscriptions()
|
||||||
WorkManager.getInstance(context).cancelWorkById(this.id)
|
|
||||||
|
if (subscriptions.isEmpty()) {
|
||||||
|
WorkManager.getInstance(context).cancelWorkById(this.id)
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
val max = subscriptions.size
|
||||||
|
var progress = 0
|
||||||
|
|
||||||
|
updateProgress(max, progress, true)
|
||||||
|
|
||||||
|
// We need all plugins loaded.
|
||||||
|
PluginManager.loadAllOnlinePlugins(context)
|
||||||
|
PluginManager.loadAllLocalPlugins(context, false)
|
||||||
|
|
||||||
|
subscriptions.apmap { savedData ->
|
||||||
|
try {
|
||||||
|
val id = savedData.id ?: return@apmap null
|
||||||
|
val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null
|
||||||
|
|
||||||
|
// Reasonable timeout to prevent having this worker run forever.
|
||||||
|
val response = withTimeoutOrNull(60_000) {
|
||||||
|
api.load(savedData.url) as? EpisodeResponse
|
||||||
|
} ?: return@apmap null
|
||||||
|
|
||||||
|
val dubPreference =
|
||||||
|
getDub(id) ?: if (
|
||||||
|
context.getApiDubstatusSettings().contains(DubStatus.Dubbed)
|
||||||
|
) {
|
||||||
|
DubStatus.Dubbed
|
||||||
|
} else {
|
||||||
|
DubStatus.Subbed
|
||||||
|
}
|
||||||
|
|
||||||
|
val latestEpisodes = response.getLatestEpisodes()
|
||||||
|
val latestPreferredEpisode = latestEpisodes[dubPreference]
|
||||||
|
|
||||||
|
val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) {
|
||||||
|
val latestSeenEpisode =
|
||||||
|
savedData.lastSeenEpisodeCount[dubPreference] ?: Int.MIN_VALUE
|
||||||
|
val shouldUpdate = latestPreferredEpisode > latestSeenEpisode
|
||||||
|
shouldUpdate to latestPreferredEpisode
|
||||||
|
} else {
|
||||||
|
val latestEpisode = latestEpisodes[DubStatus.None] ?: Int.MIN_VALUE
|
||||||
|
val latestSeenEpisode =
|
||||||
|
savedData.lastSeenEpisodeCount[DubStatus.None] ?: Int.MIN_VALUE
|
||||||
|
val shouldUpdate = latestEpisode > latestSeenEpisode
|
||||||
|
shouldUpdate to latestEpisode
|
||||||
|
}
|
||||||
|
|
||||||
|
DataStoreHelper.updateSubscribedData(
|
||||||
|
id,
|
||||||
|
savedData,
|
||||||
|
response
|
||||||
|
)
|
||||||
|
|
||||||
|
if (shouldUpdate) {
|
||||||
|
val updateHeader = savedData.name
|
||||||
|
val updateDescription = txt(
|
||||||
|
R.string.subscription_episode_released,
|
||||||
|
latestEpisode,
|
||||||
|
savedData.name
|
||||||
|
).asString(context)
|
||||||
|
|
||||||
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
|
data = savedData.url.toUri()
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
}
|
||||||
|
|
||||||
|
val pendingIntent =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PendingIntent.getActivity(context, 0, intent, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
val poster = ioWork {
|
||||||
|
savedData.posterUrl?.let { url ->
|
||||||
|
context.getImageBitmapFromUrl(
|
||||||
|
url,
|
||||||
|
savedData.posterHeaders
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val updateNotification =
|
||||||
|
updateNotificationBuilder.setContentTitle(updateHeader)
|
||||||
|
.setContentText(updateDescription)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setLargeIcon(poster)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
notificationManager.notify(id, updateNotification)
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can probably get some issues here since this is async but it does not matter much.
|
||||||
|
updateProgress(max, ++progress, false)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
logError(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.success()
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
logError(t)
|
||||||
|
// ye, while this is not correct, but because gods know why android just crashes
|
||||||
|
// and this causes major battery usage as it retries it inf times. This is better, just
|
||||||
|
// in case android decides to be android and fuck us
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
val max = subscriptions.size
|
|
||||||
var progress = 0
|
|
||||||
|
|
||||||
updateProgress(max, progress, true)
|
|
||||||
|
|
||||||
// We need all plugins loaded.
|
|
||||||
PluginManager.loadAllOnlinePlugins(context)
|
|
||||||
PluginManager.loadAllLocalPlugins(context, false)
|
|
||||||
|
|
||||||
subscriptions.apmap { savedData ->
|
|
||||||
try {
|
|
||||||
val id = savedData.id ?: return@apmap null
|
|
||||||
val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null
|
|
||||||
|
|
||||||
// Reasonable timeout to prevent having this worker run forever.
|
|
||||||
val response = withTimeoutOrNull(60_000) {
|
|
||||||
api.load(savedData.url) as? EpisodeResponse
|
|
||||||
} ?: return@apmap null
|
|
||||||
|
|
||||||
val dubPreference =
|
|
||||||
getDub(id) ?: if (
|
|
||||||
context.getApiDubstatusSettings().contains(DubStatus.Dubbed)
|
|
||||||
) {
|
|
||||||
DubStatus.Dubbed
|
|
||||||
} else {
|
|
||||||
DubStatus.Subbed
|
|
||||||
}
|
|
||||||
|
|
||||||
val latestEpisodes = response.getLatestEpisodes()
|
|
||||||
val latestPreferredEpisode = latestEpisodes[dubPreference]
|
|
||||||
|
|
||||||
val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) {
|
|
||||||
val latestSeenEpisode =
|
|
||||||
savedData.lastSeenEpisodeCount[dubPreference] ?: Int.MIN_VALUE
|
|
||||||
val shouldUpdate = latestPreferredEpisode > latestSeenEpisode
|
|
||||||
shouldUpdate to latestPreferredEpisode
|
|
||||||
} else {
|
|
||||||
val latestEpisode = latestEpisodes[DubStatus.None] ?: Int.MIN_VALUE
|
|
||||||
val latestSeenEpisode =
|
|
||||||
savedData.lastSeenEpisodeCount[DubStatus.None] ?: Int.MIN_VALUE
|
|
||||||
val shouldUpdate = latestEpisode > latestSeenEpisode
|
|
||||||
shouldUpdate to latestEpisode
|
|
||||||
}
|
|
||||||
|
|
||||||
DataStoreHelper.updateSubscribedData(
|
|
||||||
id,
|
|
||||||
savedData,
|
|
||||||
response
|
|
||||||
)
|
|
||||||
|
|
||||||
if (shouldUpdate) {
|
|
||||||
val updateHeader = savedData.name
|
|
||||||
val updateDescription = txt(
|
|
||||||
R.string.subscription_episode_released,
|
|
||||||
latestEpisode,
|
|
||||||
savedData.name
|
|
||||||
).asString(context)
|
|
||||||
|
|
||||||
val intent = Intent(context, MainActivity::class.java).apply {
|
|
||||||
data = savedData.url.toUri()
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
}
|
|
||||||
|
|
||||||
val pendingIntent =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
0,
|
|
||||||
intent,
|
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
PendingIntent.getActivity(context, 0, intent, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
val poster = ioWork {
|
|
||||||
savedData.posterUrl?.let { url ->
|
|
||||||
context.getImageBitmapFromUrl(
|
|
||||||
url,
|
|
||||||
savedData.posterHeaders
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val updateNotification =
|
|
||||||
updateNotificationBuilder.setContentTitle(updateHeader)
|
|
||||||
.setContentText(updateDescription)
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.setLargeIcon(poster)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
notificationManager.notify(id, updateNotification)
|
|
||||||
}
|
|
||||||
|
|
||||||
// You can probably get some issues here since this is async but it does not matter much.
|
|
||||||
updateProgress(max, ++progress, false)
|
|
||||||
} catch (_: Throwable) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.success()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +59,7 @@ class SubtitleResource {
|
||||||
return file
|
return file
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unzip(file: File): List<Pair<String, File>> {
|
private fun unzip(file: File): List<Pair<String, File>> {
|
||||||
val entries = mutableListOf<Pair<String, File>>()
|
val entries = mutableListOf<Pair<String, File>>()
|
||||||
|
|
||||||
ZipInputStream(file.inputStream()).use { zipInputStream ->
|
ZipInputStream(file.inputStream()).use { zipInputStream ->
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,11 @@ class AbstractSubtitleEntities {
|
||||||
|
|
||||||
data class SubtitleSearch(
|
data class SubtitleSearch(
|
||||||
var query: String = "",
|
var query: String = "",
|
||||||
var imdb: Long? = null,
|
|
||||||
var lang: String? = null,
|
var lang: String? = null,
|
||||||
|
var imdbId: String? = null,
|
||||||
|
var tmdbId: Int? = null,
|
||||||
|
var malId: Int? = null,
|
||||||
|
var aniListId: Int? = null,
|
||||||
var epNumber: Int? = null,
|
var epNumber: Int? = null,
|
||||||
var seasonNumber: Int? = null,
|
var seasonNumber: Int? = null,
|
||||||
var year: Int? = null
|
var year: Int? = null
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,26 @@ package com.lagradost.cloudstream3.syncproviders
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.SubScene
|
import com.lagradost.cloudstream3.LoadResponse
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.*
|
import com.lagradost.cloudstream3.syncproviders.providers.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
||||||
companion object {
|
companion object {
|
||||||
val malApi = MALApi(0)
|
val malApi = MALApi(0).also { api ->
|
||||||
val aniListApi = AniListApi(0)
|
LoadResponse.Companion.malIdPrefix = api.idPrefix
|
||||||
|
}
|
||||||
|
val aniListApi = AniListApi(0).also { api ->
|
||||||
|
LoadResponse.Companion.aniListIdPrefix = api.idPrefix
|
||||||
|
}
|
||||||
|
val simklApi = SimklApi(0).also { api ->
|
||||||
|
LoadResponse.Companion.simklIdPrefix = api.idPrefix
|
||||||
|
}
|
||||||
val openSubtitlesApi = OpenSubtitlesApi(0)
|
val openSubtitlesApi = OpenSubtitlesApi(0)
|
||||||
val simklApi = SimklApi(0)
|
|
||||||
val indexSubtitlesApi = IndexSubtitleApi()
|
|
||||||
val addic7ed = Addic7ed()
|
val addic7ed = Addic7ed()
|
||||||
val subScene = SubScene()
|
val subDlApi = SubDlApi(0)
|
||||||
val localListApi = LocalList()
|
val localListApi = LocalList()
|
||||||
|
val subSourceApi = SubSourceApi()
|
||||||
|
|
||||||
// used to login via app intent
|
// used to login via app intent
|
||||||
val OAuth2Apis
|
val OAuth2Apis
|
||||||
|
|
@ -27,7 +33,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
||||||
// this needs init with context and can be accessed in settings
|
// this needs init with context and can be accessed in settings
|
||||||
val accountManagers
|
val accountManagers
|
||||||
get() = listOf(
|
get() = listOf(
|
||||||
malApi, aniListApi, openSubtitlesApi, simklApi //nginxApi
|
malApi, aniListApi, openSubtitlesApi, subDlApi, simklApi //nginxApi
|
||||||
)
|
)
|
||||||
|
|
||||||
// used for active syncing
|
// used for active syncing
|
||||||
|
|
@ -37,32 +43,35 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
||||||
)
|
)
|
||||||
|
|
||||||
val inAppAuths
|
val inAppAuths
|
||||||
get() = listOf(openSubtitlesApi)//, nginxApi)
|
get() = listOf<InAppAuthAPIManager>(
|
||||||
|
openSubtitlesApi,
|
||||||
|
subDlApi
|
||||||
|
)//, nginxApi)
|
||||||
|
|
||||||
val subtitleProviders
|
val subtitleProviders
|
||||||
get() = listOf(
|
get() = listOf(
|
||||||
openSubtitlesApi,
|
openSubtitlesApi,
|
||||||
indexSubtitlesApi, // they got anti scraping measures in place :(
|
|
||||||
addic7ed,
|
addic7ed,
|
||||||
subScene
|
subDlApi,
|
||||||
|
subSourceApi
|
||||||
)
|
)
|
||||||
|
|
||||||
const val appString = "cloudstreamapp"
|
const val APP_STRING = "cloudstreamapp"
|
||||||
const val appStringRepo = "cloudstreamrepo"
|
const val APP_STRING_REPO = "cloudstreamrepo"
|
||||||
const val appStringPlayer = "cloudstreamplayer"
|
const val APP_STRING_PLAYER = "cloudstreamplayer"
|
||||||
|
|
||||||
// Instantly start the search given a query
|
// Instantly start the search given a query
|
||||||
const val appStringSearch = "cloudstreamsearch"
|
const val APP_STRING_SEARCH = "cloudstreamsearch"
|
||||||
|
|
||||||
// Instantly resume watching a show
|
// Instantly resume watching a show
|
||||||
const val appStringResumeWatching = "cloudstreamcontinuewatching"
|
const val APP_STRING_RESUME_WATCHING = "cloudstreamcontinuewatching"
|
||||||
|
|
||||||
val unixTime: Long
|
val unixTime: Long
|
||||||
get() = System.currentTimeMillis() / 1000L
|
get() = System.currentTimeMillis() / 1000L
|
||||||
val unixTimeMs: Long
|
val unixTimeMs: Long
|
||||||
get() = System.currentTimeMillis()
|
get() = System.currentTimeMillis()
|
||||||
|
|
||||||
const val maxStale = 60 * 10
|
const val MAX_STALE = 60 * 10
|
||||||
|
|
||||||
fun secondsToReadable(seconds: Int, completedValue: String): String {
|
fun secondsToReadable(seconds: Int, completedValue: String): String {
|
||||||
var secondsLong = seconds.toLong()
|
var secondsLong = seconds.toLong()
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,23 @@ import androidx.fragment.app.FragmentActivity
|
||||||
interface OAuth2API : AuthAPI {
|
interface OAuth2API : AuthAPI {
|
||||||
val key: String
|
val key: String
|
||||||
val redirectUrl: String
|
val redirectUrl: String
|
||||||
|
val supportDeviceAuth: Boolean
|
||||||
|
|
||||||
suspend fun handleRedirect(url: String) : Boolean
|
suspend fun handleRedirect(url: String) : Boolean
|
||||||
fun authenticate(activity: FragmentActivity?)
|
fun authenticate(activity: FragmentActivity?)
|
||||||
|
suspend fun getDevicePin() : PinAuthData? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun handleDeviceAuth(pinAuthData: PinAuthData) : Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PinAuthData(
|
||||||
|
val deviceCode: String,
|
||||||
|
val userCode: String,
|
||||||
|
val verificationUrl: String,
|
||||||
|
val expiresIn: Int,
|
||||||
|
val interval: Int,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -2,19 +2,10 @@ package com.lagradost.cloudstream3.syncproviders
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.ui.SyncWatchType
|
import com.lagradost.cloudstream3.ui.SyncWatchType
|
||||||
import com.lagradost.cloudstream3.ui.WatchType
|
|
||||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||||
import com.lagradost.cloudstream3.ui.result.UiText
|
import com.lagradost.cloudstream3.ui.result.UiText
|
||||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||||
|
import java.util.Date
|
||||||
enum class SyncIdName {
|
|
||||||
Anilist,
|
|
||||||
MyAnimeList,
|
|
||||||
Trakt,
|
|
||||||
Imdb,
|
|
||||||
Simkl,
|
|
||||||
LocalList,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SyncAPI : OAuth2API {
|
interface SyncAPI : OAuth2API {
|
||||||
/**
|
/**
|
||||||
|
|
@ -134,6 +125,8 @@ interface SyncAPI : OAuth2API {
|
||||||
ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed()
|
ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed()
|
||||||
ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) }
|
ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) }
|
||||||
ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime }
|
ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime }
|
||||||
|
ListSorting.ReleaseDateNew -> items.sortedByDescending { it.releaseDate }
|
||||||
|
ListSorting.ReleaseDateOld -> items.sortedBy { it.releaseDate }
|
||||||
else -> items
|
else -> items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -168,9 +161,10 @@ interface SyncAPI : OAuth2API {
|
||||||
override var posterUrl: String?,
|
override var posterUrl: String?,
|
||||||
override var posterHeaders: Map<String, String>?,
|
override var posterHeaders: Map<String, String>?,
|
||||||
override var quality: SearchQuality?,
|
override var quality: SearchQuality?,
|
||||||
|
val releaseDate: Date?,
|
||||||
override var id: Int? = null,
|
override var id: Int? = null,
|
||||||
val plot : String? = null,
|
val plot : String? = null,
|
||||||
val rating: Int? = null,
|
val rating: Int? = null,
|
||||||
val tags: List<String>? = null,
|
val tags: List<String>? = null
|
||||||
) : SearchResponse
|
) : SearchResponse
|
||||||
}
|
}
|
||||||
|
|
@ -18,13 +18,13 @@ class Addic7ed : AbstractSubApi {
|
||||||
override fun logOut() {}
|
override fun logOut() {}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val host = "https://www.addic7ed.com"
|
const val HOST = "https://www.addic7ed.com"
|
||||||
const val TAG = "ADDIC7ED"
|
const val TAG = "ADDIC7ED"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fixUrl(url: String): String {
|
private fun fixUrl(url: String): String {
|
||||||
return if (url.startsWith("/")) host + url
|
return if (url.startsWith("/")) HOST + url
|
||||||
else if (!url.startsWith("http")) "$host/$url"
|
else if (!url.startsWith("http")) "$HOST/$url"
|
||||||
else url
|
else url
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +62,7 @@ class Addic7ed : AbstractSubApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
val title = queryText.substringBefore("(").trim()
|
val title = queryText.substringBefore("(").trim()
|
||||||
val url = "$host/search.php?search=${title}&Submit=Search"
|
val url = "$HOST/search.php?search=${title}&Submit=Search"
|
||||||
val hostDocument = app.get(url).document
|
val hostDocument = app.get(url).document
|
||||||
var searchResult = ""
|
var searchResult = ""
|
||||||
if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url
|
if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url
|
||||||
|
|
@ -74,8 +74,8 @@ class Addic7ed : AbstractSubApi {
|
||||||
hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(")
|
hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(")
|
||||||
?.substringBefore(",")
|
?.substringBefore(",")
|
||||||
val doc = app.get(
|
val doc = app.get(
|
||||||
"$host/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined",
|
"$HOST/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined",
|
||||||
referer = "$host/"
|
referer = "$HOST/"
|
||||||
).document
|
).document
|
||||||
doc.select("#season tr:contains($queryLang)").mapNotNull { node ->
|
doc.select("#season tr:contains($queryLang)").mapNotNull { node ->
|
||||||
if (node.selectFirst("td")?.text()
|
if (node.selectFirst("td")?.text()
|
||||||
|
|
@ -97,7 +97,7 @@ class Addic7ed : AbstractSubApi {
|
||||||
val link = fixUrl(node.select("a.buttonDownload").attr("href"))
|
val link = fixUrl(node.select("a.buttonDownload").attr("href"))
|
||||||
val isHearingImpaired =
|
val isHearingImpaired =
|
||||||
!node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty()
|
!node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty()
|
||||||
cleanResources(results, name, link, mapOf("referer" to "$host/"), isHearingImpaired)
|
cleanResources(results, name, link, mapOf("referer" to "$HOST/"), isHearingImpaired)
|
||||||
}
|
}
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,16 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||||
import com.lagradost.cloudstream3.ui.SyncWatchType
|
import com.lagradost.cloudstream3.ui.SyncWatchType
|
||||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
|
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
|
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
override var name = "AniList"
|
override var name = "AniList"
|
||||||
|
|
@ -32,6 +33,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
override val redirectUrl = "anilistlogin"
|
override val redirectUrl = "anilistlogin"
|
||||||
override val idPrefix = "anilist"
|
override val idPrefix = "anilist"
|
||||||
override var requireLibraryRefresh = true
|
override var requireLibraryRefresh = true
|
||||||
|
override val supportDeviceAuth = false
|
||||||
override var mainUrl = "https://anilist.co"
|
override var mainUrl = "https://anilist.co"
|
||||||
override val icon = R.drawable.ic_anilist_icon
|
override val icon = R.drawable.ic_anilist_icon
|
||||||
override val requiresLogin = false
|
override val requiresLogin = false
|
||||||
|
|
@ -62,7 +64,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
override suspend fun handleRedirect(url: String): Boolean {
|
override suspend fun handleRedirect(url: String): Boolean {
|
||||||
val sanitizer =
|
val sanitizer =
|
||||||
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
|
splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR
|
||||||
val token = sanitizer["access_token"]!!
|
val token = sanitizer["access_token"]!!
|
||||||
val expiresIn = sanitizer["expires_in"]!!
|
val expiresIn = sanitizer["expires_in"]!!
|
||||||
|
|
||||||
|
|
@ -86,7 +88,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
|
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
|
||||||
val data = searchShows(name) ?: return null
|
val data = searchShows(name) ?: return null
|
||||||
return data.data?.Page?.media?.map {
|
return data.data?.page?.media?.map {
|
||||||
SyncAPI.SyncSearchResult(
|
SyncAPI.SyncSearchResult(
|
||||||
it.title.romaji ?: return null,
|
it.title.romaji ?: return null,
|
||||||
this.name,
|
this.name,
|
||||||
|
|
@ -100,7 +102,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
override suspend fun getResult(id: String): SyncAPI.SyncResult {
|
override suspend fun getResult(id: String): SyncAPI.SyncResult {
|
||||||
val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1)
|
val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1)
|
||||||
?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId")
|
?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId")
|
||||||
val season = getSeason(internalId).data.Media
|
val season = getSeason(internalId).data.media
|
||||||
|
|
||||||
return SyncAPI.SyncResult(
|
return SyncAPI.SyncResult(
|
||||||
season.id.toString(),
|
season.id.toString(),
|
||||||
|
|
@ -300,12 +302,12 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
//println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}")
|
//println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}")
|
||||||
val shows = searchShows(name.replace(blackListRegex, ""))
|
val shows = searchShows(name.replace(blackListRegex, ""))
|
||||||
|
|
||||||
shows?.data?.Page?.media?.find {
|
shows?.data?.page?.media?.find {
|
||||||
(malId ?: "NONE") == it.idMal.toString()
|
(malId ?: "NONE") == it.idMal.toString()
|
||||||
}?.let { return it }
|
}?.let { return it }
|
||||||
|
|
||||||
val filtered =
|
val filtered =
|
||||||
shows?.data?.Page?.media?.filter {
|
shows?.data?.page?.media?.filter {
|
||||||
(((it.startDate.year ?: year.toString()) == year.toString()
|
(((it.startDate.year ?: year.toString()) == year.toString()
|
||||||
|| year == null))
|
|| year == null))
|
||||||
}
|
}
|
||||||
|
|
@ -495,7 +497,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val data = postApi(q, true)
|
val data = postApi(q, true)
|
||||||
val d = parseJson<GetDataRoot>(data ?: return null)
|
val d = parseJson<GetDataRoot>(data ?: return null)
|
||||||
|
|
||||||
val main = d.data?.Media
|
val main = d.data?.media
|
||||||
if (main?.mediaListEntry != null) {
|
if (main?.mediaListEntry != null) {
|
||||||
return AniListTitleHolder(
|
return AniListTitleHolder(
|
||||||
title = main.title,
|
title = main.title,
|
||||||
|
|
@ -535,7 +537,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
headers = mapOf(
|
headers = mapOf(
|
||||||
"Authorization" to "Bearer " + (getAuth()
|
"Authorization" to "Bearer " + (getAuth()
|
||||||
?: return@suspendSafeApiCall null),
|
?: return@suspendSafeApiCall null),
|
||||||
if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
|
if (cache) "Cache-Control" to "max-stale=$MAX_STALE" else "Cache-Control" to "no-cache"
|
||||||
),
|
),
|
||||||
cacheTime = 0,
|
cacheTime = 0,
|
||||||
data = mapOf(
|
data = mapOf(
|
||||||
|
|
@ -630,8 +632,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
?: this.media.coverImage.medium,
|
?: this.media.coverImage.medium,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
this.media.seasonYear.toYear(),
|
||||||
null,
|
null,
|
||||||
plot = this.media.description
|
plot = this.media.description,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -646,7 +649,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Data(
|
data class Data(
|
||||||
@JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection
|
@JsonProperty("MediaListCollection") val mediaListCollection: MediaListCollection
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun getAniListListCached(): Array<Lists>? {
|
private fun getAniListListCached(): Array<Lists>? {
|
||||||
|
|
@ -658,7 +661,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
if (checkToken()) return null
|
if (checkToken()) return null
|
||||||
return if (requireLibraryRefresh) {
|
return if (requireLibraryRefresh) {
|
||||||
val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray()
|
val list = getFullAniListList()?.data?.mediaListCollection?.lists?.toTypedArray()
|
||||||
if (list != null) {
|
if (list != null) {
|
||||||
setKey(ANILIST_CACHED_LIST, list)
|
setKey(ANILIST_CACHED_LIST, list)
|
||||||
}
|
}
|
||||||
|
|
@ -677,7 +680,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
// To fill empty lists when AniList does not return them
|
// To fill empty lists when AniList does not return them
|
||||||
val baseMap =
|
val baseMap =
|
||||||
AniListStatusType.values().filter { it.value >= 0 }.associate {
|
AniListStatusType.entries.filter { it.value >= 0 }.associate {
|
||||||
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -688,6 +691,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
ListSorting.AlphabeticalZ,
|
ListSorting.AlphabeticalZ,
|
||||||
ListSorting.UpdatedNew,
|
ListSorting.UpdatedNew,
|
||||||
ListSorting.UpdatedOld,
|
ListSorting.UpdatedOld,
|
||||||
|
ListSorting.ReleaseDateNew,
|
||||||
|
ListSorting.ReleaseDateOld,
|
||||||
ListSorting.RatingHigh,
|
ListSorting.RatingHigh,
|
||||||
ListSorting.RatingLow,
|
ListSorting.RatingLow,
|
||||||
)
|
)
|
||||||
|
|
@ -763,7 +768,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
/** Used to query a saved MediaItem on the list to get the id for removal */
|
/** Used to query a saved MediaItem on the list to get the id for removal */
|
||||||
data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null)
|
data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null)
|
||||||
data class MediaListItem(@JsonProperty("MediaList") val MediaList: MediaListId? = null)
|
data class MediaListItem(@JsonProperty("MediaList") val mediaList: MediaListId? = null)
|
||||||
data class MediaListId(@JsonProperty("id") val id: Long? = null)
|
data class MediaListId(@JsonProperty("id") val id: Long? = null)
|
||||||
|
|
||||||
private suspend fun postDataAboutId(
|
private suspend fun postDataAboutId(
|
||||||
|
|
@ -786,7 +791,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
"""
|
"""
|
||||||
val response = postApi(idQuery)
|
val response = postApi(idQuery)
|
||||||
val listId =
|
val listId =
|
||||||
tryParseJson<MediaListItemRoot>(response)?.data?.MediaList?.id ?: return false
|
tryParseJson<MediaListItemRoot>(response)?.data?.mediaList?.id ?: return false
|
||||||
"""
|
"""
|
||||||
mutation(${'$'}id: Int = $listId) {
|
mutation(${'$'}id: Int = $listId) {
|
||||||
DeleteMediaListEntry(id: ${'$'}id) {
|
DeleteMediaListEntry(id: ${'$'}id) {
|
||||||
|
|
@ -835,7 +840,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val data = postApi(q)
|
val data = postApi(q)
|
||||||
if (data.isNullOrBlank()) return null
|
if (data.isNullOrBlank()) return null
|
||||||
val userData = parseJson<AniListRoot>(data)
|
val userData = parseJson<AniListRoot>(data)
|
||||||
val u = userData.data?.Viewer
|
val u = userData.data?.viewer
|
||||||
val user = AniListUser(
|
val user = AniListUser(
|
||||||
u?.id,
|
u?.id,
|
||||||
u?.name,
|
u?.name,
|
||||||
|
|
@ -857,8 +862,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
suspend fun getSeasonRecursive(id: Int) {
|
suspend fun getSeasonRecursive(id: Int) {
|
||||||
val season = getSeason(id)
|
val season = getSeason(id)
|
||||||
seasons.add(season)
|
seasons.add(season)
|
||||||
if (season.data.Media.format?.startsWith("TV") == true) {
|
if (season.data.media.format?.startsWith("TV") == true) {
|
||||||
season.data.Media.relations?.edges?.forEach {
|
season.data.media.relations?.edges?.forEach {
|
||||||
if (it.node?.format != null) {
|
if (it.node?.format != null) {
|
||||||
if (it.relationType == "SEQUEL" && it.node.format.startsWith("TV")) {
|
if (it.relationType == "SEQUEL" && it.node.format.startsWith("TV")) {
|
||||||
getSeasonRecursive(it.node.id)
|
getSeasonRecursive(it.node.id)
|
||||||
|
|
@ -877,7 +882,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SeasonData(
|
data class SeasonData(
|
||||||
@JsonProperty("Media") val Media: SeasonMedia,
|
@JsonProperty("Media") val media: SeasonMedia,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SeasonMedia(
|
data class SeasonMedia(
|
||||||
|
|
@ -1049,7 +1054,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class AniListData(
|
data class AniListData(
|
||||||
@JsonProperty("Viewer") val Viewer: AniListViewer?,
|
@JsonProperty("Viewer") val viewer: AniListViewer?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class AniListRoot(
|
data class AniListRoot(
|
||||||
|
|
@ -1089,7 +1094,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class LikeData(
|
data class LikeData(
|
||||||
@JsonProperty("Viewer") val Viewer: LikeViewer?,
|
@JsonProperty("Viewer") val viewer: LikeViewer?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class LikeRoot(
|
data class LikeRoot(
|
||||||
|
|
@ -1129,7 +1134,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GetDataData(
|
data class GetDataData(
|
||||||
@JsonProperty("Media") val Media: GetDataMedia?,
|
@JsonProperty("Media") val media: GetDataMedia?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GetDataRoot(
|
data class GetDataRoot(
|
||||||
|
|
@ -1162,7 +1167,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GetSearchPage(
|
data class GetSearchPage(
|
||||||
@JsonProperty("Page") val Page: GetSearchData?,
|
@JsonProperty("Page") val page: GetSearchData?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GetSearchData(
|
data class GetSearchData(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ class Dropbox : OAuth2API {
|
||||||
override val key = "zlqsamadlwydvb2"
|
override val key = "zlqsamadlwydvb2"
|
||||||
override val redirectUrl = "dropboxlogin"
|
override val redirectUrl = "dropboxlogin"
|
||||||
override val requiresLogin = true
|
override val requiresLogin = true
|
||||||
|
override val supportDeviceAuth = false
|
||||||
override val createAccountUrl: String? = null
|
override val createAccountUrl: String? = null
|
||||||
|
|
||||||
override val icon: Int
|
override val icon: Int
|
||||||
|
|
|
||||||
|
|
@ -1,265 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.syncproviders.providers
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.lagradost.cloudstream3.TvType
|
|
||||||
import com.lagradost.cloudstream3.app
|
|
||||||
import com.lagradost.cloudstream3.imdbUrlToIdNullable
|
|
||||||
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
|
|
||||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
|
||||||
|
|
||||||
class IndexSubtitleApi : AbstractSubApi {
|
|
||||||
override val name = "IndexSubtitle"
|
|
||||||
override val idPrefix = "indexsubtitle"
|
|
||||||
override val requiresLogin = false
|
|
||||||
override val icon: Nothing? = null
|
|
||||||
override val createAccountUrl: Nothing? = null
|
|
||||||
|
|
||||||
override fun loginInfo(): Nothing? = null
|
|
||||||
|
|
||||||
override fun logOut() {}
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val host = "https://indexsubtitle.com"
|
|
||||||
const val TAG = "INDEXSUBS"
|
|
||||||
|
|
||||||
fun getOrdinal(num: Int?): String? {
|
|
||||||
return when (num) {
|
|
||||||
1 -> "First"
|
|
||||||
2 -> "Second"
|
|
||||||
3 -> "Third"
|
|
||||||
4 -> "Fourth"
|
|
||||||
5 -> "Fifth"
|
|
||||||
6 -> "Sixth"
|
|
||||||
7 -> "Seventh"
|
|
||||||
8 -> "Eighth"
|
|
||||||
9 -> "Ninth"
|
|
||||||
10 -> "Tenth"
|
|
||||||
11 -> "Eleventh"
|
|
||||||
12 -> "Twelfth"
|
|
||||||
13 -> "Thirteenth"
|
|
||||||
14 -> "Fourteenth"
|
|
||||||
15 -> "Fifteenth"
|
|
||||||
16 -> "Sixteenth"
|
|
||||||
17 -> "Seventeenth"
|
|
||||||
18 -> "Eighteenth"
|
|
||||||
19 -> "Nineteenth"
|
|
||||||
20 -> "Twentieth"
|
|
||||||
21 -> "Twenty-First"
|
|
||||||
22 -> "Twenty-Second"
|
|
||||||
23 -> "Twenty-Third"
|
|
||||||
24 -> "Twenty-Fourth"
|
|
||||||
25 -> "Twenty-Fifth"
|
|
||||||
26 -> "Twenty-Sixth"
|
|
||||||
27 -> "Twenty-Seventh"
|
|
||||||
28 -> "Twenty-Eighth"
|
|
||||||
29 -> "Twenty-Ninth"
|
|
||||||
30 -> "Thirtieth"
|
|
||||||
31 -> "Thirty-First"
|
|
||||||
32 -> "Thirty-Second"
|
|
||||||
33 -> "Thirty-Third"
|
|
||||||
34 -> "Thirty-Fourth"
|
|
||||||
35 -> "Thirty-Fifth"
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fixUrl(url: String): String {
|
|
||||||
if (url.startsWith("http")) {
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
if (url.isEmpty()) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
val startsWithNoHttp = url.startsWith("//")
|
|
||||||
if (startsWithNoHttp) {
|
|
||||||
return "https:$url"
|
|
||||||
} else {
|
|
||||||
if (url.startsWith('/')) {
|
|
||||||
return host + url
|
|
||||||
}
|
|
||||||
return "$host/$url"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isRightEps(text: String, seasonNum: Int?, epNum: Int?): Boolean {
|
|
||||||
val FILTER_EPS_REGEX =
|
|
||||||
Regex("(?i)((Chapter\\s?0?${epNum})|((Season)?\\s?0?${seasonNum}?\\s?(Episode)\\s?0?${epNum}[^0-9]))|(?i)((S?0?${seasonNum}?E0?${epNum}[^0-9])|(0?${seasonNum}[a-z]0?${epNum}[^0-9]))")
|
|
||||||
return text.contains(FILTER_EPS_REGEX)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun haveEps(text: String): Boolean {
|
|
||||||
val HAVE_EPS_REGEX =
|
|
||||||
Regex("(?i)((Chapter\\s?0?\\d)|((Season)?\\s?0?\\d?\\s?(Episode)\\s?0?\\d))|(?i)((S?0?\\d?E0?\\d)|(0?\\d[a-z]0?\\d))")
|
|
||||||
return text.contains(HAVE_EPS_REGEX)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity> {
|
|
||||||
val imdbId = query.imdb ?: 0
|
|
||||||
val lang = query.lang
|
|
||||||
val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString())
|
|
||||||
val queryText = query.query
|
|
||||||
val epNum = query.epNumber ?: 0
|
|
||||||
val seasonNum = query.seasonNumber ?: 0
|
|
||||||
val yearNum = query.year ?: 0
|
|
||||||
|
|
||||||
val urlItems = ArrayList<String>()
|
|
||||||
|
|
||||||
fun cleanResources(
|
|
||||||
results: MutableList<AbstractSubtitleEntities.SubtitleEntity>,
|
|
||||||
name: String,
|
|
||||||
link: String
|
|
||||||
) {
|
|
||||||
results.add(
|
|
||||||
AbstractSubtitleEntities.SubtitleEntity(
|
|
||||||
idPrefix = idPrefix,
|
|
||||||
name = name,
|
|
||||||
lang = queryLang.toString(),
|
|
||||||
data = link,
|
|
||||||
source = this.name,
|
|
||||||
type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie,
|
|
||||||
epNumber = epNum,
|
|
||||||
seasonNumber = seasonNum,
|
|
||||||
year = yearNum,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val document = app.get("$host/?search=$queryText").document
|
|
||||||
|
|
||||||
document.select("div.my-3.p-3 div.media").map { block ->
|
|
||||||
if (seasonNum > 0) {
|
|
||||||
val name = block.select("strong.text-primary, strong.text-info").text().trim()
|
|
||||||
val season = getOrdinal(seasonNum)
|
|
||||||
if ((block.selectFirst("a")?.attr("href")
|
|
||||||
?.contains(
|
|
||||||
"$season",
|
|
||||||
ignoreCase = true
|
|
||||||
)!! || name.contains(
|
|
||||||
"$season",
|
|
||||||
ignoreCase = true
|
|
||||||
)) && name.contains(queryText, ignoreCase = true)
|
|
||||||
) {
|
|
||||||
block.select("div.media").mapNotNull {
|
|
||||||
urlItems.add(
|
|
||||||
fixUrl(
|
|
||||||
it.selectFirst("a")!!.attr("href")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (block.selectFirst("strong")!!.text().trim()
|
|
||||||
.matches(Regex("(?i)^$queryText\$"))
|
|
||||||
) {
|
|
||||||
if (block.select("span[title=Release]").isNullOrEmpty()) {
|
|
||||||
block.select("div.media").mapNotNull {
|
|
||||||
val urlItem = fixUrl(
|
|
||||||
it.selectFirst("a")!!.attr("href")
|
|
||||||
)
|
|
||||||
val itemDoc = app.get(urlItem).document
|
|
||||||
val id = imdbUrlToIdNullable(
|
|
||||||
itemDoc.selectFirst("div.d-flex span.badge.badge-primary")?.parent()
|
|
||||||
?.attr("href")
|
|
||||||
)?.toLongOrNull()
|
|
||||||
val year = itemDoc.selectFirst("div.d-flex span.badge.badge-success")
|
|
||||||
?.ownText()
|
|
||||||
?.trim().toString()
|
|
||||||
Log.i(TAG, "id => $id \nyear => $year||$yearNum")
|
|
||||||
if (imdbId > 0) {
|
|
||||||
if (id == imdbId) {
|
|
||||||
urlItems.add(urlItem)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (year.contains("$yearNum")) {
|
|
||||||
urlItems.add(urlItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (block.select("span[title=Release]").text().trim()
|
|
||||||
.contains("$yearNum")
|
|
||||||
) {
|
|
||||||
block.select("div.media").mapNotNull {
|
|
||||||
urlItems.add(
|
|
||||||
fixUrl(
|
|
||||||
it.selectFirst("a")!!.attr("href")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.i(TAG, "urlItems => $urlItems")
|
|
||||||
val results = mutableListOf<AbstractSubtitleEntities.SubtitleEntity>()
|
|
||||||
|
|
||||||
urlItems.forEach { url ->
|
|
||||||
val request = app.get(url)
|
|
||||||
if (request.isSuccessful) {
|
|
||||||
request.document.select("div.my-3.p-3 div.media").map { block ->
|
|
||||||
if (block.select("span.d-block span[data-original-title=Language]").text()
|
|
||||||
.trim()
|
|
||||||
.contains("$queryLang")
|
|
||||||
) {
|
|
||||||
var name = block.select("strong.text-primary, strong.text-info").text().trim()
|
|
||||||
val link = fixUrl(block.selectFirst("a")!!.attr("href"))
|
|
||||||
if (seasonNum > 0) {
|
|
||||||
when {
|
|
||||||
isRightEps(name, seasonNum, epNum) -> {
|
|
||||||
cleanResources(results, name, link)
|
|
||||||
}
|
|
||||||
!(haveEps(name)) -> {
|
|
||||||
name = "$name (S${seasonNum}:E${epNum})"
|
|
||||||
cleanResources(results, name, link)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cleanResources(results, name, link)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String? {
|
|
||||||
val seasonNum = data.seasonNumber
|
|
||||||
val epNum = data.epNumber
|
|
||||||
|
|
||||||
val req = app.get(data.data)
|
|
||||||
|
|
||||||
if (req.isSuccessful) {
|
|
||||||
val document = req.document
|
|
||||||
val link = if (document.select("div.my-3.p-3 div.media").size == 1) {
|
|
||||||
fixUrl(
|
|
||||||
document.selectFirst("div.my-3.p-3 div.media a")!!.attr("href")
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
document.select("div.my-3.p-3 div.media").firstNotNullOf { block ->
|
|
||||||
val name =
|
|
||||||
block.selectFirst("strong.d-block")?.text()?.trim().toString()
|
|
||||||
if (seasonNum!! > 0) {
|
|
||||||
if (isRightEps(name, seasonNum, epNum)) {
|
|
||||||
fixUrl(block.selectFirst("a")!!.attr("href"))
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fixUrl(block.selectFirst("a")!!.attr("href"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return link
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -8,7 +8,8 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||||
import com.lagradost.cloudstream3.ui.WatchType
|
import com.lagradost.cloudstream3.ui.WatchType
|
||||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
|
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
|
||||||
|
|
@ -20,6 +21,7 @@ class LocalList : SyncAPI {
|
||||||
override val name = "Local"
|
override val name = "Local"
|
||||||
override val icon: Int = R.drawable.ic_baseline_storage_24
|
override val icon: Int = R.drawable.ic_baseline_storage_24
|
||||||
override val requiresLogin = false
|
override val requiresLogin = false
|
||||||
|
override val supportDeviceAuth = false
|
||||||
override val createAccountUrl: Nothing? = null
|
override val createAccountUrl: Nothing? = null
|
||||||
override val idPrefix = "local"
|
override val idPrefix = "local"
|
||||||
override var requireLibraryRefresh = true
|
override var requireLibraryRefresh = true
|
||||||
|
|
@ -71,9 +73,9 @@ class LocalList : SyncAPI {
|
||||||
}?.distinctBy { it.first } ?: return null
|
}?.distinctBy { it.first } ?: return null
|
||||||
|
|
||||||
val list = ioWork {
|
val list = ioWork {
|
||||||
val isTrueTv = isTrueTvSettings()
|
val isTrueTv = isLayout(TV)
|
||||||
|
|
||||||
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
|
val baseMap = WatchType.entries.filter { it != WatchType.NONE }.associate {
|
||||||
// None is not something to display
|
// None is not something to display
|
||||||
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
||||||
} + mapOf(
|
} + mapOf(
|
||||||
|
|
@ -117,8 +119,11 @@ class LocalList : SyncAPI {
|
||||||
ListSorting.AlphabeticalZ,
|
ListSorting.AlphabeticalZ,
|
||||||
ListSorting.UpdatedNew,
|
ListSorting.UpdatedNew,
|
||||||
ListSorting.UpdatedOld,
|
ListSorting.UpdatedOld,
|
||||||
|
ListSorting.ReleaseDateNew,
|
||||||
|
ListSorting.ReleaseDateOld,
|
||||||
// ListSorting.RatingHigh,
|
// ListSorting.RatingHigh,
|
||||||
// ListSorting.RatingLow,
|
// ListSorting.RatingLow,
|
||||||
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,19 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||||
import com.lagradost.cloudstream3.ui.SyncWatchType
|
import com.lagradost.cloudstream3.ui.SyncWatchType
|
||||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
|
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
|
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.time.Instant
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
/** max 100 via https://myanimelist.net/apiconfig/references/api/v2#tag/anime */
|
/** max 100 via https://myanimelist.net/apiconfig/references/api/v2#tag/anime */
|
||||||
const val MAL_MAX_SEARCH_LIMIT = 25
|
const val MAL_MAX_SEARCH_LIMIT = 25
|
||||||
|
|
@ -40,6 +45,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
private val apiUrl = "https://api.myanimelist.net"
|
private val apiUrl = "https://api.myanimelist.net"
|
||||||
override val icon = R.drawable.mal_logo
|
override val icon = R.drawable.mal_logo
|
||||||
override val requiresLogin = false
|
override val requiresLogin = false
|
||||||
|
override val supportDeviceAuth = false
|
||||||
override val syncIdName = SyncIdName.MyAnimeList
|
override val syncIdName = SyncIdName.MyAnimeList
|
||||||
override var requireLibraryRefresh = true
|
override var requireLibraryRefresh = true
|
||||||
override val createAccountUrl = "$mainUrl/register.php"
|
override val createAccountUrl = "$mainUrl/register.php"
|
||||||
|
|
@ -50,7 +56,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loginInfo(): AuthAPI.LoginInfo? {
|
override fun loginInfo(): AuthAPI.LoginInfo? {
|
||||||
//getMalUser(true)?
|
|
||||||
getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
|
getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
|
||||||
return AuthAPI.LoginInfo(
|
return AuthAPI.LoginInfo(
|
||||||
profilePicture = user.picture,
|
profilePicture = user.picture,
|
||||||
|
|
@ -83,7 +88,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
this.name,
|
this.name,
|
||||||
node.id.toString(),
|
node.id.toString(),
|
||||||
"$mainUrl/anime/${node.id}/",
|
"$mainUrl/anime/${node.id}/",
|
||||||
node.main_picture?.large ?: node.main_picture?.medium
|
node.mainPicture?.large ?: node.mainPicture?.medium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -177,7 +182,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
private fun parseDate(string: String?): Long? {
|
private fun parseDate(string: String?): Long? {
|
||||||
return try {
|
return try {
|
||||||
SimpleDateFormat("yyyy-MM-dd")?.parse(string ?: return null)?.time
|
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(string ?: return null)?.time
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
@ -189,7 +194,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
apiName = this.name,
|
apiName = this.name,
|
||||||
syncId = node.id.toString(),
|
syncId = node.id.toString(),
|
||||||
url = "$mainUrl/anime/${node.id}",
|
url = "$mainUrl/anime/${node.id}",
|
||||||
posterUrl = node.main_picture?.large
|
posterUrl = node.mainPicture?.large
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,12 +248,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val internalId = id.toIntOrNull() ?: return null
|
val internalId = id.toIntOrNull() ?: return null
|
||||||
|
|
||||||
val data =
|
val data =
|
||||||
getDataAboutMalId(internalId)?.my_list_status //?: throw ErrorLoadingException("No my_list_status")
|
getDataAboutMalId(internalId)?.myListStatus //?: throw ErrorLoadingException("No my_list_status")
|
||||||
return SyncAPI.SyncStatus(
|
return SyncAPI.SyncStatus(
|
||||||
score = data?.score,
|
score = data?.score,
|
||||||
status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)) ,
|
status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)),
|
||||||
isFavorite = null,
|
isFavorite = null,
|
||||||
watchedEpisodes = data?.num_episodes_watched,
|
watchedEpisodes = data?.numEpisodesWatched,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,7 +295,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
private fun parseDateLong(string: String?): Long? {
|
private fun parseDateLong(string: String?): Long? {
|
||||||
return try {
|
return try {
|
||||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(
|
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse(
|
||||||
string ?: return null
|
string ?: return null
|
||||||
)?.time?.div(1000)
|
)?.time?.div(1000)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
@ -301,7 +306,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
override suspend fun handleRedirect(url: String): Boolean {
|
override suspend fun handleRedirect(url: String): Boolean {
|
||||||
val sanitizer =
|
val sanitizer =
|
||||||
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
|
splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR
|
||||||
val state = sanitizer["state"]!!
|
val state = sanitizer["state"]!!
|
||||||
if (state == "RequestID$requestId") {
|
if (state == "RequestID$requestId") {
|
||||||
val currentCode = sanitizer["code"]!!
|
val currentCode = sanitizer["code"]!!
|
||||||
|
|
@ -350,9 +355,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
try {
|
try {
|
||||||
if (response != "") {
|
if (response != "") {
|
||||||
val token = parseJson<ResponseToken>(response)
|
val token = parseJson<ResponseToken>(response)
|
||||||
setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime))
|
setKey(accountId, MAL_UNIXTIME_KEY, (token.expiresIn + unixTime))
|
||||||
setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token)
|
setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refreshToken)
|
||||||
setKey(accountId, MAL_TOKEN_KEY, token.access_token)
|
setKey(accountId, MAL_TOKEN_KEY, token.accessToken)
|
||||||
requireLibraryRefresh = true
|
requireLibraryRefresh = true
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
@ -394,56 +399,62 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
data class Node(
|
data class Node(
|
||||||
@JsonProperty("id") val id: Int,
|
@JsonProperty("id") val id: Int,
|
||||||
@JsonProperty("title") val title: String,
|
@JsonProperty("title") val title: String,
|
||||||
@JsonProperty("main_picture") val main_picture: MainPicture?,
|
@JsonProperty("main_picture") val mainPicture: MainPicture?,
|
||||||
@JsonProperty("alternative_titles") val alternative_titles: AlternativeTitles?,
|
@JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?,
|
||||||
@JsonProperty("media_type") val media_type: String?,
|
@JsonProperty("media_type") val mediaType: String?,
|
||||||
@JsonProperty("num_episodes") val num_episodes: Int?,
|
@JsonProperty("num_episodes") val numEpisodes: Int?,
|
||||||
@JsonProperty("status") val status: String?,
|
@JsonProperty("status") val status: String?,
|
||||||
@JsonProperty("start_date") val start_date: String?,
|
@JsonProperty("start_date") val startDate: String?,
|
||||||
@JsonProperty("end_date") val end_date: String?,
|
@JsonProperty("end_date") val endDate: String?,
|
||||||
@JsonProperty("average_episode_duration") val average_episode_duration: Int?,
|
@JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?,
|
||||||
@JsonProperty("synopsis") val synopsis: String?,
|
@JsonProperty("synopsis") val synopsis: String?,
|
||||||
@JsonProperty("mean") val mean: Double?,
|
@JsonProperty("mean") val mean: Double?,
|
||||||
@JsonProperty("genres") val genres: List<Genres>?,
|
@JsonProperty("genres") val genres: List<Genres>?,
|
||||||
@JsonProperty("rank") val rank: Int?,
|
@JsonProperty("rank") val rank: Int?,
|
||||||
@JsonProperty("popularity") val popularity: Int?,
|
@JsonProperty("popularity") val popularity: Int?,
|
||||||
@JsonProperty("num_list_users") val num_list_users: Int?,
|
@JsonProperty("num_list_users") val numListUsers: Int?,
|
||||||
@JsonProperty("num_favorites") val num_favorites: Int?,
|
@JsonProperty("num_favorites") val numFavorites: Int?,
|
||||||
@JsonProperty("num_scoring_users") val num_scoring_users: Int?,
|
@JsonProperty("num_scoring_users") val numScoringUsers: Int?,
|
||||||
@JsonProperty("start_season") val start_season: StartSeason?,
|
@JsonProperty("start_season") val startSeason: StartSeason?,
|
||||||
@JsonProperty("broadcast") val broadcast: Broadcast?,
|
@JsonProperty("broadcast") val broadcast: Broadcast?,
|
||||||
@JsonProperty("nsfw") val nsfw: String?,
|
@JsonProperty("nsfw") val nsfw: String?,
|
||||||
@JsonProperty("created_at") val created_at: String?,
|
@JsonProperty("created_at") val createdAt: String?,
|
||||||
@JsonProperty("updated_at") val updated_at: String?
|
@JsonProperty("updated_at") val updatedAt: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ListStatus(
|
data class ListStatus(
|
||||||
@JsonProperty("status") val status: String?,
|
@JsonProperty("status") val status: String?,
|
||||||
@JsonProperty("score") val score: Int,
|
@JsonProperty("score") val score: Int,
|
||||||
@JsonProperty("num_episodes_watched") val num_episodes_watched: Int,
|
@JsonProperty("num_episodes_watched") val numEpisodesWatched: Int,
|
||||||
@JsonProperty("is_rewatching") val is_rewatching: Boolean,
|
@JsonProperty("is_rewatching") val isRewatching: Boolean,
|
||||||
@JsonProperty("updated_at") val updated_at: String,
|
@JsonProperty("updated_at") val updatedAt: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Data(
|
data class Data(
|
||||||
@JsonProperty("node") val node: Node,
|
@JsonProperty("node") val node: Node,
|
||||||
@JsonProperty("list_status") val list_status: ListStatus?,
|
@JsonProperty("list_status") val listStatus: ListStatus?,
|
||||||
) {
|
) {
|
||||||
fun toLibraryItem(): SyncAPI.LibraryItem {
|
fun toLibraryItem(): SyncAPI.LibraryItem {
|
||||||
return SyncAPI.LibraryItem(
|
return SyncAPI.LibraryItem(
|
||||||
this.node.title,
|
this.node.title,
|
||||||
"https://myanimelist.net/anime/${this.node.id}/",
|
"https://myanimelist.net/anime/${this.node.id}/",
|
||||||
this.node.id.toString(),
|
this.node.id.toString(),
|
||||||
this.list_status?.num_episodes_watched,
|
this.listStatus?.numEpisodesWatched,
|
||||||
this.node.num_episodes,
|
this.node.numEpisodes,
|
||||||
this.list_status?.score?.times(10),
|
this.listStatus?.score?.times(10),
|
||||||
parseDateLong(this.list_status?.updated_at),
|
parseDateLong(this.listStatus?.updatedAt),
|
||||||
"MAL",
|
"MAL",
|
||||||
TvType.Anime,
|
TvType.Anime,
|
||||||
this.node.main_picture?.large ?: this.node.main_picture?.medium,
|
this.node.mainPicture?.large ?: this.node.mainPicture?.medium,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
plot = this.node.synopsis,
|
plot = this.node.synopsis,
|
||||||
|
releaseDate = if (this.node.startDate == null) null else try {Date.from(
|
||||||
|
Instant.from(
|
||||||
|
DateTimeFormatter.ofPattern(if (this.node.startDate.length == 4) "yyyy" else if (this.node.startDate.length == 7) "yyyy-MM" else "yyyy-MM-dd")
|
||||||
|
.parse(this.node.startDate)
|
||||||
|
)
|
||||||
|
)} catch (_: RuntimeException) {null}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -469,8 +480,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Broadcast(
|
data class Broadcast(
|
||||||
@JsonProperty("day_of_the_week") val day_of_the_week: String?,
|
@JsonProperty("day_of_the_week") val dayOfTheWeek: String?,
|
||||||
@JsonProperty("start_time") val start_time: String?
|
@JsonProperty("start_time") val startTime: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun getMalAnimeListCached(): Array<Data>? {
|
private fun getMalAnimeListCached(): Array<Data>? {
|
||||||
|
|
@ -490,14 +501,14 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
|
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
|
||||||
val list = getMalAnimeListSmart()?.groupBy {
|
val list = getMalAnimeListSmart()?.groupBy {
|
||||||
convertToStatus(it.list_status?.status ?: "").stringRes
|
convertToStatus(it.listStatus?.status ?: "").stringRes
|
||||||
}?.mapValues { group ->
|
}?.mapValues { group ->
|
||||||
group.value.map { it.toLibraryItem() }
|
group.value.map { it.toLibraryItem() }
|
||||||
} ?: emptyMap()
|
} ?: emptyMap()
|
||||||
|
|
||||||
// To fill empty lists when MAL does not return them
|
// To fill empty lists when MAL does not return them
|
||||||
val baseMap =
|
val baseMap =
|
||||||
MalStatusType.values().filter { it.value >= 0 }.associate {
|
MalStatusType.entries.filter { it.value >= 0 }.associate {
|
||||||
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
it.stringRes to emptyList<SyncAPI.LibraryItem>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -508,6 +519,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
ListSorting.AlphabeticalZ,
|
ListSorting.AlphabeticalZ,
|
||||||
ListSorting.UpdatedNew,
|
ListSorting.UpdatedNew,
|
||||||
ListSorting.UpdatedOld,
|
ListSorting.UpdatedOld,
|
||||||
|
ListSorting.ReleaseDateNew,
|
||||||
|
ListSorting.ReleaseDateOld,
|
||||||
ListSorting.RatingHigh,
|
ListSorting.RatingHigh,
|
||||||
ListSorting.RatingLow,
|
ListSorting.RatingLow,
|
||||||
)
|
)
|
||||||
|
|
@ -572,7 +585,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
).text
|
).text
|
||||||
val values = parseJson<MalRoot>(res)
|
val values = parseJson<MalRoot>(res)
|
||||||
val titles =
|
val titles =
|
||||||
values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) }
|
values.data.map { MalTitleHolder(it.listStatus, it.node.id, it.node.title) }
|
||||||
for (t in titles) {
|
for (t in titles) {
|
||||||
allTitles[t.id] = t
|
allTitles[t.id] = t
|
||||||
}
|
}
|
||||||
|
|
@ -581,11 +594,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? {
|
private fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? {
|
||||||
// No time remaining if the show has already ended
|
// No time remaining if the show has already ended
|
||||||
try {
|
try {
|
||||||
endDate?.let {
|
endDate?.let {
|
||||||
if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null
|
if (SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(it)
|
||||||
|
?.before(Date.from(Instant.now())) != false
|
||||||
|
) return@convertJapanTimeToTimeRemaining null
|
||||||
}
|
}
|
||||||
} catch (e: ParseException) {
|
} catch (e: ParseException) {
|
||||||
logError(e)
|
logError(e)
|
||||||
|
|
@ -602,7 +617,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH)
|
val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH)
|
||||||
val currentYear = currentDate.get(Calendar.YEAR)
|
val currentYear = currentDate.get(Calendar.YEAR)
|
||||||
|
|
||||||
val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm")
|
val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm", Locale.getDefault())
|
||||||
dateFormat.timeZone = TimeZone.getTimeZone("Japan")
|
dateFormat.timeZone = TimeZone.getTimeZone("Japan")
|
||||||
val parsedDate =
|
val parsedDate =
|
||||||
dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null
|
dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null
|
||||||
|
|
@ -646,13 +661,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
id: Int,
|
id: Int,
|
||||||
status: MalStatusType? = null,
|
status: MalStatusType? = null,
|
||||||
score: Int? = null,
|
score: Int? = null,
|
||||||
num_watched_episodes: Int? = null,
|
numWatchedEpisodes: Int? = null,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val res = setScoreRequest(
|
val res = setScoreRequest(
|
||||||
id,
|
id,
|
||||||
if (status == null) null else malStatusAsString[maxOf(0, status.value)],
|
if (status == null) null else malStatusAsString[maxOf(0, status.value)],
|
||||||
score,
|
score,
|
||||||
num_watched_episodes
|
numWatchedEpisodes
|
||||||
)
|
)
|
||||||
|
|
||||||
return if (res.isNullOrBlank()) {
|
return if (res.isNullOrBlank()) {
|
||||||
|
|
@ -669,17 +684,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
private suspend fun setScoreRequest(
|
private suspend fun setScoreRequest(
|
||||||
id: Int,
|
id: Int,
|
||||||
status: String? = null,
|
status: String? = null,
|
||||||
score: Int? = null,
|
score: Int? = null,
|
||||||
num_watched_episodes: Int? = null,
|
numWatchedEpisodes: Int? = null,
|
||||||
): String? {
|
): String? {
|
||||||
val data = mapOf(
|
val data = mapOf(
|
||||||
"status" to status,
|
"status" to status,
|
||||||
"score" to score?.toString(),
|
"score" to score?.toString(),
|
||||||
"num_watched_episodes" to num_watched_episodes?.toString()
|
"num_watched_episodes" to numWatchedEpisodes?.toString()
|
||||||
).filter { it.value != null } as Map<String, String>
|
).filterValues { it != null } as Map<String, String>
|
||||||
|
|
||||||
return app.put(
|
return app.put(
|
||||||
"$apiUrl/v2/anime/$id/my_list_status",
|
"$apiUrl/v2/anime/$id/my_list_status",
|
||||||
|
|
@ -692,10 +708,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
|
|
||||||
data class ResponseToken(
|
data class ResponseToken(
|
||||||
@JsonProperty("token_type") val token_type: String,
|
@JsonProperty("token_type") val tokenType: String,
|
||||||
@JsonProperty("expires_in") val expires_in: Int,
|
@JsonProperty("expires_in") val expiresIn: Int,
|
||||||
@JsonProperty("access_token") val access_token: String,
|
@JsonProperty("access_token") val accessToken: String,
|
||||||
@JsonProperty("refresh_token") val refresh_token: String,
|
@JsonProperty("refresh_token") val refreshToken: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MalRoot(
|
data class MalRoot(
|
||||||
|
|
@ -704,7 +720,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
data class MalDatum(
|
data class MalDatum(
|
||||||
@JsonProperty("node") val node: MalNode,
|
@JsonProperty("node") val node: MalNode,
|
||||||
@JsonProperty("list_status") val list_status: MalStatus,
|
@JsonProperty("list_status") val listStatus: MalStatus,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MalNode(
|
data class MalNode(
|
||||||
|
|
@ -721,16 +737,16 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
data class MalStatus(
|
data class MalStatus(
|
||||||
@JsonProperty("status") val status: String,
|
@JsonProperty("status") val status: String,
|
||||||
@JsonProperty("score") val score: Int,
|
@JsonProperty("score") val score: Int,
|
||||||
@JsonProperty("num_episodes_watched") val num_episodes_watched: Int,
|
@JsonProperty("num_episodes_watched") val numEpisodesWatched: Int,
|
||||||
@JsonProperty("is_rewatching") val is_rewatching: Boolean,
|
@JsonProperty("is_rewatching") val isRewatching: Boolean,
|
||||||
@JsonProperty("updated_at") val updated_at: String,
|
@JsonProperty("updated_at") val updatedAt: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MalUser(
|
data class MalUser(
|
||||||
@JsonProperty("id") val id: Int,
|
@JsonProperty("id") val id: Int,
|
||||||
@JsonProperty("name") val name: String,
|
@JsonProperty("name") val name: String,
|
||||||
@JsonProperty("location") val location: String,
|
@JsonProperty("location") val location: String,
|
||||||
@JsonProperty("joined_at") val joined_at: String,
|
@JsonProperty("joined_at") val joinedAt: String,
|
||||||
@JsonProperty("picture") val picture: String?,
|
@JsonProperty("picture") val picture: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -743,9 +759,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
data class SmallMalAnime(
|
data class SmallMalAnime(
|
||||||
@JsonProperty("id") val id: Int,
|
@JsonProperty("id") val id: Int,
|
||||||
@JsonProperty("title") val title: String?,
|
@JsonProperty("title") val title: String?,
|
||||||
@JsonProperty("num_episodes") val num_episodes: Int,
|
@JsonProperty("num_episodes") val numEpisodes: Int,
|
||||||
@JsonProperty("my_list_status") val my_list_status: MalStatus?,
|
@JsonProperty("my_list_status") val myListStatus: MalStatus?,
|
||||||
@JsonProperty("main_picture") val main_picture: MalMainPicture?,
|
@JsonProperty("main_picture") val mainPicture: MalMainPicture?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MalSearchNode(
|
data class MalSearchNode(
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,10 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile
|
const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile
|
||||||
const val apiKey = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2"
|
const val API_KEY = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2"
|
||||||
const val host = "https://api.opensubtitles.com/api/v1"
|
const val HOST = "https://api.opensubtitles.com/api/v1"
|
||||||
const val TAG = "OPENSUBS"
|
const val TAG = "OPENSUBS"
|
||||||
const val coolDownDuration: Long = 1000L * 30L // CoolDown if 429 error code in ms
|
const val COOLDOWN_DURATION: Long = 1000L * 30L // CoolDown if 429 error code in ms
|
||||||
var currentCoolDown: Long = 0L
|
var currentCoolDown: Long = 0L
|
||||||
var currentSession: SubtitleOAuthEntity? = null
|
var currentSession: SubtitleOAuthEntity? = null
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +48,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
chain.request().newBuilder()
|
chain.request().newBuilder()
|
||||||
.removeHeader("user-agent")
|
.removeHeader("user-agent")
|
||||||
.addHeader("user-agent", userAgent)
|
.addHeader("user-agent", userAgent)
|
||||||
.addHeader("Api-Key", apiKey)
|
.addHeader("Api-Key", API_KEY)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -65,7 +65,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun throwGotTooManyRequests() {
|
private fun throwGotTooManyRequests() {
|
||||||
currentCoolDown = unixTimeMs + coolDownDuration
|
currentCoolDown = unixTimeMs + COOLDOWN_DURATION
|
||||||
throw ErrorLoadingException("Too many requests")
|
throw ErrorLoadingException("Too many requests")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,7 +114,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
private suspend fun initLogin(username: String, password: String): Boolean {
|
private suspend fun initLogin(username: String, password: String): Boolean {
|
||||||
//Log.i(TAG, "DATA = [$username] [$password]")
|
//Log.i(TAG, "DATA = [$username] [$password]")
|
||||||
val response = app.post(
|
val response = app.post(
|
||||||
url = "$host/login",
|
url = "$HOST/login",
|
||||||
headers = mapOf(
|
headers = mapOf(
|
||||||
"Content-Type" to "application/json",
|
"Content-Type" to "application/json",
|
||||||
),
|
),
|
||||||
|
|
@ -133,7 +133,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
SubtitleOAuthEntity(
|
SubtitleOAuthEntity(
|
||||||
user = username,
|
user = username,
|
||||||
pass = password,
|
pass = password,
|
||||||
access_token = token.token ?: run {
|
accessToken = token.token ?: run {
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
@ -185,7 +185,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
throwIfCantDoRequest()
|
throwIfCantDoRequest()
|
||||||
val fixedLang = fixLanguage(query.lang)
|
val fixedLang = fixLanguage(query.lang)
|
||||||
|
|
||||||
val imdbId = query.imdb ?: 0
|
val imdbId = query.imdbId?.replace("tt", "")?.toInt() ?: 0
|
||||||
val queryText = query.query
|
val queryText = query.query
|
||||||
val epNum = query.epNumber ?: 0
|
val epNum = query.epNumber ?: 0
|
||||||
val seasonNum = query.seasonNumber ?: 0
|
val seasonNum = query.seasonNumber ?: 0
|
||||||
|
|
@ -196,8 +196,8 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
|
|
||||||
val searchQueryUrl = when (imdbId > 0) {
|
val searchQueryUrl = when (imdbId > 0) {
|
||||||
//Use imdb_id to search if its valid
|
//Use imdb_id to search if its valid
|
||||||
true -> "$host/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
|
true -> "$HOST/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
|
||||||
false -> "$host/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
|
false -> "$HOST/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
|
||||||
}
|
}
|
||||||
|
|
||||||
val req = app.get(
|
val req = app.get(
|
||||||
|
|
@ -232,7 +232,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber
|
val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber
|
||||||
val year = featureDetails?.year ?: query.year
|
val year = featureDetails?.year ?: query.year
|
||||||
val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie
|
val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie
|
||||||
val isHearingImpaired = attr.hearing_impaired ?: false
|
val isHearingImpaired = attr.hearingImpaired ?: false
|
||||||
//Log.i(TAG, "Result id/name => ${item.id} / $name")
|
//Log.i(TAG, "Result id/name => ${item.id} / $name")
|
||||||
item.attributes?.files?.forEach { file ->
|
item.attributes?.files?.forEach { file ->
|
||||||
val resultData = file.fileId?.toString() ?: ""
|
val resultData = file.fileId?.toString() ?: ""
|
||||||
|
|
@ -265,11 +265,11 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
throwIfCantDoRequest()
|
throwIfCantDoRequest()
|
||||||
|
|
||||||
val req = app.post(
|
val req = app.post(
|
||||||
url = "$host/download",
|
url = "$HOST/download",
|
||||||
headers = mapOf(
|
headers = mapOf(
|
||||||
Pair(
|
Pair(
|
||||||
"Authorization",
|
"Authorization",
|
||||||
"Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}"
|
"Bearer ${currentSession?.accessToken ?: throw ErrorLoadingException("No access token active in current session")}"
|
||||||
),
|
),
|
||||||
Pair("Content-Type", "application/json"),
|
Pair("Content-Type", "application/json"),
|
||||||
Pair("Accept", "*/*")
|
Pair("Accept", "*/*")
|
||||||
|
|
@ -298,7 +298,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
data class SubtitleOAuthEntity(
|
data class SubtitleOAuthEntity(
|
||||||
var user: String,
|
var user: String,
|
||||||
var pass: String,
|
var pass: String,
|
||||||
var access_token: String,
|
var accessToken: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class OAuthToken(
|
data class OAuthToken(
|
||||||
|
|
@ -323,7 +323,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
||||||
@JsonProperty("url") var url: String? = null,
|
@JsonProperty("url") var url: String? = null,
|
||||||
@JsonProperty("files") var files: List<ResultFiles>? = listOf(),
|
@JsonProperty("files") var files: List<ResultFiles>? = listOf(),
|
||||||
@JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(),
|
@JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(),
|
||||||
@JsonProperty("hearing_impaired") var hearing_impaired: Boolean? = null,
|
@JsonProperty("hearing_impaired") var hearingImpaired: Boolean? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ResultFiles(
|
data class ResultFiles(
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.BuildConfig
|
import com.lagradost.cloudstream3.BuildConfig
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.readIdFromString
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.SimklSyncServices
|
||||||
import com.lagradost.cloudstream3.TvType
|
import com.lagradost.cloudstream3.TvType
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mapper
|
import com.lagradost.cloudstream3.mapper
|
||||||
|
|
@ -22,13 +24,14 @@ import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||||
import com.lagradost.cloudstream3.ui.SyncWatchType
|
import com.lagradost.cloudstream3.ui.SyncWatchType
|
||||||
import com.lagradost.cloudstream3.ui.library.ListSorting
|
import com.lagradost.cloudstream3.ui.library.ListSorting
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
|
|
@ -36,6 +39,7 @@ import java.security.SecureRandom
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
|
|
@ -45,6 +49,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
override var name = "Simkl"
|
override var name = "Simkl"
|
||||||
override val key = "simkl-key"
|
override val key = "simkl-key"
|
||||||
override val redirectUrl = "simkl"
|
override val redirectUrl = "simkl"
|
||||||
|
override val supportDeviceAuth = true
|
||||||
override val idPrefix = "simkl"
|
override val idPrefix = "simkl"
|
||||||
override var requireLibraryRefresh = true
|
override var requireLibraryRefresh = true
|
||||||
override var mainUrl = "https://api.simkl.com"
|
override var mainUrl = "https://api.simkl.com"
|
||||||
|
|
@ -141,8 +146,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID
|
private const val CLIENT_ID: String = BuildConfig.SIMKL_CLIENT_ID
|
||||||
private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET
|
private const val CLIENT_SECRET: String = BuildConfig.SIMKL_CLIENT_SECRET
|
||||||
private var lastLoginState = ""
|
private var lastLoginState = ""
|
||||||
|
|
||||||
const val SIMKL_TOKEN_KEY: String = "simkl_token"
|
const val SIMKL_TOKEN_KEY: String = "simkl_token"
|
||||||
|
|
@ -151,10 +156,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time"
|
const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time"
|
||||||
|
|
||||||
/** 2014-09-01T09:10:11Z -> 1409562611 */
|
/** 2014-09-01T09:10:11Z -> 1409562611 */
|
||||||
private const val simklDateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
private const val SIMKL_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
||||||
fun getUnixTime(string: String?): Long? {
|
fun getUnixTime(string: String?): Long? {
|
||||||
return try {
|
return try {
|
||||||
SimpleDateFormat(simklDateFormat).apply {
|
SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply {
|
||||||
this.timeZone = TimeZone.getTimeZone("UTC")
|
this.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
}.parse(
|
}.parse(
|
||||||
string ?: return null
|
string ?: return null
|
||||||
|
|
@ -168,7 +173,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
/** 1409562611 -> 2014-09-01T09:10:11Z */
|
/** 1409562611 -> 2014-09-01T09:10:11Z */
|
||||||
fun getDateTime(unixTime: Long?): String? {
|
fun getDateTime(unixTime: Long?): String? {
|
||||||
return try {
|
return try {
|
||||||
SimpleDateFormat(simklDateFormat).apply {
|
SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply {
|
||||||
this.timeZone = TimeZone.getTimeZone("UTC")
|
this.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
}.format(
|
}.format(
|
||||||
Date.from(
|
Date.from(
|
||||||
|
|
@ -182,32 +187,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set of sync services simkl is compatible with.
|
|
||||||
* Add more as required: https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id
|
|
||||||
*/
|
|
||||||
enum class SyncServices(val originalName: String) {
|
|
||||||
Simkl("simkl"),
|
|
||||||
Imdb("imdb"),
|
|
||||||
Tmdb("tmdb"),
|
|
||||||
AniList("anilist"),
|
|
||||||
Mal("mal"),
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ID string is a way to keep a collection of services in one single ID using a map
|
|
||||||
* This adds a database service (like imdb) to the string and returns the new string.
|
|
||||||
*/
|
|
||||||
fun addIdToString(idString: String?, database: SyncServices, id: String?): String? {
|
|
||||||
if (id == null) return idString
|
|
||||||
return (readIdFromString(idString) + mapOf(database to id)).toJson()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Read the id string to get all other ids */
|
|
||||||
fun readIdFromString(idString: String?): Map<SyncServices, String> {
|
|
||||||
return tryParseJson(idString) ?: return emptyMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPosterUrl(poster: String): String {
|
fun getPosterUrl(poster: String): String {
|
||||||
return "https://wsrv.nl/?url=https://simkl.in/posters/${poster}_m.webp"
|
return "https://wsrv.nl/?url=https://simkl.in/posters/${poster}_m.webp"
|
||||||
}
|
}
|
||||||
|
|
@ -231,7 +210,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromString(string: String): SimklListStatusType? {
|
fun fromString(string: String): SimklListStatusType? {
|
||||||
return SimklListStatusType.values().firstOrNull {
|
return SimklListStatusType.entries.firstOrNull {
|
||||||
it.originalName == string
|
it.originalName == string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -242,17 +221,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||||
data class TokenRequest(
|
data class TokenRequest(
|
||||||
@JsonProperty("code") val code: String,
|
@JsonProperty("code") val code: String,
|
||||||
@JsonProperty("client_id") val client_id: String = clientId,
|
@JsonProperty("client_id") val clientId: String = CLIENT_ID,
|
||||||
@JsonProperty("client_secret") val client_secret: String = clientSecret,
|
@JsonProperty("client_secret") val clientSecret: String = CLIENT_SECRET,
|
||||||
@JsonProperty("redirect_uri") val redirect_uri: String = "$appString://simkl",
|
@JsonProperty("redirect_uri") val redirectUri: String = "$APP_STRING://simkl",
|
||||||
@JsonProperty("grant_type") val grant_type: String = "authorization_code"
|
@JsonProperty("grant_type") val grantType: String = "authorization_code"
|
||||||
)
|
)
|
||||||
|
|
||||||
data class TokenResponse(
|
data class TokenResponse(
|
||||||
/** No expiration date */
|
/** No expiration date */
|
||||||
val access_token: String,
|
@JsonProperty("access_token") val accessToken: String,
|
||||||
val token_type: String,
|
@JsonProperty("token_type") val tokenType: String,
|
||||||
val scope: String
|
@JsonProperty("scope") val scope: String
|
||||||
)
|
)
|
||||||
// -------------------
|
// -------------------
|
||||||
|
|
||||||
|
|
@ -267,17 +246,32 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class PinAuthResponse(
|
||||||
|
@JsonProperty("result") val result: String,
|
||||||
|
@JsonProperty("device_code") val deviceCode: String,
|
||||||
|
@JsonProperty("user_code") val userCode: String,
|
||||||
|
@JsonProperty("verification_url") val verificationUrl: String,
|
||||||
|
@JsonProperty("expires_in") val expiresIn: Int,
|
||||||
|
@JsonProperty("interval") val interval: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PinExchangeResponse(
|
||||||
|
@JsonProperty("result") val result: String,
|
||||||
|
@JsonProperty("message") val message: String? = null,
|
||||||
|
@JsonProperty("access_token") val accessToken: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
// -------------------
|
// -------------------
|
||||||
data class ActivitiesResponse(
|
data class ActivitiesResponse(
|
||||||
val all: String?,
|
@JsonProperty("all") val all: String?,
|
||||||
val tv_shows: UpdatedAt,
|
@JsonProperty("tv_shows") val tvShows: UpdatedAt,
|
||||||
val anime: UpdatedAt,
|
@JsonProperty("anime") val anime: UpdatedAt,
|
||||||
val movies: UpdatedAt,
|
@JsonProperty("movies") val movies: UpdatedAt,
|
||||||
) {
|
) {
|
||||||
data class UpdatedAt(
|
data class UpdatedAt(
|
||||||
val all: String?,
|
@JsonProperty("all") val all: String?,
|
||||||
val removed_from_list: String?,
|
@JsonProperty("removed_from_list") val removedFromList: String?,
|
||||||
val rated_at: String?,
|
@JsonProperty("rated_at") val ratedAt: String?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,7 +310,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonProperty("title") val title: String?,
|
@JsonProperty("title") val title: String?,
|
||||||
@JsonProperty("year") val year: Int?,
|
@JsonProperty("year") val year: Int?,
|
||||||
@JsonProperty("ids") val ids: Ids?,
|
@JsonProperty("ids") val ids: Ids?,
|
||||||
@JsonProperty("total_episodes") val total_episodes: Int? = null,
|
@JsonProperty("total_episodes") val totalEpisodes: Int? = null,
|
||||||
@JsonProperty("status") val status: String? = null,
|
@JsonProperty("status") val status: String? = null,
|
||||||
@JsonProperty("poster") val poster: String? = null,
|
@JsonProperty("poster") val poster: String? = null,
|
||||||
@JsonProperty("type") val type: String? = null,
|
@JsonProperty("type") val type: String? = null,
|
||||||
|
|
@ -344,13 +338,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonProperty("anilist") val anilist: String? = null,
|
@JsonProperty("anilist") val anilist: String? = null,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun fromMap(map: Map<SyncServices, String>): Ids {
|
fun fromMap(map: Map<SimklSyncServices, String>): Ids {
|
||||||
return Ids(
|
return Ids(
|
||||||
simkl = map[SyncServices.Simkl]?.toIntOrNull(),
|
simkl = map[SimklSyncServices.Simkl]?.toIntOrNull(),
|
||||||
imdb = map[SyncServices.Imdb],
|
imdb = map[SimklSyncServices.Imdb],
|
||||||
tmdb = map[SyncServices.Tmdb],
|
tmdb = map[SimklSyncServices.Tmdb],
|
||||||
mal = map[SyncServices.Mal],
|
mal = map[SimklSyncServices.Mal],
|
||||||
anilist = map[SyncServices.AniList]
|
anilist = map[SimklSyncServices.AniList]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -548,7 +542,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint { "Requesting episodes from $url" }
|
debugPrint { "Requesting episodes from $url" }
|
||||||
return app.get(url, params = mapOf("client_id" to clientId))
|
return app.get(url, params = mapOf("client_id" to CLIENT_ID))
|
||||||
.parsedSafe<Array<EpisodeMetadata>>()?.also {
|
.parsedSafe<Array<EpisodeMetadata>>()?.also {
|
||||||
val cacheTime =
|
val cacheTime =
|
||||||
if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value
|
if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value
|
||||||
|
|
@ -566,7 +560,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonProperty("seasons") seasons: List<Season>? = null,
|
@JsonProperty("seasons") seasons: List<Season>? = null,
|
||||||
@JsonProperty("episodes") episodes: List<Season.Episode>? = null,
|
@JsonProperty("episodes") episodes: List<Season.Episode>? = null,
|
||||||
@JsonProperty("rating") val rating: Int? = null,
|
@JsonProperty("rating") val rating: Int? = null,
|
||||||
@JsonProperty("rated_at") val rated_at: String? = null,
|
@JsonProperty("rated_at") val ratedAt: String? = null,
|
||||||
) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes)
|
) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes)
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||||
|
|
@ -575,7 +569,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonProperty("year") year: Int?,
|
@JsonProperty("year") year: Int?,
|
||||||
@JsonProperty("ids") ids: Ids?,
|
@JsonProperty("ids") ids: Ids?,
|
||||||
@JsonProperty("rating") val rating: Int,
|
@JsonProperty("rating") val rating: Int,
|
||||||
@JsonProperty("rated_at") val rated_at: String? = getDateTime(unixTime)
|
@JsonProperty("rated_at") val ratedAt: String? = getDateTime(unixTime)
|
||||||
) : MediaObject(title, year, ids)
|
) : MediaObject(title, year, ids)
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||||
|
|
@ -584,7 +578,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonProperty("year") year: Int?,
|
@JsonProperty("year") year: Int?,
|
||||||
@JsonProperty("ids") ids: Ids?,
|
@JsonProperty("ids") ids: Ids?,
|
||||||
@JsonProperty("to") val to: String,
|
@JsonProperty("to") val to: String,
|
||||||
@JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime)
|
@JsonProperty("watched_at") val watchedAt: String? = getDateTime(unixTime)
|
||||||
) : MediaObject(title, year, ids)
|
) : MediaObject(title, year, ids)
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||||
|
|
@ -639,24 +633,24 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Metadata {
|
interface Metadata {
|
||||||
val last_watched_at: String?
|
val lastWatchedAt: String?
|
||||||
val status: String?
|
val status: String?
|
||||||
val user_rating: Int?
|
val userRating: Int?
|
||||||
val last_watched: String?
|
val lastWatched: String?
|
||||||
val watched_episodes_count: Int?
|
val watchedEpisodesCount: Int?
|
||||||
val total_episodes_count: Int?
|
val totalEpisodesCount: Int?
|
||||||
|
|
||||||
fun getIds(): ShowMetadata.Show.Ids
|
fun getIds(): ShowMetadata.Show.Ids
|
||||||
fun toLibraryItem(): SyncAPI.LibraryItem
|
fun toLibraryItem(): SyncAPI.LibraryItem
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MovieMetadata(
|
data class MovieMetadata(
|
||||||
override val last_watched_at: String?,
|
@JsonProperty("last_watched_at") override val lastWatchedAt: String?,
|
||||||
override val status: String,
|
@JsonProperty("status") override val status: String,
|
||||||
override val user_rating: Int?,
|
@JsonProperty("user_rating") override val userRating: Int?,
|
||||||
override val last_watched: String?,
|
@JsonProperty("last_watched") override val lastWatched: String?,
|
||||||
override val watched_episodes_count: Int?,
|
@JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?,
|
||||||
override val total_episodes_count: Int?,
|
@JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?,
|
||||||
val movie: ShowMetadata.Show
|
val movie: ShowMetadata.Show
|
||||||
) : Metadata {
|
) : Metadata {
|
||||||
override fun getIds(): ShowMetadata.Show.Ids {
|
override fun getIds(): ShowMetadata.Show.Ids {
|
||||||
|
|
@ -668,27 +662,28 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
this.movie.title,
|
this.movie.title,
|
||||||
"https://simkl.com/tv/${movie.ids.simkl}",
|
"https://simkl.com/tv/${movie.ids.simkl}",
|
||||||
movie.ids.simkl.toString(),
|
movie.ids.simkl.toString(),
|
||||||
this.watched_episodes_count,
|
this.watchedEpisodesCount,
|
||||||
this.total_episodes_count,
|
this.totalEpisodesCount,
|
||||||
this.user_rating?.times(10),
|
this.userRating?.times(10),
|
||||||
getUnixTime(last_watched_at) ?: 0,
|
getUnixTime(lastWatchedAt) ?: 0,
|
||||||
"Simkl",
|
"Simkl",
|
||||||
TvType.Movie,
|
TvType.Movie,
|
||||||
this.movie.poster?.let { getPosterUrl(it) },
|
this.movie.poster?.let { getPosterUrl(it) },
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
movie.ids.simkl,
|
this.movie.year?.toYear(),
|
||||||
|
movie.ids.simkl
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ShowMetadata(
|
data class ShowMetadata(
|
||||||
@JsonProperty("last_watched_at") override val last_watched_at: String?,
|
@JsonProperty("last_watched_at") override val lastWatchedAt: String?,
|
||||||
@JsonProperty("status") override val status: String,
|
@JsonProperty("status") override val status: String,
|
||||||
@JsonProperty("user_rating") override val user_rating: Int?,
|
@JsonProperty("user_rating") override val userRating: Int?,
|
||||||
@JsonProperty("last_watched") override val last_watched: String?,
|
@JsonProperty("last_watched") override val lastWatched: String?,
|
||||||
@JsonProperty("watched_episodes_count") override val watched_episodes_count: Int?,
|
@JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?,
|
||||||
@JsonProperty("total_episodes_count") override val total_episodes_count: Int?,
|
@JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?,
|
||||||
@JsonProperty("show") val show: Show
|
@JsonProperty("show") val show: Show
|
||||||
) : Metadata {
|
) : Metadata {
|
||||||
override fun getIds(): Show.Ids {
|
override fun getIds(): Show.Ids {
|
||||||
|
|
@ -700,15 +695,16 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
this.show.title,
|
this.show.title,
|
||||||
"https://simkl.com/tv/${show.ids.simkl}",
|
"https://simkl.com/tv/${show.ids.simkl}",
|
||||||
show.ids.simkl.toString(),
|
show.ids.simkl.toString(),
|
||||||
this.watched_episodes_count,
|
this.watchedEpisodesCount,
|
||||||
this.total_episodes_count,
|
this.totalEpisodesCount,
|
||||||
this.user_rating?.times(10),
|
this.userRating?.times(10),
|
||||||
getUnixTime(last_watched_at) ?: 0,
|
getUnixTime(lastWatchedAt) ?: 0,
|
||||||
"Simkl",
|
"Simkl",
|
||||||
TvType.Anime,
|
TvType.Anime,
|
||||||
this.show.poster?.let { getPosterUrl(it) },
|
this.show.poster?.let { getPosterUrl(it) },
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
this.show.year?.toYear(),
|
||||||
show.ids.simkl
|
show.ids.simkl
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -732,13 +728,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonProperty("anilist") val anilist: String?,
|
@JsonProperty("anilist") val anilist: String?,
|
||||||
@JsonProperty("traktslug") val traktslug: String?
|
@JsonProperty("traktslug") val traktslug: String?
|
||||||
) {
|
) {
|
||||||
fun matchesId(database: SyncServices, id: String): Boolean {
|
fun matchesId(database: SimklSyncServices, id: String): Boolean {
|
||||||
return when (database) {
|
return when (database) {
|
||||||
SyncServices.Simkl -> this.simkl == id.toIntOrNull()
|
SimklSyncServices.Simkl -> this.simkl == id.toIntOrNull()
|
||||||
SyncServices.AniList -> this.anilist == id
|
SimklSyncServices.AniList -> this.anilist == id
|
||||||
SyncServices.Mal -> this.mal == id
|
SimklSyncServices.Mal -> this.mal == id
|
||||||
SyncServices.Tmdb -> this.tmdb == id
|
SimklSyncServices.Tmdb -> this.tmdb == id
|
||||||
SyncServices.Imdb -> this.imdb == id
|
SimklSyncServices.Imdb -> this.imdb == id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -757,7 +753,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
chain.request()
|
chain.request()
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.addHeader("Authorization", "Bearer $token")
|
.addHeader("Authorization", "Bearer $token")
|
||||||
.addHeader("simkl-api-key", clientId)
|
.addHeader("simkl-api-key", CLIENT_ID)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -818,7 +814,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val episodeConstructor = SimklEpisodeConstructor(
|
val episodeConstructor = SimklEpisodeConstructor(
|
||||||
searchResult.ids?.simkl,
|
searchResult.ids?.simkl,
|
||||||
searchResult.type,
|
searchResult.type,
|
||||||
searchResult.total_episodes,
|
searchResult.totalEpisodes,
|
||||||
searchResult.hasEnded()
|
searchResult.hasEnded()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -840,12 +836,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
?: return null,
|
?: return null,
|
||||||
score = foundItem.user_rating,
|
score = foundItem.userRating,
|
||||||
watchedEpisodes = foundItem.watched_episodes_count,
|
watchedEpisodes = foundItem.watchedEpisodesCount,
|
||||||
maxEpisodes = searchResult.total_episodes,
|
maxEpisodes = searchResult.totalEpisodes,
|
||||||
episodeConstructor = episodeConstructor,
|
episodeConstructor = episodeConstructor,
|
||||||
oldEpisodes = foundItem.watched_episodes_count ?: 0,
|
oldEpisodes = foundItem.watchedEpisodesCount ?: 0,
|
||||||
oldScore = foundItem.user_rating,
|
oldScore = foundItem.userRating,
|
||||||
oldStatus = foundItem.status
|
oldStatus = foundItem.status
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -853,7 +849,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
status = SyncWatchType.fromInternalId(SimklListStatusType.None.value),
|
status = SyncWatchType.fromInternalId(SimklListStatusType.None.value),
|
||||||
score = 0,
|
score = 0,
|
||||||
watchedEpisodes = 0,
|
watchedEpisodes = 0,
|
||||||
maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes,
|
maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.totalEpisodes,
|
||||||
episodeConstructor = episodeConstructor,
|
episodeConstructor = episodeConstructor,
|
||||||
oldEpisodes = 0,
|
oldEpisodes = 0,
|
||||||
oldStatus = null,
|
oldStatus = null,
|
||||||
|
|
@ -899,12 +895,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
|
|
||||||
/** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */
|
/** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */
|
||||||
suspend fun searchByIds(serviceMap: Map<SyncServices, String>): Array<MediaObject>? {
|
private suspend fun searchByIds(serviceMap: Map<SimklSyncServices, String>): Array<MediaObject>? {
|
||||||
if (serviceMap.isEmpty()) return emptyArray()
|
if (serviceMap.isEmpty()) return emptyArray()
|
||||||
|
|
||||||
return app.get(
|
return app.get(
|
||||||
"$mainUrl/search/id",
|
"$mainUrl/search/id",
|
||||||
params = mapOf("client_id" to clientId) + serviceMap.map { (service, id) ->
|
params = mapOf("client_id" to CLIENT_ID) + serviceMap.map { (service, id) ->
|
||||||
service.originalName to id
|
service.originalName to id
|
||||||
}
|
}
|
||||||
).parsedSafe()
|
).parsedSafe()
|
||||||
|
|
@ -912,14 +908,14 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
|
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
|
||||||
return app.get(
|
return app.get(
|
||||||
"$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name)
|
"$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to name)
|
||||||
).parsedSafe<Array<MediaObject>>()?.mapNotNull { it.toSyncSearchResult() }
|
).parsedSafe<Array<MediaObject>>()?.mapNotNull { it.toSyncSearchResult() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun authenticate(activity: FragmentActivity?) {
|
override fun authenticate(activity: FragmentActivity?) {
|
||||||
lastLoginState = BigInteger(130, SecureRandom()).toString(32)
|
lastLoginState = BigInteger(130, SecureRandom()).toString(32)
|
||||||
val url =
|
val url =
|
||||||
"https://simkl.com/oauth/authorize?response_type=code&client_id=$clientId&redirect_uri=$appString://${redirectUrl}&state=$lastLoginState"
|
"https://simkl.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrl}&state=$lastLoginState"
|
||||||
openBrowser(url, activity)
|
openBrowser(url, activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -969,15 +965,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val activities = getActivities()
|
val activities = getActivities()
|
||||||
val lastCacheUpdate = getKey<Long>(accountId, SIMKL_CACHED_LIST_TIME)
|
val lastCacheUpdate = getKey<Long>(accountId, SIMKL_CACHED_LIST_TIME)
|
||||||
val lastRemoval = listOf(
|
val lastRemoval = listOf(
|
||||||
activities?.tv_shows?.removed_from_list,
|
activities?.tvShows?.removedFromList,
|
||||||
activities?.anime?.removed_from_list,
|
activities?.anime?.removedFromList,
|
||||||
activities?.movies?.removed_from_list
|
activities?.movies?.removedFromList
|
||||||
).maxOf {
|
).maxOf {
|
||||||
getUnixTime(it) ?: -1
|
getUnixTime(it) ?: -1
|
||||||
}
|
}
|
||||||
val lastRealUpdate =
|
val lastRealUpdate =
|
||||||
listOf(
|
listOf(
|
||||||
activities?.tv_shows?.all,
|
activities?.tvShows?.all,
|
||||||
activities?.anime?.all,
|
activities?.anime?.all,
|
||||||
activities?.movies?.all,
|
activities?.movies?.all,
|
||||||
).maxOf {
|
).maxOf {
|
||||||
|
|
@ -1034,6 +1030,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
ListSorting.AlphabeticalZ,
|
ListSorting.AlphabeticalZ,
|
||||||
ListSorting.UpdatedNew,
|
ListSorting.UpdatedNew,
|
||||||
ListSorting.UpdatedOld,
|
ListSorting.UpdatedOld,
|
||||||
|
ListSorting.ReleaseDateNew,
|
||||||
|
ListSorting.ReleaseDateOld,
|
||||||
ListSorting.RatingHigh,
|
ListSorting.RatingHigh,
|
||||||
ListSorting.RatingLow,
|
ListSorting.RatingLow,
|
||||||
)
|
)
|
||||||
|
|
@ -1045,6 +1043,44 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
return simklUrlRegex.find(url)?.groupValues?.get(1) ?: ""
|
return simklUrlRegex.find(url)?.groupValues?.get(1) ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getDevicePin(): OAuth2API.PinAuthData? {
|
||||||
|
val pinAuthResp = app.get(
|
||||||
|
"$mainUrl/oauth/pin?client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrl}"
|
||||||
|
).parsedSafe<PinAuthResponse>() ?: return null
|
||||||
|
|
||||||
|
return OAuth2API.PinAuthData(
|
||||||
|
deviceCode = pinAuthResp.deviceCode,
|
||||||
|
userCode = pinAuthResp.userCode,
|
||||||
|
verificationUrl = pinAuthResp.verificationUrl,
|
||||||
|
expiresIn = pinAuthResp.expiresIn,
|
||||||
|
interval = pinAuthResp.interval
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun handleDeviceAuth(pinAuthData: OAuth2API.PinAuthData): Boolean {
|
||||||
|
val pinAuthResp = app.get(
|
||||||
|
"$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$CLIENT_ID"
|
||||||
|
).parsedSafe<PinExchangeResponse>() ?: return false
|
||||||
|
|
||||||
|
if (pinAuthResp.accessToken != null) {
|
||||||
|
switchToNewAccount()
|
||||||
|
setKey(accountId, SIMKL_TOKEN_KEY, pinAuthResp.accessToken)
|
||||||
|
|
||||||
|
val user = getUser()
|
||||||
|
if (user == null) {
|
||||||
|
removeKey(accountId, SIMKL_TOKEN_KEY)
|
||||||
|
switchToOldAccount()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
setKey(accountId, SIMKL_USER_KEY, user)
|
||||||
|
registerAccount()
|
||||||
|
requireLibraryRefresh = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun handleRedirect(url: String): Boolean {
|
override suspend fun handleRedirect(url: String): Boolean {
|
||||||
val uri = url.toUri()
|
val uri = url.toUri()
|
||||||
val state = uri.getQueryParameter("state")
|
val state = uri.getQueryParameter("state")
|
||||||
|
|
@ -1058,7 +1094,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
).parsedSafe<TokenResponse>() ?: return false
|
).parsedSafe<TokenResponse>() ?: return false
|
||||||
|
|
||||||
switchToNewAccount()
|
switchToNewAccount()
|
||||||
setKey(accountId, SIMKL_TOKEN_KEY, token.access_token)
|
setKey(accountId, SIMKL_TOKEN_KEY, token.accessToken)
|
||||||
|
|
||||||
val user = getUser()
|
val user = getUser()
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.syncproviders.providers
|
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.app
|
|
||||||
import com.lagradost.cloudstream3.mvvm.debugPrint
|
|
||||||
import com.lagradost.cloudstream3.subtitles.AbstractSubProvider
|
|
||||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
|
||||||
import com.lagradost.cloudstream3.subtitles.SubtitleResource
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.providers.IndexSubtitleApi.Companion.getOrdinal
|
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
|
||||||
|
|
||||||
class SubScene : AbstractSubProvider {
|
|
||||||
val mainUrl = "https://subscene.com"
|
|
||||||
val name = "Subscene"
|
|
||||||
override val idPrefix = "subscene"
|
|
||||||
|
|
||||||
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity>? {
|
|
||||||
val seasonName =
|
|
||||||
query.seasonNumber?.let { number ->
|
|
||||||
// Need to translate "7" to "Seventh Season"
|
|
||||||
getOrdinal(number)?.let { words -> " - $words Season" }
|
|
||||||
} ?: ""
|
|
||||||
|
|
||||||
val fullQuery = query.query + seasonName
|
|
||||||
|
|
||||||
val doc = app.post(
|
|
||||||
"$mainUrl/subtitles/searchbytitle",
|
|
||||||
data = mapOf("query" to fullQuery, "l" to "")
|
|
||||||
).document
|
|
||||||
|
|
||||||
return doc.select("div.title a").map { element ->
|
|
||||||
val href = "$mainUrl${element.attr("href")}"
|
|
||||||
val title = element.text()
|
|
||||||
|
|
||||||
AbstractSubtitleEntities.SubtitleEntity(
|
|
||||||
idPrefix = idPrefix,
|
|
||||||
name = title,
|
|
||||||
source = name,
|
|
||||||
data = href,
|
|
||||||
lang = query.lang ?: "en",
|
|
||||||
epNumber = query.epNumber
|
|
||||||
)
|
|
||||||
}.distinctBy { it.data }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) {
|
|
||||||
val resultDoc = app.get(data.data).document
|
|
||||||
val queryLanguage = SubtitleHelper.fromTwoLettersToLanguage(data.lang) ?: "English"
|
|
||||||
|
|
||||||
val results = resultDoc.select("table tbody tr").mapNotNull { element ->
|
|
||||||
val anchor = element.select("a")
|
|
||||||
val href = anchor.attr("href") ?: return@mapNotNull null
|
|
||||||
val fixedHref = "$mainUrl${href}"
|
|
||||||
val spans = anchor.select("span")
|
|
||||||
val language = spans.firstOrNull()?.text()
|
|
||||||
val title = spans.getOrNull(1)?.text()
|
|
||||||
val isPositive = anchor.select("span.positive-icon").isNotEmpty()
|
|
||||||
|
|
||||||
TableElement(title, language, fixedHref, isPositive)
|
|
||||||
}.sortedBy {
|
|
||||||
it.getScore(queryLanguage, data.epNumber)
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint { "$name found subtitles: ${results.takeLast(3)}" }
|
|
||||||
// Last = highest score
|
|
||||||
val selectedResult = results.lastOrNull() ?: return
|
|
||||||
|
|
||||||
val subtitleDocument = app.get(selectedResult.href).document
|
|
||||||
val subtitleDownloadUrl =
|
|
||||||
"$mainUrl${subtitleDocument.select("div.download a").attr("href")}"
|
|
||||||
|
|
||||||
this.addZipUrl(subtitleDownloadUrl) { name, _ ->
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class to manage the various different subtitle results and rank them.
|
|
||||||
*/
|
|
||||||
data class TableElement(
|
|
||||||
val title: String?,
|
|
||||||
val language: String?,
|
|
||||||
val href: String,
|
|
||||||
val isPositive: Boolean
|
|
||||||
) {
|
|
||||||
private fun matchesLanguage(other: String): Boolean {
|
|
||||||
return language != null && (language.contains(other, ignoreCase = true) ||
|
|
||||||
other.contains(language, ignoreCase = true))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scores in this order:
|
|
||||||
* Preferred Language > Episode number > Positive rating > English Language
|
|
||||||
*/
|
|
||||||
fun getScore(queryLanguage: String, episodeNum: Int?): Int {
|
|
||||||
var score = 0
|
|
||||||
if (this.matchesLanguage(queryLanguage)) {
|
|
||||||
score += 8
|
|
||||||
}
|
|
||||||
// Matches Episode 7 using "E07" with any number of leading zeroes
|
|
||||||
if (episodeNum != null && title != null && title.contains(
|
|
||||||
Regex(
|
|
||||||
"""E0*${episodeNum}""",
|
|
||||||
RegexOption.IGNORE_CASE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
score += 4
|
|
||||||
}
|
|
||||||
if (isPositive) {
|
|
||||||
score += 2
|
|
||||||
}
|
|
||||||
if (this.matchesLanguage("English")) {
|
|
||||||
score += 1
|
|
||||||
}
|
|
||||||
return score
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
package com.lagradost.cloudstream3.syncproviders.providers
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.TvType
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.subtitles.AbstractSubProvider
|
||||||
|
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||||
|
import com.lagradost.cloudstream3.subtitles.SubtitleResource
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||||
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
|
|
||||||
|
class SubSourceApi : AbstractSubProvider {
|
||||||
|
override val idPrefix = "subsource"
|
||||||
|
val name = "SubSource"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val APIURL = "https://api.subsource.net/api"
|
||||||
|
const val DOWNLOADENDPOINT = "https://api.subsource.net/api/downloadSub"
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity>? {
|
||||||
|
|
||||||
|
//Only supports Imdb Id search for now
|
||||||
|
if (query.imdbId == null) return null
|
||||||
|
val queryLang = SubtitleHelper.fromTwoLettersToLanguage(query.lang!!)
|
||||||
|
val type = if ((query.seasonNumber ?: 0) > 0) TvType.TvSeries else TvType.Movie
|
||||||
|
|
||||||
|
val searchRes = app.post(
|
||||||
|
url = "$APIURL/searchMovie",
|
||||||
|
data = mapOf(
|
||||||
|
"query" to query.imdbId!!
|
||||||
|
)
|
||||||
|
).parsedSafe<ApiSearch>() ?: return null
|
||||||
|
|
||||||
|
val postData = if (type == TvType.TvSeries) {
|
||||||
|
mapOf(
|
||||||
|
"langs" to "[]",
|
||||||
|
"movieName" to searchRes.found.first().linkName,
|
||||||
|
"season" to "season-${query.seasonNumber}"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
mapOf(
|
||||||
|
"langs" to "[]",
|
||||||
|
"movieName" to searchRes.found.first().linkName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val getMovieRes = app.post(
|
||||||
|
url = "$APIURL/getMovie",
|
||||||
|
data = postData
|
||||||
|
).parsedSafe<ApiResponse>().let {
|
||||||
|
// api doesn't has episode number or lang filtering
|
||||||
|
if (type == TvType.Movie) {
|
||||||
|
it?.subs?.filter { sub ->
|
||||||
|
sub.lang == queryLang
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
it?.subs?.filter { sub ->
|
||||||
|
sub.releaseName!!.contains(
|
||||||
|
String.format(
|
||||||
|
null,
|
||||||
|
"E%02d",
|
||||||
|
query.epNumber
|
||||||
|
)
|
||||||
|
) && sub.lang == queryLang
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: return null
|
||||||
|
|
||||||
|
return getMovieRes.map { subtitle ->
|
||||||
|
AbstractSubtitleEntities.SubtitleEntity(
|
||||||
|
idPrefix = this.idPrefix,
|
||||||
|
name = subtitle.releaseName!!,
|
||||||
|
lang = subtitle.lang!!,
|
||||||
|
data = SubData(
|
||||||
|
movie = subtitle.linkName!!,
|
||||||
|
lang = subtitle.lang,
|
||||||
|
id = subtitle.subId.toString(),
|
||||||
|
).toJson(),
|
||||||
|
type = type,
|
||||||
|
source = this.name,
|
||||||
|
epNumber = query.epNumber,
|
||||||
|
seasonNumber = query.seasonNumber,
|
||||||
|
isHearingImpaired = subtitle.hi == 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) {
|
||||||
|
|
||||||
|
val parsedSub = parseJson<SubData>(data.data)
|
||||||
|
|
||||||
|
val subRes = app.post(
|
||||||
|
url = "$APIURL/getSub",
|
||||||
|
data = mapOf(
|
||||||
|
"movie" to parsedSub.movie,
|
||||||
|
"lang" to data.lang,
|
||||||
|
"id" to parsedSub.id
|
||||||
|
)
|
||||||
|
).parsedSafe<SubTitleLink>() ?: return
|
||||||
|
|
||||||
|
this.addZipUrl(
|
||||||
|
"$DOWNLOADENDPOINT/${subRes.sub.downloadToken}"
|
||||||
|
) { name, _ ->
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ApiSearch(
|
||||||
|
@JsonProperty("success") val success: Boolean,
|
||||||
|
@JsonProperty("found") val found: List<Found>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Found(
|
||||||
|
@JsonProperty("id") val id: Long,
|
||||||
|
@JsonProperty("title") val title: String,
|
||||||
|
@JsonProperty("seasons") val seasons: Long,
|
||||||
|
@JsonProperty("type") val type: String,
|
||||||
|
@JsonProperty("releaseYear") val releaseYear: Long,
|
||||||
|
@JsonProperty("linkName") val linkName: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ApiResponse(
|
||||||
|
@JsonProperty("success") val success: Boolean,
|
||||||
|
@JsonProperty("movie") val movie: Movie,
|
||||||
|
@JsonProperty("subs") val subs: List<Sub>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Movie(
|
||||||
|
@JsonProperty("id") val id: Long? = null,
|
||||||
|
@JsonProperty("type") val type: String? = null,
|
||||||
|
@JsonProperty("year") val year: Long? = null,
|
||||||
|
@JsonProperty("fullName") val fullName: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Sub(
|
||||||
|
@JsonProperty("hi") val hi: Int? = null,
|
||||||
|
@JsonProperty("fullLink") val fullLink: String? = null,
|
||||||
|
@JsonProperty("linkName") val linkName: String? = null,
|
||||||
|
@JsonProperty("lang") val lang: String? = null,
|
||||||
|
@JsonProperty("releaseName") val releaseName: String? = null,
|
||||||
|
@JsonProperty("subId") val subId: Long? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SubData(
|
||||||
|
@JsonProperty("movie") val movie: String,
|
||||||
|
@JsonProperty("lang") val lang: String,
|
||||||
|
@JsonProperty("id") val id: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SubTitleLink(
|
||||||
|
@JsonProperty("sub") val sub: SubToken,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SubToken(
|
||||||
|
@JsonProperty("downloadToken") val downloadToken: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
package com.lagradost.cloudstream3.syncproviders.providers
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
|
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||||
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.TvType
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
|
||||||
|
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||||
|
import com.lagradost.cloudstream3.subtitles.SubtitleResource
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AuthAPI.LoginInfo
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
|
||||||
|
|
||||||
|
class SubDlApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi {
|
||||||
|
override val idPrefix = "subdl"
|
||||||
|
override val name = "SubDL"
|
||||||
|
override val icon = R.drawable.subdl_logo_big
|
||||||
|
override val requiresPassword = true
|
||||||
|
override val requiresEmail = true
|
||||||
|
override val createAccountUrl = "https://subdl.com/login"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val APIURL = "https://api.subdl.com"
|
||||||
|
const val APIENDPOINT = "$APIURL/api/v1/subtitles"
|
||||||
|
const val DOWNLOADENDPOINT = "https://dl.subdl.com"
|
||||||
|
const val SUBDL_SUBTITLES_USER_KEY: String = "subdl_user"
|
||||||
|
var currentSession: SubtitleOAuthEntity? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun initialize() {
|
||||||
|
currentSession = getAuthKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun logOut() {
|
||||||
|
setAuthKey(null)
|
||||||
|
removeAccountKeys()
|
||||||
|
currentSession = getAuthKey()
|
||||||
|
}
|
||||||
|
override suspend fun login(data: InAppAuthAPI.LoginData): Boolean {
|
||||||
|
val email = data.email ?: throw ErrorLoadingException("Requires Email")
|
||||||
|
val password = data.password ?: throw ErrorLoadingException("Requires Password")
|
||||||
|
switchToNewAccount()
|
||||||
|
try {
|
||||||
|
if (initLogin(email, password)) {
|
||||||
|
registerAccount()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
switchToOldAccount()
|
||||||
|
}
|
||||||
|
switchToOldAccount()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLatestLoginData(): InAppAuthAPI.LoginData? {
|
||||||
|
val current = getAuthKey() ?: return null
|
||||||
|
return InAppAuthAPI.LoginData(
|
||||||
|
email = current.userEmail,
|
||||||
|
password = current.pass
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loginInfo(): LoginInfo? {
|
||||||
|
getAuthKey()?.let { user ->
|
||||||
|
return LoginInfo(
|
||||||
|
profilePicture = null,
|
||||||
|
name = user.name ?: user.userEmail,
|
||||||
|
accountIndex = accountIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity>? {
|
||||||
|
|
||||||
|
val queryText = query.query
|
||||||
|
val epNum = query.epNumber ?: 0
|
||||||
|
val seasonNum = query.seasonNumber ?: 0
|
||||||
|
val yearNum = query.year ?: 0
|
||||||
|
|
||||||
|
val idQuery = when {
|
||||||
|
query.imdbId != null -> "&imdb_id=${query.imdbId}"
|
||||||
|
query.tmdbId != null -> "&tmdb_id=${query.tmdbId}"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
val epQuery = if (epNum > 0) "&episode_number=$epNum" else ""
|
||||||
|
val seasonQuery = if (seasonNum > 0) "&season_number=$seasonNum" else ""
|
||||||
|
val yearQuery = if (yearNum > 0) "&year=$yearNum" else ""
|
||||||
|
|
||||||
|
val searchQueryUrl = when (idQuery) {
|
||||||
|
//Use imdb/tmdb id to search if its valid
|
||||||
|
null -> "$APIENDPOINT?api_key=${currentSession?.apiKey}&film_name=$queryText&languages=${query.lang}$epQuery$seasonQuery$yearQuery"
|
||||||
|
else -> "$APIENDPOINT?api_key=${currentSession?.apiKey}$idQuery&languages=${query.lang}$epQuery$seasonQuery$yearQuery"
|
||||||
|
}
|
||||||
|
|
||||||
|
val req = app.get(
|
||||||
|
url = searchQueryUrl,
|
||||||
|
headers = mapOf(
|
||||||
|
"Accept" to "application/json"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return req.parsedSafe<ApiResponse>()?.subtitles?.map { subtitle ->
|
||||||
|
|
||||||
|
val lang = subtitle.lang.replaceFirstChar { it.uppercase() }
|
||||||
|
val resEpNum = subtitle.episode ?: query.epNumber
|
||||||
|
val resSeasonNum = subtitle.season ?: query.seasonNumber
|
||||||
|
val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie
|
||||||
|
|
||||||
|
AbstractSubtitleEntities.SubtitleEntity(
|
||||||
|
idPrefix = this.idPrefix,
|
||||||
|
name = subtitle.releaseName,
|
||||||
|
lang = lang,
|
||||||
|
data = "${DOWNLOADENDPOINT}${subtitle.url}",
|
||||||
|
type = type,
|
||||||
|
source = this.name,
|
||||||
|
epNumber = resEpNum,
|
||||||
|
seasonNumber = resSeasonNum,
|
||||||
|
isHearingImpaired = subtitle.hearingImpaired ?: false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) {
|
||||||
|
this.addZipUrl(data.data) { name, _ ->
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun initLogin(useremail: String, password: String): Boolean {
|
||||||
|
|
||||||
|
val tokenResponse = app.post(
|
||||||
|
url = "$APIURL/login",
|
||||||
|
data = mapOf(
|
||||||
|
"email" to useremail,
|
||||||
|
"password" to password
|
||||||
|
)
|
||||||
|
).parsedSafe<OAuthTokenResponse>()
|
||||||
|
|
||||||
|
if (tokenResponse?.token == null) return false
|
||||||
|
|
||||||
|
val apiResponse = app.get(
|
||||||
|
url = "$APIURL/user/userApi",
|
||||||
|
headers = mapOf(
|
||||||
|
"Authorization" to "Bearer ${tokenResponse.token}"
|
||||||
|
)
|
||||||
|
).parsedSafe<ApiKeyResponse>()
|
||||||
|
|
||||||
|
if (apiResponse?.ok == false) return false
|
||||||
|
|
||||||
|
setAuthKey(
|
||||||
|
SubtitleOAuthEntity(
|
||||||
|
userEmail = useremail,
|
||||||
|
pass = password,
|
||||||
|
name = tokenResponse.userData?.username ?: tokenResponse.userData?.name,
|
||||||
|
accessToken = tokenResponse.token,
|
||||||
|
apiKey = apiResponse?.apiKey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAuthKey(): SubtitleOAuthEntity? {
|
||||||
|
return getKey(accountId, SUBDL_SUBTITLES_USER_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAuthKey(data: SubtitleOAuthEntity?) {
|
||||||
|
if (data == null) removeKey(
|
||||||
|
accountId,
|
||||||
|
SUBDL_SUBTITLES_USER_KEY
|
||||||
|
)
|
||||||
|
currentSession = data
|
||||||
|
setKey(accountId, SUBDL_SUBTITLES_USER_KEY, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SubtitleOAuthEntity(
|
||||||
|
@JsonProperty("userEmail") var userEmail: String,
|
||||||
|
@JsonProperty("pass") var pass: String,
|
||||||
|
@JsonProperty("name") var name: String? = null,
|
||||||
|
@JsonProperty("accessToken") var accessToken: String? = null,
|
||||||
|
@JsonProperty("apiKey") var apiKey: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class OAuthTokenResponse(
|
||||||
|
@JsonProperty("token") val token: String? = null,
|
||||||
|
@JsonProperty("userData") val userData: UserData? = null,
|
||||||
|
@JsonProperty("status") val status: Boolean? = null,
|
||||||
|
@JsonProperty("message") val message: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UserData(
|
||||||
|
@JsonProperty("email") val email: String,
|
||||||
|
@JsonProperty("name") val name: String,
|
||||||
|
@JsonProperty("country") val country: String,
|
||||||
|
@JsonProperty("scStepCode") val scStepCode: String,
|
||||||
|
@JsonProperty("scVerified") val scVerified: Boolean,
|
||||||
|
@JsonProperty("username") val username: String? = null,
|
||||||
|
@JsonProperty("scUsername") val scUsername: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ApiKeyResponse(
|
||||||
|
@JsonProperty("ok") val ok: Boolean? = false,
|
||||||
|
@JsonProperty("api_key") val apiKey: String? = null,
|
||||||
|
@JsonProperty("usage") val usage: Usage? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Usage(
|
||||||
|
@JsonProperty("total") val total: Long? = 0,
|
||||||
|
@JsonProperty("today") val today: Long? = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ApiResponse(
|
||||||
|
@JsonProperty("status") val status: Boolean? = null,
|
||||||
|
@JsonProperty("results") val results: List<Result>? = null,
|
||||||
|
@JsonProperty("subtitles") val subtitles: List<Subtitle>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Result(
|
||||||
|
@JsonProperty("sd_id") val sdId: Int? = null,
|
||||||
|
@JsonProperty("type") val type: String? = null,
|
||||||
|
@JsonProperty("name") val name: String? = null,
|
||||||
|
@JsonProperty("imdb_id") val imdbId: String? = null,
|
||||||
|
@JsonProperty("tmdb_id") val tmdbId: Long? = null,
|
||||||
|
@JsonProperty("first_air_date") val firstAirDate: String? = null,
|
||||||
|
@JsonProperty("year") val year: Int? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Subtitle(
|
||||||
|
@JsonProperty("release_name") val releaseName: String,
|
||||||
|
@JsonProperty("name") val name: String,
|
||||||
|
@JsonProperty("lang") val lang: String,
|
||||||
|
@JsonProperty("author") val author: String? = null,
|
||||||
|
@JsonProperty("url") val url: String? = null,
|
||||||
|
@JsonProperty("subtitlePage") val subtitlePage: String? = null,
|
||||||
|
@JsonProperty("season") val season: Int? = null,
|
||||||
|
@JsonProperty("episode") val episode: Int? = null,
|
||||||
|
@JsonProperty("language") val language: String? = null,
|
||||||
|
@JsonProperty("hi") val hearingImpaired: Boolean? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -50,7 +50,7 @@ class APIRepository(val api: MainAPI) {
|
||||||
|
|
||||||
private val cache = threadSafeListOf<SavedLoadResponse>()
|
private val cache = threadSafeListOf<SavedLoadResponse>()
|
||||||
private var cacheIndex: Int = 0
|
private var cacheIndex: Int = 0
|
||||||
const val cacheSize = 20
|
const val CACHE_SIZE = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun afterPluginsLoaded(forceReload: Boolean) {
|
private fun afterPluginsLoaded(forceReload: Boolean) {
|
||||||
|
|
@ -94,9 +94,9 @@ class APIRepository(val api: MainAPI) {
|
||||||
val add = SavedLoadResponse(unixTime, response, lookingForHash)
|
val add = SavedLoadResponse(unixTime, response, lookingForHash)
|
||||||
|
|
||||||
synchronized(cache) {
|
synchronized(cache) {
|
||||||
if (cache.size > cacheSize) {
|
if (cache.size > CACHE_SIZE) {
|
||||||
cache[cacheIndex] = add // rolling cache
|
cache[cacheIndex] = add // rolling cache
|
||||||
cacheIndex = (cacheIndex + 1) % cacheSize
|
cacheIndex = (cacheIndex + 1) % CACHE_SIZE
|
||||||
} else {
|
} else {
|
||||||
cache.add(add)
|
cache.add(add)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
252
app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt
Normal file
252
app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
package com.lagradost.cloudstream3.ui
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.children
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||||
|
import androidx.recyclerview.widget.AsyncListDiffer
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
|
open class ViewHolderState<T>(val view: ViewBinding) : ViewHolder(view.root) {
|
||||||
|
open fun save(): T? = null
|
||||||
|
open fun restore(state: T) = Unit
|
||||||
|
open fun onViewAttachedToWindow() = Unit
|
||||||
|
open fun onViewDetachedFromWindow() = Unit
|
||||||
|
open fun onViewRecycled() = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Based of the concept https://github.com/brahmkshatriya/echo/blob/main/app%2Fsrc%2Fmain%2Fjava%2Fdev%2Fbrahmkshatriya%2Fecho%2Fui%2Fadapters%2FMediaItemsContainerAdapter.kt#L108-L154
|
||||||
|
class StateViewModel : ViewModel() {
|
||||||
|
val layoutManagerStates = hashMapOf<Int, HashMap<Int, Any?>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class NoStateAdapter<T : Any>(fragment: Fragment) : BaseAdapter<T, Any>(fragment, 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BaseAdapter is a persistent state stored adapter that supports headers and footers.
|
||||||
|
* This should be used for restoring eg scroll or focus related to a view when it is recreated.
|
||||||
|
*
|
||||||
|
* Id is a per fragment based unique id used to store the underlying data done in an internal ViewModel.
|
||||||
|
*
|
||||||
|
* diffCallback is how the view should be handled when updating, override onUpdateContent for updates
|
||||||
|
*
|
||||||
|
* NOTE:
|
||||||
|
*
|
||||||
|
* By default it should save automatically, but you can also call save(recycle)
|
||||||
|
*
|
||||||
|
* By default no state is stored, but doing an id != 0 will store
|
||||||
|
*
|
||||||
|
* By default no headers or footers exist, override footers and headers count
|
||||||
|
*/
|
||||||
|
abstract class BaseAdapter<
|
||||||
|
T : Any,
|
||||||
|
S : Any>(
|
||||||
|
fragment: Fragment,
|
||||||
|
val id: Int = 0,
|
||||||
|
diffCallback: DiffUtil.ItemCallback<T> = BaseDiffCallback()
|
||||||
|
) : RecyclerView.Adapter<ViewHolderState<S>>() {
|
||||||
|
open val footers: Int = 0
|
||||||
|
open val headers: Int = 0
|
||||||
|
|
||||||
|
fun getItem(position: Int): T {
|
||||||
|
return mDiffer.currentList[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getItemOrNull(position: Int): T? {
|
||||||
|
return mDiffer.currentList.getOrNull(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mDiffer: AsyncListDiffer<T> = AsyncListDiffer(
|
||||||
|
object : NonFinalAdapterListUpdateCallback(this) {
|
||||||
|
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||||
|
super.onMoved(fromPosition + headers, toPosition + headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemoved(position: Int, count: Int) {
|
||||||
|
super.onRemoved(position + headers, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||||
|
super.onChanged(position + headers, count, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInserted(position: Int, count: Int) {
|
||||||
|
super.onInserted(position + headers, count)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AsyncDifferConfig.Builder(diffCallback).build()
|
||||||
|
)
|
||||||
|
|
||||||
|
open fun submitList(list: List<T>?) {
|
||||||
|
// deep copy at least the top list, because otherwise adapter can go crazy
|
||||||
|
mDiffer.submitList(list?.let { CopyOnWriteArrayList(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return mDiffer.currentList.size + footers + headers
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onUpdateContent(holder: ViewHolderState<S>, item: T, position: Int) =
|
||||||
|
onBindContent(holder, item, position)
|
||||||
|
|
||||||
|
open fun onBindContent(holder: ViewHolderState<S>, item: T, position: Int) = Unit
|
||||||
|
open fun onBindFooter(holder: ViewHolderState<S>) = Unit
|
||||||
|
open fun onBindHeader(holder: ViewHolderState<S>) = Unit
|
||||||
|
open fun onCreateContent(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError()
|
||||||
|
open fun onCreateFooter(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError()
|
||||||
|
open fun onCreateHeader(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError()
|
||||||
|
|
||||||
|
override fun onViewAttachedToWindow(holder: ViewHolderState<S>) {
|
||||||
|
holder.onViewAttachedToWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewDetachedFromWindow(holder: ViewHolderState<S>) {
|
||||||
|
holder.onViewDetachedFromWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun save(recyclerView: RecyclerView) {
|
||||||
|
for (child in recyclerView.children) {
|
||||||
|
val holder =
|
||||||
|
recyclerView.findContainingViewHolder(child) as? ViewHolderState<S> ?: continue
|
||||||
|
setState(holder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
stateViewModel.layoutManagerStates[id]?.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun getState(holder: ViewHolderState<S>): S? =
|
||||||
|
stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S
|
||||||
|
|
||||||
|
private fun setState(holder: ViewHolderState<S>) {
|
||||||
|
if(id == 0) return
|
||||||
|
|
||||||
|
if (!stateViewModel.layoutManagerStates.contains(id)) {
|
||||||
|
stateViewModel.layoutManagerStates[id] = HashMap()
|
||||||
|
}
|
||||||
|
stateViewModel.layoutManagerStates[id]?.let { map ->
|
||||||
|
map[holder.absoluteAdapterPosition] = holder.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val attachListener = object : View.OnAttachStateChangeListener {
|
||||||
|
override fun onViewAttachedToWindow(v: View) = Unit
|
||||||
|
override fun onViewDetachedFromWindow(v: View) {
|
||||||
|
if (v !is RecyclerView) return
|
||||||
|
save(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||||
|
recyclerView.addOnAttachStateChangeListener(attachListener)
|
||||||
|
super.onAttachedToRecyclerView(recyclerView)
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||||
|
recyclerView.removeOnAttachStateChangeListener(attachListener)
|
||||||
|
super.onDetachedFromRecyclerView(recyclerView)
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun getItemViewType(position: Int): Int {
|
||||||
|
if (position < headers) {
|
||||||
|
return HEADER
|
||||||
|
}
|
||||||
|
if (position - headers >= mDiffer.currentList.size) {
|
||||||
|
return FOOTER
|
||||||
|
}
|
||||||
|
|
||||||
|
return CONTENT
|
||||||
|
}
|
||||||
|
|
||||||
|
private val stateViewModel: StateViewModel by fragment.viewModels()
|
||||||
|
|
||||||
|
final override fun onViewRecycled(holder: ViewHolderState<S>) {
|
||||||
|
setState(holder)
|
||||||
|
holder.onViewRecycled()
|
||||||
|
super.onViewRecycled(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderState<S> {
|
||||||
|
return when (viewType) {
|
||||||
|
CONTENT -> onCreateContent(parent)
|
||||||
|
HEADER -> onCreateHeader(parent)
|
||||||
|
FOOTER -> onCreateFooter(parent)
|
||||||
|
else -> throw NotImplementedError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://medium.com/@domen.lanisnik/efficiently-updating-recyclerview-items-using-payloads-1305f65f3068
|
||||||
|
override fun onBindViewHolder(
|
||||||
|
holder: ViewHolderState<S>,
|
||||||
|
position: Int,
|
||||||
|
payloads: MutableList<Any>
|
||||||
|
) {
|
||||||
|
if (payloads.isEmpty()) {
|
||||||
|
super.onBindViewHolder(holder, position, payloads)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
when (getItemViewType(position)) {
|
||||||
|
CONTENT -> {
|
||||||
|
val realPosition = position - headers
|
||||||
|
val item = getItem(realPosition)
|
||||||
|
onUpdateContent(holder, item, realPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
FOOTER -> {
|
||||||
|
onBindFooter(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
HEADER -> {
|
||||||
|
onBindHeader(holder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onBindViewHolder(holder: ViewHolderState<S>, position: Int) {
|
||||||
|
when (getItemViewType(position)) {
|
||||||
|
CONTENT -> {
|
||||||
|
val realPosition = position - headers
|
||||||
|
val item = getItem(realPosition)
|
||||||
|
onBindContent(holder, item, realPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
FOOTER -> {
|
||||||
|
onBindFooter(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
HEADER -> {
|
||||||
|
onBindHeader(holder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getState(holder)?.let { state ->
|
||||||
|
holder.restore(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val HEADER: Int = 1
|
||||||
|
private const val FOOTER: Int = 2
|
||||||
|
private const val CONTENT: Int = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BaseDiffCallback<T : Any>(
|
||||||
|
val itemSame: (T, T) -> Boolean = { a, b -> a.hashCode() == b.hashCode() },
|
||||||
|
val contentSame: (T, T) -> Boolean = { a, b -> a.hashCode() == b.hashCode() }
|
||||||
|
) : DiffUtil.ItemCallback<T>() {
|
||||||
|
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = itemSame(oldItem, newItem)
|
||||||
|
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = contentSame(oldItem, newItem)
|
||||||
|
override fun getChangePayload(oldItem: T, newItem: T): Any = Any()
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import android.view.Menu
|
||||||
import android.view.View.*
|
import android.view.View.*
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.databind.json.JsonMapper
|
import com.fasterxml.jackson.databind.json.JsonMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.kotlinModule
|
import com.fasterxml.jackson.module.kotlin.kotlinModule
|
||||||
|
|
@ -23,13 +24,13 @@ import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.mvvm.Resource
|
import com.lagradost.cloudstream3.mvvm.Resource
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||||
import com.lagradost.cloudstream3.sortSubs
|
|
||||||
import com.lagradost.cloudstream3.sortUrls
|
import com.lagradost.cloudstream3.sortUrls
|
||||||
import com.lagradost.cloudstream3.ui.player.LoadType
|
import com.lagradost.cloudstream3.ui.player.LoadType
|
||||||
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
|
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
|
||||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||||
import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment
|
import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||||
import com.lagradost.cloudstream3.utils.CastHelper.awaitLinks
|
import com.lagradost.cloudstream3.utils.CastHelper.awaitLinks
|
||||||
import com.lagradost.cloudstream3.utils.CastHelper.getMediaInfo
|
import com.lagradost.cloudstream3.utils.CastHelper.getMediaInfo
|
||||||
|
|
@ -263,6 +264,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
||||||
|
|
||||||
var isLoadingMore = false
|
var isLoadingMore = false
|
||||||
|
|
||||||
|
|
||||||
override fun onMediaStatusUpdated() {
|
override fun onMediaStatusUpdated() {
|
||||||
super.onMediaStatusUpdated()
|
super.onMediaStatusUpdated()
|
||||||
val meta = getCurrentMetaData()
|
val meta = getCurrentMetaData()
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
class GrdLayoutManager(val context: Context, _spanCount: Int) :
|
class GrdLayoutManager(val context: Context, spanCount: Int) :
|
||||||
GridLayoutManager(context, _spanCount) {
|
GridLayoutManager(context, spanCount) {
|
||||||
override fun onFocusSearchFailed(
|
override fun onFocusSearchFailed(
|
||||||
focused: View,
|
focused: View,
|
||||||
focusDirection: Int,
|
focusDirection: Int,
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ class EasterEggMonke : AppCompatActivity() {
|
||||||
FrameLayout.LayoutParams.WRAP_CONTENT)
|
FrameLayout.LayoutParams.WRAP_CONTENT)
|
||||||
binding.frame.addView(newStar)
|
binding.frame.addView(newStar)
|
||||||
|
|
||||||
newStar.scaleX = Math.random().toFloat() * 1.5f + newStar.scaleX
|
newStar.scaleX += Math.random().toFloat() * 1.5f
|
||||||
newStar.scaleY = newStar.scaleX
|
newStar.scaleY = newStar.scaleX
|
||||||
starW *= newStar.scaleX
|
starW *= newStar.scaleX
|
||||||
starH *= newStar.scaleY
|
starH *= newStar.scaleY
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.lagradost.cloudstream3.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ListUpdateCallback that dispatches update events to the given adapter.
|
||||||
|
*
|
||||||
|
* @see DiffUtil.DiffResult.dispatchUpdatesTo
|
||||||
|
*/
|
||||||
|
open class NonFinalAdapterListUpdateCallback
|
||||||
|
/**
|
||||||
|
* Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter.
|
||||||
|
*
|
||||||
|
* @param mAdapter The Adapter to send updates to.
|
||||||
|
*/(private var mAdapter: RecyclerView.Adapter<*>) :
|
||||||
|
ListUpdateCallback {
|
||||||
|
|
||||||
|
override fun onInserted(position: Int, count: Int) {
|
||||||
|
mAdapter.notifyItemRangeInserted(position, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemoved(position: Int, count: Int) {
|
||||||
|
mAdapter.notifyItemRangeRemoved(position, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||||
|
mAdapter.notifyItemMoved(fromPosition, toPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
|
||||||
|
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||||
|
mAdapter.notifyItemRangeChanged(position, count, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ enum class WatchType(val internalId: Int, @StringRes val stringRes: Int, @Drawab
|
||||||
NONE(5, R.string.type_none, R.drawable.ic_baseline_add_24);
|
NONE(5, R.string.type_none, R.drawable.ic_baseline_add_24);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE
|
fun fromInternalId(id: Int?) = entries.find { value -> value.internalId == id } ?: NONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,6 +36,6 @@ enum class SyncWatchType(val internalId: Int, @StringRes val stringRes: Int, @Dr
|
||||||
REWATCHING(5, R.string.type_re_watching, R.drawable.ic_baseline_bookmark_24);
|
REWATCHING(5, R.string.type_re_watching, R.drawable.ic_baseline_bookmark_24);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE
|
fun fromInternalId(id: Int?) = entries.find { value -> value.internalId == id } ?: NONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,16 @@ import android.webkit.JavascriptInterface
|
||||||
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.lagradost.cloudstream3.MainActivity
|
import com.lagradost.cloudstream3.MainActivity
|
||||||
import com.lagradost.cloudstream3.USER_AGENT
|
import com.lagradost.cloudstream3.USER_AGENT
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentWebviewBinding
|
import com.lagradost.cloudstream3.databinding.FragmentWebviewBinding
|
||||||
import com.lagradost.cloudstream3.network.WebViewResolver
|
import com.lagradost.cloudstream3.network.WebViewResolver
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
|
import com.lagradost.cloudstream3.utils.AppContextUtils.loadRepository
|
||||||
|
|
||||||
|
|
||||||
class WebviewFragment : Fragment() {
|
class WebviewFragment : Fragment() {
|
||||||
|
|
@ -29,6 +31,7 @@ class WebviewFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding?.webView?.webViewClient = object : WebViewClient() {
|
binding?.webView?.webViewClient = object : WebViewClient() {
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
override fun shouldOverrideUrlLoading(
|
override fun shouldOverrideUrlLoading(
|
||||||
view: WebView?,
|
view: WebView?,
|
||||||
request: WebResourceRequest?
|
request: WebResourceRequest?
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ import com.lagradost.cloudstream3.databinding.AccountListItemBinding
|
||||||
import com.lagradost.cloudstream3.databinding.AccountListItemEditBinding
|
import com.lagradost.cloudstream3.databinding.AccountListItemEditBinding
|
||||||
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountEditDialog
|
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountEditDialog
|
||||||
import com.lagradost.cloudstream3.ui.result.setImage
|
import com.lagradost.cloudstream3.ui.result.setImage
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
|
|
||||||
|
|
@ -38,7 +40,7 @@ class AccountAdapter(
|
||||||
is AccountListItemBinding -> binding.apply {
|
is AccountListItemBinding -> binding.apply {
|
||||||
if (account == null) return@apply
|
if (account == null) return@apply
|
||||||
|
|
||||||
val isTv = isTvSettings() || !root.isInTouchMode
|
val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode
|
||||||
|
|
||||||
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex
|
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex
|
||||||
|
|
||||||
|
|
@ -80,7 +82,7 @@ class AccountAdapter(
|
||||||
is AccountListItemEditBinding -> binding.apply {
|
is AccountListItemEditBinding -> binding.apply {
|
||||||
if (account == null) return@apply
|
if (account == null) return@apply
|
||||||
|
|
||||||
val isTv = isTvSettings() || !root.isInTouchMode
|
val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode
|
||||||
|
|
||||||
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex
|
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.observe
|
import com.lagradost.cloudstream3.mvvm.observe
|
||||||
import com.lagradost.cloudstream3.ui.result.setImage
|
import com.lagradost.cloudstream3.ui.result.setImage
|
||||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDefaultAccount
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDefaultAccount
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
|
@ -19,17 +18,23 @@ import com.lagradost.cloudstream3.mvvm.observe
|
||||||
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
|
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
|
||||||
import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_EDIT_ACCOUNT
|
import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_EDIT_ACCOUNT
|
||||||
import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_SELECT_ACCOUNT
|
import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_SELECT_ACCOUNT
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTruePhone
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator
|
import com.lagradost.cloudstream3.utils.BiometricAuthenticator
|
||||||
|
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback
|
||||||
|
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt
|
||||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
|
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
|
||||||
|
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled
|
||||||
|
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo
|
||||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
|
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||||
|
|
||||||
class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.BiometricAuthCallback {
|
class AccountSelectActivity : AppCompatActivity(), BiometricCallback {
|
||||||
|
|
||||||
lateinit var viewModel: AccountViewModel
|
lateinit var viewModel: AccountViewModel
|
||||||
|
|
||||||
|
|
@ -47,7 +52,6 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
|
||||||
)
|
)
|
||||||
|
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false)
|
|
||||||
val skipStartup = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false
|
val skipStartup = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false
|
||||||
) || accounts.count() <= 1
|
) || accounts.count() <= 1
|
||||||
|
|
||||||
|
|
@ -55,16 +59,25 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
|
||||||
|
|
||||||
fun askBiometricAuth() {
|
fun askBiometricAuth() {
|
||||||
|
|
||||||
if (isTruePhone() && authEnabled) {
|
if (isLayout(PHONE) && isAuthEnabled(this)) {
|
||||||
if (deviceHasPasswordPinLock(this)) {
|
if (deviceHasPasswordPinLock(this)) {
|
||||||
startBiometricAuthentication(this, R.string.biometric_authentication_title, false)
|
startBiometricAuthentication(
|
||||||
|
this,
|
||||||
|
R.string.biometric_authentication_title,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
BiometricAuthenticator.promptInfo?.let {
|
promptInfo?.let { prompt ->
|
||||||
BiometricAuthenticator.biometricPrompt?.authenticate(it)
|
biometricPrompt?.authenticate(prompt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
showToast(R.string.phone_not_secured, Toast.LENGTH_LONG)
|
}
|
||||||
|
|
||||||
|
observe(viewModel.isAllowedLogin) { isAllowedLogin ->
|
||||||
|
if (isAllowedLogin) {
|
||||||
|
// We are allowed to continue to MainActivity
|
||||||
|
navigateToMainActivity()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,12 +88,6 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
|
||||||
if (currentAccount?.lockPin != null) {
|
if (currentAccount?.lockPin != null) {
|
||||||
CommonActivity.init(this)
|
CommonActivity.init(this)
|
||||||
viewModel.handleAccountSelect(currentAccount, this, true)
|
viewModel.handleAccountSelect(currentAccount, this, true)
|
||||||
observe(viewModel.isAllowedLogin) { isAllowedLogin ->
|
|
||||||
if (isAllowedLogin) {
|
|
||||||
// We are allowed to continue to MainActivity
|
|
||||||
navigateToMainActivity()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (accounts.count() > 1) {
|
if (accounts.count() > 1) {
|
||||||
showToast(this, getString(
|
showToast(this, getString(
|
||||||
|
|
@ -108,12 +115,6 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
|
||||||
// Handle the selected account
|
// Handle the selected account
|
||||||
accountSelectCallback = {
|
accountSelectCallback = {
|
||||||
viewModel.handleAccountSelect(it, this)
|
viewModel.handleAccountSelect(it, this)
|
||||||
observe(viewModel.isAllowedLogin) { isAllowedLogin ->
|
|
||||||
if (isAllowedLogin) {
|
|
||||||
// We are allowed to continue to MainActivity
|
|
||||||
navigateToMainActivity()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
accountCreateCallback = { viewModel.handleAccountUpdate(it, this) },
|
accountCreateCallback = { viewModel.handleAccountUpdate(it, this) },
|
||||||
accountEditCallback = {
|
accountEditCallback = {
|
||||||
|
|
@ -131,7 +132,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
|
||||||
|
|
||||||
recyclerView.adapter = adapter
|
recyclerView.adapter = adapter
|
||||||
|
|
||||||
if (isTvSettings()) {
|
if (isLayout(TV or EMULATOR)) {
|
||||||
binding.editAccountButton.setBackgroundResource(
|
binding.editAccountButton.setBackgroundResource(
|
||||||
R.drawable.player_button_tv_attr_no_bg
|
R.drawable.player_button_tv_attr_no_bg
|
||||||
)
|
)
|
||||||
|
|
@ -172,7 +173,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
|
||||||
viewModel.toggleIsEditing()
|
viewModel.toggleIsEditing()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTvSettings()) {
|
if (isLayout(TV or EMULATOR)) {
|
||||||
recyclerView.spanCount = if (liveAccounts.count() + 1 <= 6) {
|
recyclerView.spanCount = if (liveAccounts.count() + 1 <= 6) {
|
||||||
liveAccounts.count() + 1
|
liveAccounts.count() + 1
|
||||||
} else 6
|
} else 6
|
||||||
|
|
@ -191,4 +192,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
|
||||||
override fun onAuthenticationSuccess() {
|
override fun onAuthenticationSuccess() {
|
||||||
Log.i(BiometricAuthenticator.TAG,"Authentication successful in AccountSelectActivity")
|
Log.i(BiometricAuthenticator.TAG,"Authentication successful in AccountSelectActivity")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationError() {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,414 @@
|
||||||
|
package com.lagradost.cloudstream3.ui.download
|
||||||
|
|
||||||
|
import android.text.format.Formatter.formatShortFileSize
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.CheckBox
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding
|
||||||
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
import com.lagradost.cloudstream3.ui.download.button.DownloadStatusTell
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
|
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
|
|
||||||
|
const val DOWNLOAD_ACTION_PLAY_FILE = 0
|
||||||
|
const val DOWNLOAD_ACTION_DELETE_FILE = 1
|
||||||
|
const val DOWNLOAD_ACTION_RESUME_DOWNLOAD = 2
|
||||||
|
const val DOWNLOAD_ACTION_PAUSE_DOWNLOAD = 3
|
||||||
|
const val DOWNLOAD_ACTION_DOWNLOAD = 4
|
||||||
|
const val DOWNLOAD_ACTION_LONG_CLICK = 5
|
||||||
|
|
||||||
|
const val DOWNLOAD_ACTION_GO_TO_CHILD = 0
|
||||||
|
const val DOWNLOAD_ACTION_LOAD_RESULT = 1
|
||||||
|
|
||||||
|
sealed class VisualDownloadCached {
|
||||||
|
abstract val currentBytes: Long
|
||||||
|
abstract val totalBytes: Long
|
||||||
|
abstract val data: VideoDownloadHelper.DownloadCached
|
||||||
|
abstract var isSelected: Boolean
|
||||||
|
|
||||||
|
data class Child(
|
||||||
|
override val currentBytes: Long,
|
||||||
|
override val totalBytes: Long,
|
||||||
|
override val data: VideoDownloadHelper.DownloadEpisodeCached,
|
||||||
|
override var isSelected: Boolean,
|
||||||
|
) : VisualDownloadCached()
|
||||||
|
|
||||||
|
data class Header(
|
||||||
|
override val currentBytes: Long,
|
||||||
|
override val totalBytes: Long,
|
||||||
|
override val data: VideoDownloadHelper.DownloadHeaderCached,
|
||||||
|
override var isSelected: Boolean,
|
||||||
|
val child: VideoDownloadHelper.DownloadEpisodeCached?,
|
||||||
|
val currentOngoingDownloads: Int,
|
||||||
|
val totalDownloads: Int,
|
||||||
|
) : VisualDownloadCached()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DownloadClickEvent(
|
||||||
|
val action: Int,
|
||||||
|
val data: VideoDownloadHelper.DownloadEpisodeCached
|
||||||
|
)
|
||||||
|
|
||||||
|
data class DownloadHeaderClickEvent(
|
||||||
|
val action: Int,
|
||||||
|
val data: VideoDownloadHelper.DownloadHeaderCached
|
||||||
|
)
|
||||||
|
|
||||||
|
class DownloadAdapter(
|
||||||
|
private val onHeaderClickEvent: (DownloadHeaderClickEvent) -> Unit,
|
||||||
|
private val onItemClickEvent: (DownloadClickEvent) -> Unit,
|
||||||
|
private val onItemSelectionChanged: (Int, Boolean) -> Unit,
|
||||||
|
) : ListAdapter<VisualDownloadCached, DownloadAdapter.DownloadViewHolder>(DiffCallback()) {
|
||||||
|
|
||||||
|
private var isMultiDeleteState: Boolean = false
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val VIEW_TYPE_HEADER = 0
|
||||||
|
private const val VIEW_TYPE_CHILD = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class DownloadViewHolder(
|
||||||
|
private val binding: ViewBinding
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(card: VisualDownloadCached?) {
|
||||||
|
when (binding) {
|
||||||
|
is DownloadHeaderEpisodeBinding -> bindHeader(card as? VisualDownloadCached.Header)
|
||||||
|
is DownloadChildEpisodeBinding -> bindChild(card as? VisualDownloadCached.Child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindHeader(card: VisualDownloadCached.Header?) {
|
||||||
|
if (binding !is DownloadHeaderEpisodeBinding || card == null) return
|
||||||
|
|
||||||
|
val data = card.data
|
||||||
|
binding.apply {
|
||||||
|
episodeHolder.apply {
|
||||||
|
if (isMultiDeleteState) {
|
||||||
|
setOnClickListener {
|
||||||
|
toggleIsChecked(deleteCheckbox, data.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnLongClickListener {
|
||||||
|
toggleIsChecked(deleteCheckbox, data.id)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadHeaderPoster.apply {
|
||||||
|
setImage(data.poster)
|
||||||
|
if (isMultiDeleteState) {
|
||||||
|
setOnClickListener {
|
||||||
|
toggleIsChecked(deleteCheckbox, data.id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setOnClickListener {
|
||||||
|
onHeaderClickEvent.invoke(
|
||||||
|
DownloadHeaderClickEvent(
|
||||||
|
DOWNLOAD_ACTION_LOAD_RESULT,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnLongClickListener {
|
||||||
|
toggleIsChecked(deleteCheckbox, data.id)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
downloadHeaderTitle.text = data.name
|
||||||
|
val formattedSize = formatShortFileSize(itemView.context, card.totalBytes)
|
||||||
|
|
||||||
|
if (card.child != null) {
|
||||||
|
handleChildDownload(card, formattedSize)
|
||||||
|
} else handleParentDownload(card, formattedSize)
|
||||||
|
|
||||||
|
if (isMultiDeleteState) {
|
||||||
|
deleteCheckbox.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
onItemSelectionChanged.invoke(data.id, isChecked)
|
||||||
|
}
|
||||||
|
} else deleteCheckbox.setOnCheckedChangeListener(null)
|
||||||
|
|
||||||
|
deleteCheckbox.apply {
|
||||||
|
isVisible = isMultiDeleteState
|
||||||
|
isChecked = card.isSelected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DownloadHeaderEpisodeBinding.handleChildDownload(
|
||||||
|
card: VisualDownloadCached.Header,
|
||||||
|
formattedSize: String
|
||||||
|
) {
|
||||||
|
card.child ?: return
|
||||||
|
downloadHeaderGotoChild.isVisible = false
|
||||||
|
|
||||||
|
val posDur = getViewPos(card.data.id)
|
||||||
|
downloadHeaderEpisodeProgress.apply {
|
||||||
|
isVisible = posDur != null
|
||||||
|
posDur?.let {
|
||||||
|
val visualPos = it.fixVisual()
|
||||||
|
max = (visualPos.duration / 1000).toInt()
|
||||||
|
progress = (visualPos.position / 1000).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val status = downloadButton.getStatus(card.child.id, card.currentBytes, card.totalBytes)
|
||||||
|
if (status == DownloadStatusTell.IsDone) {
|
||||||
|
// We do this here instead if we are finished downloading
|
||||||
|
// so that we can use the value from the view model
|
||||||
|
// rather than extra unneeded disk operations and to prevent a
|
||||||
|
// delay in updating download icon state.
|
||||||
|
downloadButton.setProgress(card.currentBytes, card.totalBytes)
|
||||||
|
downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes)
|
||||||
|
// We will let the view model handle this
|
||||||
|
downloadButton.doSetProgress = false
|
||||||
|
downloadButton.progressBar.progressDrawable =
|
||||||
|
downloadButton.getDrawableFromStatus(status)
|
||||||
|
?.let { ContextCompat.getDrawable(downloadButton.context, it) }
|
||||||
|
downloadHeaderInfo.text = formattedSize
|
||||||
|
} else {
|
||||||
|
// We need to make sure we restore the correct progress
|
||||||
|
// when we refresh data in the adapter.
|
||||||
|
downloadButton.resetView()
|
||||||
|
val drawable = downloadButton.getDrawableFromStatus(status)?.let {
|
||||||
|
ContextCompat.getDrawable(downloadButton.context, it)
|
||||||
|
}
|
||||||
|
downloadButton.statusView.setImageDrawable(drawable)
|
||||||
|
downloadButton.progressBar.progressDrawable =
|
||||||
|
ContextCompat.getDrawable(
|
||||||
|
downloadButton.context,
|
||||||
|
downloadButton.progressDrawable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, onItemClickEvent)
|
||||||
|
downloadButton.isVisible = !isMultiDeleteState
|
||||||
|
|
||||||
|
if (!isMultiDeleteState) {
|
||||||
|
episodeHolder.setOnClickListener {
|
||||||
|
onItemClickEvent.invoke(
|
||||||
|
DownloadClickEvent(
|
||||||
|
DOWNLOAD_ACTION_PLAY_FILE,
|
||||||
|
card.child
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DownloadHeaderEpisodeBinding.handleParentDownload(
|
||||||
|
card: VisualDownloadCached.Header,
|
||||||
|
formattedSize: String
|
||||||
|
) {
|
||||||
|
downloadButton.isVisible = false
|
||||||
|
downloadHeaderEpisodeProgress.isVisible = false
|
||||||
|
downloadHeaderGotoChild.isVisible = !isMultiDeleteState
|
||||||
|
|
||||||
|
try {
|
||||||
|
downloadHeaderInfo.text =
|
||||||
|
downloadHeaderInfo.context.getString(R.string.extra_info_format).format(
|
||||||
|
card.totalDownloads,
|
||||||
|
downloadHeaderInfo.context.resources.getQuantityString(
|
||||||
|
R.plurals.episodes,
|
||||||
|
card.totalDownloads
|
||||||
|
),
|
||||||
|
formattedSize
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
downloadHeaderInfo.text = null
|
||||||
|
logError(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMultiDeleteState) {
|
||||||
|
episodeHolder.setOnClickListener {
|
||||||
|
onHeaderClickEvent.invoke(
|
||||||
|
DownloadHeaderClickEvent(
|
||||||
|
DOWNLOAD_ACTION_GO_TO_CHILD,
|
||||||
|
card.data
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindChild(card: VisualDownloadCached.Child?) {
|
||||||
|
if (binding !is DownloadChildEpisodeBinding || card == null) return
|
||||||
|
|
||||||
|
val data = card.data
|
||||||
|
binding.apply {
|
||||||
|
val posDur = getViewPos(data.id)
|
||||||
|
downloadChildEpisodeProgress.apply {
|
||||||
|
isVisible = posDur != null
|
||||||
|
posDur?.let {
|
||||||
|
val visualPos = it.fixVisual()
|
||||||
|
max = (visualPos.duration / 1000).toInt()
|
||||||
|
progress = (visualPos.position / 1000).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val status = downloadButton.getStatus(data.id, card.currentBytes, card.totalBytes)
|
||||||
|
if (status == DownloadStatusTell.IsDone) {
|
||||||
|
// We do this here instead if we are finished downloading
|
||||||
|
// so that we can use the value from the view model
|
||||||
|
// rather than extra unneeded disk operations and to prevent a
|
||||||
|
// delay in updating download icon state.
|
||||||
|
downloadButton.setProgress(card.currentBytes, card.totalBytes)
|
||||||
|
downloadButton.applyMetaData(data.id, card.currentBytes, card.totalBytes)
|
||||||
|
// We will let the view model handle this
|
||||||
|
downloadButton.doSetProgress = false
|
||||||
|
downloadButton.progressBar.progressDrawable =
|
||||||
|
downloadButton.getDrawableFromStatus(status)
|
||||||
|
?.let { ContextCompat.getDrawable(downloadButton.context, it) }
|
||||||
|
downloadChildEpisodeTextExtra.text =
|
||||||
|
formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes)
|
||||||
|
} else {
|
||||||
|
// We need to make sure we restore the correct progress
|
||||||
|
// when we refresh data in the adapter.
|
||||||
|
downloadButton.resetView()
|
||||||
|
val drawable = downloadButton.getDrawableFromStatus(status)?.let {
|
||||||
|
ContextCompat.getDrawable(downloadButton.context, it)
|
||||||
|
}
|
||||||
|
downloadButton.statusView.setImageDrawable(drawable)
|
||||||
|
downloadButton.progressBar.progressDrawable =
|
||||||
|
ContextCompat.getDrawable(
|
||||||
|
downloadButton.context,
|
||||||
|
downloadButton.progressDrawable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadButton.setDefaultClickListener(
|
||||||
|
data,
|
||||||
|
downloadChildEpisodeTextExtra,
|
||||||
|
onItemClickEvent
|
||||||
|
)
|
||||||
|
downloadButton.isVisible = !isMultiDeleteState
|
||||||
|
|
||||||
|
downloadChildEpisodeText.apply {
|
||||||
|
text = context.getNameFull(data.name, data.episode, data.season)
|
||||||
|
isSelected = true // Needed for text repeating
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadChildEpisodeHolder.setOnClickListener {
|
||||||
|
onItemClickEvent.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadChildEpisodeHolder.apply {
|
||||||
|
when {
|
||||||
|
isMultiDeleteState -> {
|
||||||
|
setOnClickListener {
|
||||||
|
toggleIsChecked(deleteCheckbox, data.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
setOnClickListener {
|
||||||
|
onItemClickEvent.invoke(
|
||||||
|
DownloadClickEvent(
|
||||||
|
DOWNLOAD_ACTION_PLAY_FILE,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnLongClickListener {
|
||||||
|
toggleIsChecked(deleteCheckbox, data.id)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMultiDeleteState) {
|
||||||
|
deleteCheckbox.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
onItemSelectionChanged.invoke(data.id, isChecked)
|
||||||
|
}
|
||||||
|
} else deleteCheckbox.setOnCheckedChangeListener(null)
|
||||||
|
|
||||||
|
deleteCheckbox.apply {
|
||||||
|
isVisible = isMultiDeleteState
|
||||||
|
isChecked = card.isSelected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadViewHolder {
|
||||||
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
val binding = when (viewType) {
|
||||||
|
VIEW_TYPE_HEADER -> DownloadHeaderEpisodeBinding.inflate(inflater, parent, false)
|
||||||
|
VIEW_TYPE_CHILD -> DownloadChildEpisodeBinding.inflate(inflater, parent, false)
|
||||||
|
else -> throw IllegalArgumentException("Invalid view type")
|
||||||
|
}
|
||||||
|
return DownloadViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return when (getItem(position)) {
|
||||||
|
is VisualDownloadCached.Child -> VIEW_TYPE_CHILD
|
||||||
|
is VisualDownloadCached.Header -> VIEW_TYPE_HEADER
|
||||||
|
else -> throw IllegalArgumentException("Invalid data type at position $position")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIsMultiDeleteState(value: Boolean) {
|
||||||
|
if (isMultiDeleteState == value) return
|
||||||
|
isMultiDeleteState = value
|
||||||
|
notifyItemRangeChanged(0, itemCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyAllSelected() {
|
||||||
|
currentList.indices.forEach { index ->
|
||||||
|
if (!currentList[index].isSelected) {
|
||||||
|
notifyItemChanged(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifySelectionStates() {
|
||||||
|
currentList.indices.forEach { index ->
|
||||||
|
if (currentList[index].isSelected) {
|
||||||
|
notifyItemChanged(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleIsChecked(checkbox: CheckBox, itemId: Int) {
|
||||||
|
val isChecked = !checkbox.isChecked
|
||||||
|
checkbox.isChecked = isChecked
|
||||||
|
onItemSelectionChanged.invoke(itemId, isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiffCallback : DiffUtil.ItemCallback<VisualDownloadCached>() {
|
||||||
|
override fun areItemsTheSame(
|
||||||
|
oldItem: VisualDownloadCached,
|
||||||
|
newItem: VisualDownloadCached
|
||||||
|
): Boolean {
|
||||||
|
return oldItem.data.id == newItem.data.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(
|
||||||
|
oldItem: VisualDownloadCached,
|
||||||
|
newItem: VisualDownloadCached
|
||||||
|
): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,28 +1,30 @@
|
||||||
package com.lagradost.cloudstream3.ui.download
|
package com.lagradost.cloudstream3.ui.download
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.widget.Toast
|
import android.net.Uri
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
|
||||||
import com.lagradost.cloudstream3.CommonActivity.activity
|
import com.lagradost.cloudstream3.CommonActivity.activity
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.ui.player.DownloadFileGenerator
|
import com.lagradost.cloudstream3.ui.player.DownloadFileGenerator
|
||||||
|
import com.lagradost.cloudstream3.ui.player.ExtractorUri
|
||||||
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
|
import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
|
||||||
|
import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE
|
||||||
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
|
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
|
|
||||||
object DownloadButtonSetup {
|
object DownloadButtonSetup {
|
||||||
fun handleDownloadClick(click: DownloadClickEvent) {
|
fun handleDownloadClick(click: DownloadClickEvent) {
|
||||||
val id = click.data.id
|
val id = click.data.id
|
||||||
if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return
|
|
||||||
when (click.action) {
|
when (click.action) {
|
||||||
DOWNLOAD_ACTION_DELETE_FILE -> {
|
DOWNLOAD_ACTION_DELETE_FILE -> {
|
||||||
activity?.let { ctx ->
|
activity?.let { ctx ->
|
||||||
|
|
@ -31,9 +33,15 @@ object DownloadButtonSetup {
|
||||||
DialogInterface.OnClickListener { _, which ->
|
DialogInterface.OnClickListener { _, which ->
|
||||||
when (which) {
|
when (which) {
|
||||||
DialogInterface.BUTTON_POSITIVE -> {
|
DialogInterface.BUTTON_POSITIVE -> {
|
||||||
VideoDownloadManager.deleteFileAndUpdateSettings(ctx, id)
|
VideoDownloadManager.deleteFilesAndUpdateSettings(
|
||||||
|
ctx,
|
||||||
|
setOf(id),
|
||||||
|
MainScope()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DialogInterface.BUTTON_NEGATIVE -> {
|
DialogInterface.BUTTON_NEGATIVE -> {
|
||||||
|
// Do nothing on cancel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -58,11 +66,13 @@ object DownloadButtonSetup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DOWNLOAD_ACTION_PAUSE_DOWNLOAD -> {
|
DOWNLOAD_ACTION_PAUSE_DOWNLOAD -> {
|
||||||
VideoDownloadManager.downloadEvent.invoke(
|
VideoDownloadManager.downloadEvent.invoke(
|
||||||
Pair(click.data.id, VideoDownloadManager.DownloadActionType.Pause)
|
Pair(click.data.id, VideoDownloadManager.DownloadActionType.Pause)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DOWNLOAD_ACTION_RESUME_DOWNLOAD -> {
|
DOWNLOAD_ACTION_RESUME_DOWNLOAD -> {
|
||||||
activity?.let { ctx ->
|
activity?.let { ctx ->
|
||||||
if (VideoDownloadManager.downloadStatus.containsKey(id) && VideoDownloadManager.downloadStatus[id] == VideoDownloadManager.DownloadType.IsPaused) {
|
if (VideoDownloadManager.downloadStatus.containsKey(id) && VideoDownloadManager.downloadStatus[id] == VideoDownloadManager.DownloadType.IsPaused) {
|
||||||
|
|
@ -81,6 +91,7 @@ object DownloadButtonSetup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DOWNLOAD_ACTION_LONG_CLICK -> {
|
DOWNLOAD_ACTION_LONG_CLICK -> {
|
||||||
activity?.let { act ->
|
activity?.let { act ->
|
||||||
val length =
|
val length =
|
||||||
|
|
@ -90,64 +101,80 @@ object DownloadButtonSetup {
|
||||||
)?.fileLength
|
)?.fileLength
|
||||||
?: 0
|
?: 0
|
||||||
if (length > 0) {
|
if (length > 0) {
|
||||||
showToast(R.string.delete, Toast.LENGTH_LONG)
|
showSnackbar(
|
||||||
} else {
|
act,
|
||||||
showToast(R.string.download, Toast.LENGTH_LONG)
|
R.string.offline_file,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DOWNLOAD_ACTION_PLAY_FILE -> {
|
DOWNLOAD_ACTION_PLAY_FILE -> {
|
||||||
activity?.let { act ->
|
activity?.let { act ->
|
||||||
val info =
|
|
||||||
VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
|
|
||||||
act,
|
|
||||||
click.data.id
|
|
||||||
) ?: return
|
|
||||||
val keyInfo = getKey<VideoDownloadManager.DownloadedFileInfo>(
|
|
||||||
VideoDownloadManager.KEY_DOWNLOAD_INFO,
|
|
||||||
click.data.id.toString()
|
|
||||||
) ?: return
|
|
||||||
val parent = getKey<VideoDownloadHelper.DownloadHeaderCached>(
|
val parent = getKey<VideoDownloadHelper.DownloadHeaderCached>(
|
||||||
DOWNLOAD_HEADER_CACHE,
|
DOWNLOAD_HEADER_CACHE,
|
||||||
click.data.parentId.toString()
|
click.data.parentId.toString()
|
||||||
) ?: return
|
) ?: return
|
||||||
|
|
||||||
act.navigate(
|
val episodes = getKeys(DOWNLOAD_EPISODE_CACHE)
|
||||||
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
|
?.mapNotNull {
|
||||||
DownloadFileGenerator(
|
getKey<VideoDownloadHelper.DownloadEpisodeCached>(it)
|
||||||
listOf(
|
}
|
||||||
ExtractorUri(
|
?.filter { it.parentId == click.data.parentId }
|
||||||
uri = info.path,
|
|
||||||
|
|
||||||
id = click.data.id,
|
val currentSeason = click.data.season ?: 0
|
||||||
parentId = click.data.parentId,
|
val currentEpisode = click.data.episode
|
||||||
name = act.getString(R.string.downloaded_file), //click.data.name ?: keyInfo.displayName
|
|
||||||
season = click.data.season,
|
|
||||||
episode = click.data.episode,
|
|
||||||
headerName = parent.name,
|
|
||||||
tvType = parent.type,
|
|
||||||
|
|
||||||
basePath = keyInfo.basePath,
|
val items = mutableListOf<ExtractorUri>()
|
||||||
displayName = keyInfo.displayName,
|
|
||||||
relativePath = keyInfo.relativePath,
|
// Make sure we only get this episode and episodes after it,
|
||||||
)
|
// and that we can go to the next season if we need to.
|
||||||
)
|
val allRelevantEpisodes = episodes
|
||||||
|
?.sortedWith(
|
||||||
|
compareByDescending<VideoDownloadHelper.DownloadEpisodeCached> { it.id == click.data.id }
|
||||||
|
.thenBy { it.season ?: 0 }
|
||||||
|
.thenBy { it.episode }
|
||||||
|
)
|
||||||
|
?.filter {
|
||||||
|
if (it.season == null) return@filter true
|
||||||
|
val isCurrentOrLaterInSeason = it.season == currentSeason && (it.episode >= currentEpisode || it.id == click.data.id)
|
||||||
|
val isInFutureSeasons = it.season > currentSeason
|
||||||
|
|
||||||
|
isCurrentOrLaterInSeason || isInFutureSeasons
|
||||||
|
}
|
||||||
|
|
||||||
|
allRelevantEpisodes?.forEach {
|
||||||
|
val keyInfo = getKey<VideoDownloadManager.DownloadedFileInfo>(
|
||||||
|
VideoDownloadManager.KEY_DOWNLOAD_INFO,
|
||||||
|
it.id.toString()
|
||||||
|
) ?: return@forEach
|
||||||
|
|
||||||
|
items.add(
|
||||||
|
ExtractorUri(
|
||||||
|
// We just use a temporary placeholder for the URI,
|
||||||
|
// it will be updated in generateLinks().
|
||||||
|
// We just do this for performance since getting
|
||||||
|
// all paths at once can be quite expensive.
|
||||||
|
uri = Uri.EMPTY,
|
||||||
|
id = it.id,
|
||||||
|
parentId = it.parentId,
|
||||||
|
name = act.getString(R.string.downloaded_file),
|
||||||
|
season = it.season,
|
||||||
|
episode = it.episode,
|
||||||
|
headerName = parent.name,
|
||||||
|
tvType = parent.type,
|
||||||
|
basePath = keyInfo.basePath,
|
||||||
|
displayName = keyInfo.displayName,
|
||||||
|
relativePath = keyInfo.relativePath,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
//R.id.global_to_navigation_player, PlayerFragment.newInstance(
|
}
|
||||||
// UriData(
|
|
||||||
// info.path.toString(),
|
act.navigate(
|
||||||
// keyInfo.basePath,
|
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
|
||||||
// keyInfo.relativePath,
|
DownloadFileGenerator(items)
|
||||||
// keyInfo.displayName,
|
)
|
||||||
// click.data.parentId,
|
|
||||||
// click.data.id,
|
|
||||||
// headerName ?: "null",
|
|
||||||
// if (click.data.episode <= 0) null else click.data.episode,
|
|
||||||
// click.data.season
|
|
||||||
// ),
|
|
||||||
// getViewPos(click.data.id)?.position ?: 0
|
|
||||||
//)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.ui.download
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding
|
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
|
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
|
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
|
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
|
||||||
|
|
||||||
const val DOWNLOAD_ACTION_PLAY_FILE = 0
|
|
||||||
const val DOWNLOAD_ACTION_DELETE_FILE = 1
|
|
||||||
const val DOWNLOAD_ACTION_RESUME_DOWNLOAD = 2
|
|
||||||
const val DOWNLOAD_ACTION_PAUSE_DOWNLOAD = 3
|
|
||||||
const val DOWNLOAD_ACTION_DOWNLOAD = 4
|
|
||||||
const val DOWNLOAD_ACTION_LONG_CLICK = 5
|
|
||||||
|
|
||||||
data class VisualDownloadChildCached(
|
|
||||||
val currentBytes: Long,
|
|
||||||
val totalBytes: Long,
|
|
||||||
val data: VideoDownloadHelper.DownloadEpisodeCached,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class DownloadClickEvent(val action: Int, val data: VideoDownloadHelper.DownloadEpisodeCached)
|
|
||||||
|
|
||||||
class DownloadChildAdapter(
|
|
||||||
var cardList: List<VisualDownloadChildCached>,
|
|
||||||
private val clickCallback: (DownloadClickEvent) -> Unit,
|
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
|
||||||
return DownloadChildViewHolder(
|
|
||||||
DownloadChildEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
|
||||||
clickCallback
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
|
||||||
when (holder) {
|
|
||||||
is DownloadChildViewHolder -> {
|
|
||||||
holder.bind(cardList[position])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return cardList.size
|
|
||||||
}
|
|
||||||
|
|
||||||
class DownloadChildViewHolder
|
|
||||||
constructor(
|
|
||||||
val binding: DownloadChildEpisodeBinding,
|
|
||||||
private val clickCallback: (DownloadClickEvent) -> Unit,
|
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
/*private val title: TextView = itemView.download_child_episode_text
|
|
||||||
private val extraInfo: TextView = itemView.download_child_episode_text_extra
|
|
||||||
private val holder: CardView = itemView.download_child_episode_holder
|
|
||||||
private val progressBar: ContentLoadingProgressBar = itemView.download_child_episode_progress
|
|
||||||
private val progressBarDownload: ContentLoadingProgressBar = itemView.download_child_episode_progress_downloaded
|
|
||||||
private val downloadImage: ImageView = itemView.download_child_episode_download*/
|
|
||||||
|
|
||||||
|
|
||||||
fun bind(card: VisualDownloadChildCached) {
|
|
||||||
val d = card.data
|
|
||||||
|
|
||||||
val posDur = getViewPos(d.id)
|
|
||||||
binding.downloadChildEpisodeProgress.apply {
|
|
||||||
if (posDur != null) {
|
|
||||||
val visualPos = posDur.fixVisual()
|
|
||||||
max = (visualPos.duration / 1000).toInt()
|
|
||||||
progress = (visualPos.position / 1000).toInt()
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.downloadButton.setDefaultClickListener(card.data, binding.downloadChildEpisodeTextExtra, clickCallback)
|
|
||||||
|
|
||||||
binding.downloadChildEpisodeText.apply {
|
|
||||||
text = context.getNameFull(d.name, d.episode, d.season)
|
|
||||||
isSelected = true // is needed for text repeating
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
binding.downloadChildEpisodeHolder.setOnClickListener {
|
|
||||||
clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +1,33 @@
|
||||||
package com.lagradost.cloudstream3.ui.download
|
package com.lagradost.cloudstream3.ui.download
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.text.format.Formatter.formatShortFileSize
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding
|
import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding
|
||||||
|
import com.lagradost.cloudstream3.mvvm.observe
|
||||||
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
||||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
|
import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback
|
||||||
|
import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class DownloadChildFragment : Fragment() {
|
class DownloadChildFragment : Fragment() {
|
||||||
|
private lateinit var downloadsViewModel: DownloadViewModel
|
||||||
|
private var binding: FragmentChildDownloadsBinding? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance(headerName: String, folder: String): Bundle {
|
fun newInstance(headerName: String, folder: String): Bundle {
|
||||||
return Bundle().apply {
|
return Bundle().apply {
|
||||||
|
|
@ -31,92 +38,170 @@ class DownloadChildFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it }
|
detachBackPressedCallback()
|
||||||
binding = null
|
binding = null
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
var binding: FragmentChildDownloadsBinding? = null
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
|
downloadsViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
|
||||||
val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false)
|
val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false)
|
||||||
binding = localBinding
|
binding = localBinding
|
||||||
return localBinding.root//inflater.inflate(R.layout.fragment_child_downloads, container, false)
|
return localBinding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateList(folder: String) = main {
|
|
||||||
context?.let { ctx ->
|
|
||||||
val data = withContext(Dispatchers.IO) { ctx.getKeys(folder) }
|
|
||||||
val eps = withContext(Dispatchers.IO) {
|
|
||||||
data.mapNotNull { key ->
|
|
||||||
context?.getKey<VideoDownloadHelper.DownloadEpisodeCached>(key)
|
|
||||||
}.mapNotNull {
|
|
||||||
val info = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(ctx, it.id)
|
|
||||||
?: return@mapNotNull null
|
|
||||||
VisualDownloadChildCached(info.fileLength, info.totalBytes, it)
|
|
||||||
}
|
|
||||||
}.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 }
|
|
||||||
if (eps.isEmpty()) {
|
|
||||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
|
||||||
return@main
|
|
||||||
}
|
|
||||||
|
|
||||||
(binding?.downloadChildList?.adapter as DownloadChildAdapter? ?: return@main).cardList =
|
|
||||||
eps
|
|
||||||
binding?.downloadChildList?.adapter?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var downloadDeleteEventListener: ((Int) -> Unit)? = null
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We never want to retain multi-delete state
|
||||||
|
* when navigating to downloads. Setting this state
|
||||||
|
* immediately can sometimes result in the observer
|
||||||
|
* not being notified in time to update the UI.
|
||||||
|
*
|
||||||
|
* By posting to the main looper, we ensure that this
|
||||||
|
* operation is executed after the view has been fully created
|
||||||
|
* and all initializations are completed, allowing the
|
||||||
|
* observer to properly receive and handle the state change.
|
||||||
|
*/
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
downloadsViewModel.setIsMultiDeleteState(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We have to make sure selected items are
|
||||||
|
* cleared here as well so we don't run in an
|
||||||
|
* inconsistent state where selected items do
|
||||||
|
* not match the multi delete state we are in.
|
||||||
|
*/
|
||||||
|
downloadsViewModel.clearSelectedItems()
|
||||||
|
|
||||||
val folder = arguments?.getString("folder")
|
val folder = arguments?.getString("folder")
|
||||||
val name = arguments?.getString("name")
|
val name = arguments?.getString("name")
|
||||||
if (folder == null) {
|
if (folder == null) {
|
||||||
activity?.onBackPressedDispatcher?.onBackPressed() // TODO FIX
|
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fixPaddingStatusbar(binding?.downloadChildRoot)
|
|
||||||
|
|
||||||
binding?.downloadChildToolbar?.apply {
|
binding?.downloadChildToolbar?.apply {
|
||||||
title = name
|
title = name
|
||||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
if (isLayout(PHONE or EMULATOR)) {
|
||||||
setNavigationOnClickListener {
|
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
setNavigationOnClickListener {
|
||||||
}
|
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
|
||||||
DownloadChildAdapter(
|
|
||||||
ArrayList(),
|
|
||||||
) { click ->
|
|
||||||
handleDownloadClick(click)
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadDeleteEventListener = { id: Int ->
|
|
||||||
val list = (binding?.downloadChildList?.adapter as DownloadChildAdapter?)?.cardList
|
|
||||||
if (list != null) {
|
|
||||||
if (list.any { it.data.id == id }) {
|
|
||||||
updateList(folder)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setAppBarNoScrollFlagsOnTV()
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it }
|
binding?.downloadDeleteAppbar?.setAppBarNoScrollFlagsOnTV()
|
||||||
|
|
||||||
binding?.downloadChildList?.adapter = adapter
|
observe(downloadsViewModel.childCards) {
|
||||||
binding?.downloadChildList?.setLinearListLayout(
|
if (it.isEmpty()) {
|
||||||
isHorizontal = false,
|
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||||
nextDown = FOCUS_SELF,
|
return@observe
|
||||||
nextRight = FOCUS_SELF
|
}
|
||||||
)//layoutManager = GridLayoutManager(context, 1)
|
|
||||||
|
|
||||||
updateList(folder)
|
(binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(it)
|
||||||
|
}
|
||||||
|
observe(downloadsViewModel.isMultiDeleteState) { isMultiDeleteState ->
|
||||||
|
val adapter = binding?.downloadChildList?.adapter as? DownloadAdapter
|
||||||
|
adapter?.setIsMultiDeleteState(isMultiDeleteState)
|
||||||
|
binding?.downloadDeleteAppbar?.isVisible = isMultiDeleteState
|
||||||
|
if (!isMultiDeleteState) {
|
||||||
|
detachBackPressedCallback()
|
||||||
|
downloadsViewModel.clearSelectedItems()
|
||||||
|
binding?.downloadChildToolbar?.isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
observe(downloadsViewModel.selectedBytes) {
|
||||||
|
updateDeleteButton(downloadsViewModel.selectedItemIds.value?.count() ?: 0, it)
|
||||||
|
}
|
||||||
|
observe(downloadsViewModel.selectedItemIds) {
|
||||||
|
handleSelectedChange(it)
|
||||||
|
updateDeleteButton(it.count(), downloadsViewModel.selectedBytes.value ?: 0L)
|
||||||
|
|
||||||
|
binding?.btnDelete?.isVisible = it.isNotEmpty()
|
||||||
|
binding?.selectItemsText?.isVisible = it.isEmpty()
|
||||||
|
|
||||||
|
val allSelected = downloadsViewModel.isAllSelected()
|
||||||
|
if (allSelected) {
|
||||||
|
binding?.btnToggleAll?.setText(R.string.deselect_all)
|
||||||
|
} else binding?.btnToggleAll?.setText(R.string.select_all)
|
||||||
|
}
|
||||||
|
|
||||||
|
val adapter = DownloadAdapter(
|
||||||
|
{},
|
||||||
|
{ click ->
|
||||||
|
if (click.action == DOWNLOAD_ACTION_DELETE_FILE) {
|
||||||
|
context?.let { ctx ->
|
||||||
|
downloadsViewModel.handleSingleDelete(ctx, click.data.id)
|
||||||
|
}
|
||||||
|
} else handleDownloadClick(click)
|
||||||
|
},
|
||||||
|
{ itemId, isChecked ->
|
||||||
|
if (isChecked) {
|
||||||
|
downloadsViewModel.addSelected(itemId)
|
||||||
|
} else downloadsViewModel.removeSelected(itemId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
binding?.downloadChildList?.apply {
|
||||||
|
setHasFixedSize(true)
|
||||||
|
setItemViewCacheSize(20)
|
||||||
|
this.adapter = adapter
|
||||||
|
setLinearListLayout(
|
||||||
|
isHorizontal = false,
|
||||||
|
nextRight = FOCUS_SELF,
|
||||||
|
nextDown = FOCUS_SELF,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
context?.let { downloadsViewModel.updateChildList(it, folder) }
|
||||||
|
fixPaddingStatusbar(binding?.downloadChildRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSelectedChange(selected: MutableSet<Int>) {
|
||||||
|
if (selected.isNotEmpty()) {
|
||||||
|
binding?.downloadDeleteAppbar?.isVisible = true
|
||||||
|
binding?.downloadChildToolbar?.isVisible = false
|
||||||
|
activity?.attachBackPressedCallback {
|
||||||
|
downloadsViewModel.setIsMultiDeleteState(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding?.btnDelete?.setOnClickListener {
|
||||||
|
context?.let { ctx ->
|
||||||
|
downloadsViewModel.handleMultiDelete(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding?.btnCancel?.setOnClickListener {
|
||||||
|
downloadsViewModel.setIsMultiDeleteState(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding?.btnToggleAll?.setOnClickListener {
|
||||||
|
val allSelected = downloadsViewModel.isAllSelected()
|
||||||
|
val adapter = binding?.downloadChildList?.adapter as? DownloadAdapter
|
||||||
|
if (allSelected) {
|
||||||
|
adapter?.notifySelectionStates()
|
||||||
|
downloadsViewModel.clearSelectedItems()
|
||||||
|
} else {
|
||||||
|
adapter?.notifyAllSelected()
|
||||||
|
downloadsViewModel.selectAllItems()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadsViewModel.setIsMultiDeleteState(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDeleteButton(count: Int, selectedBytes: Long) {
|
||||||
|
val formattedSize = formatShortFileSize(context, selectedBytes)
|
||||||
|
binding?.btnDelete?.text =
|
||||||
|
getString(R.string.delete_format).format(count, formattedSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,55 +1,62 @@
|
||||||
package com.lagradost.cloudstream3.ui.download
|
package com.lagradost.cloudstream3.ui.download
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.text.format.Formatter.formatShortFileSize
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.widget.doOnTextChanged
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.isMovieType
|
import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.StreamInputBinding
|
||||||
|
import com.lagradost.cloudstream3.isEpisodeBased
|
||||||
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.mvvm.observe
|
import com.lagradost.cloudstream3.mvvm.observe
|
||||||
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
||||||
|
import com.lagradost.cloudstream3.ui.player.BasicLink
|
||||||
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
||||||
import com.lagradost.cloudstream3.ui.player.LinkGenerator
|
import com.lagradost.cloudstream3.ui.player.LinkGenerator
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||||
|
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult
|
||||||
|
import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback
|
||||||
|
import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback
|
||||||
import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE
|
import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE
|
||||||
import com.lagradost.cloudstream3.utils.DataStore
|
import com.lagradost.cloudstream3.utils.DataStore.getFolderName
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
|
||||||
import android.text.format.Formatter.formatShortFileSize
|
|
||||||
import androidx.core.widget.doOnTextChanged
|
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding
|
|
||||||
import com.lagradost.cloudstream3.databinding.StreamInputBinding
|
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
|
||||||
import com.lagradost.cloudstream3.ui.player.BasicLink
|
|
||||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
|
||||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
|
|
||||||
const val DOWNLOAD_NAVIGATE_TO = "downloadpage"
|
const val DOWNLOAD_NAVIGATE_TO = "downloadpage"
|
||||||
|
|
||||||
class DownloadFragment : Fragment() {
|
class DownloadFragment : Fragment() {
|
||||||
private lateinit var downloadsViewModel: DownloadViewModel
|
private lateinit var downloadsViewModel: DownloadViewModel
|
||||||
|
private var binding: FragmentDownloadsBinding? = null
|
||||||
|
|
||||||
private fun View.setLayoutWidth(weight: Long) {
|
private fun View.setLayoutWidth(weight: Long) {
|
||||||
val param = LinearLayout.LayoutParams(
|
val param = LinearLayout.LayoutParams(
|
||||||
|
|
@ -60,221 +67,325 @@ class DownloadFragment : Fragment() {
|
||||||
this.layoutParams = param
|
this.layoutParams = param
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setList(list: List<VisualDownloadHeaderCached>) {
|
|
||||||
main {
|
|
||||||
(binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList = list
|
|
||||||
binding?.downloadList?.adapter?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
if (downloadDeleteEventListener != null) {
|
detachBackPressedCallback()
|
||||||
VideoDownloadManager.downloadDeleteEvent -= downloadDeleteEventListener!!
|
|
||||||
downloadDeleteEventListener = null
|
|
||||||
}
|
|
||||||
binding = null
|
binding = null
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
var binding: FragmentDownloadsBinding? = null
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View {
|
||||||
downloadsViewModel =
|
downloadsViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
|
||||||
ViewModelProvider(this)[DownloadViewModel::class.java]
|
|
||||||
|
|
||||||
val localBinding = FragmentDownloadsBinding.inflate(inflater, container, false)
|
val localBinding = FragmentDownloadsBinding.inflate(inflater, container, false)
|
||||||
binding = localBinding
|
binding = localBinding
|
||||||
return localBinding.root//inflater.inflate(R.layout.fragment_downloads, container, false)
|
return localBinding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
private var downloadDeleteEventListener: ((Int) -> Unit)? = null
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
|
binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV()
|
||||||
|
binding?.downloadDeleteAppbar?.setAppBarNoScrollFlagsOnTV()
|
||||||
|
|
||||||
observe(downloadsViewModel.noDownloadsText) {
|
/**
|
||||||
binding?.textNoDownloads?.text = it
|
* We never want to retain multi-delete state
|
||||||
|
* when navigating to downloads. Setting this state
|
||||||
|
* immediately can sometimes result in the observer
|
||||||
|
* not being notified in time to update the UI.
|
||||||
|
*
|
||||||
|
* By posting to the main looper, we ensure that this
|
||||||
|
* operation is executed after the view has been fully created
|
||||||
|
* and all initializations are completed, allowing the
|
||||||
|
* observer to properly receive and handle the state change.
|
||||||
|
*/
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
downloadsViewModel.setIsMultiDeleteState(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We have to make sure selected items are
|
||||||
|
* cleared here as well so we don't run in an
|
||||||
|
* inconsistent state where selected items do
|
||||||
|
* not match the multi delete state we are in.
|
||||||
|
*/
|
||||||
|
downloadsViewModel.clearSelectedItems()
|
||||||
|
|
||||||
observe(downloadsViewModel.headerCards) {
|
observe(downloadsViewModel.headerCards) {
|
||||||
setList(it)
|
(binding?.downloadList?.adapter as? DownloadAdapter)?.submitList(it)
|
||||||
binding?.downloadLoading?.isVisible = false
|
binding?.downloadLoading?.isVisible = false
|
||||||
|
binding?.textNoDownloads?.isVisible = it.isEmpty()
|
||||||
}
|
}
|
||||||
observe(downloadsViewModel.availableBytes) {
|
observe(downloadsViewModel.availableBytes) {
|
||||||
binding?.downloadFreeTxt?.text =
|
updateStorageInfo(
|
||||||
getString(R.string.storage_size_format).format(
|
view.context,
|
||||||
getString(R.string.free_storage),
|
it,
|
||||||
formatShortFileSize(view.context, it)
|
R.string.free_storage,
|
||||||
)
|
binding?.downloadFreeTxt,
|
||||||
binding?.downloadFree?.setLayoutWidth(it)
|
binding?.downloadFree
|
||||||
|
)
|
||||||
}
|
}
|
||||||
observe(downloadsViewModel.usedBytes) {
|
observe(downloadsViewModel.usedBytes) {
|
||||||
binding?.apply {
|
updateStorageInfo(
|
||||||
downloadUsedTxt.text =
|
view.context,
|
||||||
getString(R.string.storage_size_format).format(
|
it,
|
||||||
getString(R.string.used_storage),
|
R.string.used_storage,
|
||||||
formatShortFileSize(view.context, it)
|
binding?.downloadUsedTxt,
|
||||||
)
|
binding?.downloadUsed
|
||||||
downloadUsed.setLayoutWidth(it)
|
|
||||||
downloadStorageAppbar.isVisible = it > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
observe(downloadsViewModel.downloadBytes) {
|
|
||||||
binding?.apply {
|
|
||||||
downloadAppTxt.text =
|
|
||||||
getString(R.string.storage_size_format).format(
|
|
||||||
getString(R.string.app_storage),
|
|
||||||
formatShortFileSize(view.context, it)
|
|
||||||
)
|
|
||||||
downloadApp.setLayoutWidth(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
|
||||||
DownloadHeaderAdapter(
|
|
||||||
ArrayList(),
|
|
||||||
{ click ->
|
|
||||||
when (click.action) {
|
|
||||||
0 -> {
|
|
||||||
if (click.data.type.isMovieType()) {
|
|
||||||
//wont be called
|
|
||||||
} else {
|
|
||||||
val folder = DataStore.getFolderName(
|
|
||||||
DOWNLOAD_EPISODE_CACHE,
|
|
||||||
click.data.id.toString()
|
|
||||||
)
|
|
||||||
activity?.navigate(
|
|
||||||
R.id.action_navigation_downloads_to_navigation_download_child,
|
|
||||||
DownloadChildFragment.newInstance(click.data.name, folder)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
1 -> {
|
|
||||||
(activity as AppCompatActivity?)?.loadResult(
|
|
||||||
click.data.url,
|
|
||||||
click.data.apiName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
{ downloadClickEvent ->
|
|
||||||
if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter
|
|
||||||
handleDownloadClick(downloadClickEvent)
|
|
||||||
if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) {
|
|
||||||
context?.let { ctx ->
|
|
||||||
downloadsViewModel.updateList(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
downloadDeleteEventListener = { id ->
|
// Prevent race condition and make sure
|
||||||
val list = (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList
|
// we don't display it early
|
||||||
if (list != null) {
|
if (
|
||||||
if (list.any { it.data.id == id }) {
|
downloadsViewModel.isMultiDeleteState.value == null ||
|
||||||
context?.let { ctx ->
|
downloadsViewModel.isMultiDeleteState.value == false
|
||||||
setList(ArrayList())
|
) binding?.downloadStorageAppbar?.isVisible = it > 0
|
||||||
downloadsViewModel.updateList(ctx)
|
}
|
||||||
}
|
observe(downloadsViewModel.downloadBytes) {
|
||||||
|
updateStorageInfo(
|
||||||
|
view.context,
|
||||||
|
it,
|
||||||
|
R.string.app_storage,
|
||||||
|
binding?.downloadAppTxt,
|
||||||
|
binding?.downloadApp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
observe(downloadsViewModel.selectedBytes) {
|
||||||
|
updateDeleteButton(downloadsViewModel.selectedItemIds.value?.count() ?: 0, it)
|
||||||
|
}
|
||||||
|
observe(downloadsViewModel.isMultiDeleteState) { isMultiDeleteState ->
|
||||||
|
val adapter = binding?.downloadList?.adapter as? DownloadAdapter
|
||||||
|
adapter?.setIsMultiDeleteState(isMultiDeleteState)
|
||||||
|
binding?.downloadDeleteAppbar?.isVisible = isMultiDeleteState
|
||||||
|
if (!isMultiDeleteState) {
|
||||||
|
detachBackPressedCallback()
|
||||||
|
downloadsViewModel.clearSelectedItems()
|
||||||
|
// Prevent race condition and make sure
|
||||||
|
// we don't display it early
|
||||||
|
if (downloadsViewModel.usedBytes.value?.let { it > 0 } == true) {
|
||||||
|
binding?.downloadStorageAppbar?.isVisible = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
observe(downloadsViewModel.selectedItemIds) {
|
||||||
|
handleSelectedChange(it)
|
||||||
|
updateDeleteButton(it.count(), downloadsViewModel.selectedBytes.value ?: 0L)
|
||||||
|
|
||||||
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it }
|
binding?.btnDelete?.isVisible = it.isNotEmpty()
|
||||||
|
binding?.selectItemsText?.isVisible = it.isEmpty()
|
||||||
|
|
||||||
|
val allSelected = downloadsViewModel.isAllSelected()
|
||||||
|
if (allSelected) {
|
||||||
|
binding?.btnToggleAll?.setText(R.string.deselect_all)
|
||||||
|
} else binding?.btnToggleAll?.setText(R.string.select_all)
|
||||||
|
}
|
||||||
|
|
||||||
|
val adapter = DownloadAdapter(
|
||||||
|
{ click -> handleItemClick(click) },
|
||||||
|
{ click ->
|
||||||
|
if (click.action == DOWNLOAD_ACTION_DELETE_FILE) {
|
||||||
|
context?.let { ctx ->
|
||||||
|
downloadsViewModel.handleSingleDelete(ctx, click.data.id)
|
||||||
|
}
|
||||||
|
} else handleDownloadClick(click)
|
||||||
|
},
|
||||||
|
{ itemId, isChecked ->
|
||||||
|
if (isChecked) {
|
||||||
|
downloadsViewModel.addSelected(itemId)
|
||||||
|
} else downloadsViewModel.removeSelected(itemId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
binding?.downloadList?.apply {
|
binding?.downloadList?.apply {
|
||||||
|
setHasFixedSize(true)
|
||||||
|
setItemViewCacheSize(20)
|
||||||
this.adapter = adapter
|
this.adapter = adapter
|
||||||
setLinearListLayout(
|
setLinearListLayout(
|
||||||
isHorizontal = false,
|
isHorizontal = false,
|
||||||
nextRight = FOCUS_SELF,
|
nextRight = FOCUS_SELF,
|
||||||
nextUp = FOCUS_SELF,
|
nextDown = FOCUS_SELF,
|
||||||
nextDown = FOCUS_SELF
|
|
||||||
)
|
)
|
||||||
//layoutManager = GridLayoutManager(context, 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should be visible in emulator layout
|
binding?.apply {
|
||||||
binding?.downloadStreamButton?.isGone = isTrueTvSettings()
|
openLocalVideoButton.apply {
|
||||||
binding?.downloadStreamButton?.setOnClickListener {
|
isGone = isLayout(TV)
|
||||||
val dialog =
|
setOnClickListener { openLocalVideo() }
|
||||||
Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom)
|
|
||||||
|
|
||||||
val binding = StreamInputBinding.inflate(dialog.layoutInflater)
|
|
||||||
|
|
||||||
dialog.setContentView(binding.root)
|
|
||||||
|
|
||||||
dialog.show()
|
|
||||||
|
|
||||||
// If user has clicked the switch do not interfere
|
|
||||||
var preventAutoSwitching = false
|
|
||||||
binding.hlsSwitch.setOnClickListener {
|
|
||||||
preventAutoSwitching = true
|
|
||||||
}
|
}
|
||||||
|
downloadStreamButton.apply {
|
||||||
fun activateSwitchOnHls(text: String?) {
|
isGone = isLayout(TV)
|
||||||
binding.hlsSwitch.isChecked = normalSafeApiCall {
|
setOnClickListener { showStreamInputDialog(it.context) }
|
||||||
URI(text).path?.substringAfterLast(".")?.contains("m3u")
|
|
||||||
} == true
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
binding.streamReferer.doOnTextChanged { text, _, _, _ ->
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (!preventAutoSwitching)
|
binding?.downloadList?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
||||||
activateSwitchOnHls(text?.toString())
|
handleScroll(scrollY - oldScrollY)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)?.primaryClip?.getItemAt(
|
context?.let { downloadsViewModel.updateHeaderList(it) }
|
||||||
0
|
fixPaddingStatusbar(binding?.downloadRoot)
|
||||||
)?.text?.toString()?.let { copy ->
|
}
|
||||||
val fixedText = copy.trim()
|
|
||||||
binding.streamUrl.setText(fixedText)
|
|
||||||
activateSwitchOnHls(fixedText)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.applyBtt.setOnClickListener {
|
|
||||||
val url = binding.streamUrl.text?.toString()
|
|
||||||
if (url.isNullOrEmpty()) {
|
|
||||||
showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT)
|
|
||||||
} else {
|
|
||||||
val referer = binding.streamReferer.text?.toString()
|
|
||||||
|
|
||||||
|
private fun handleItemClick(click: DownloadHeaderClickEvent) {
|
||||||
|
when (click.action) {
|
||||||
|
DOWNLOAD_ACTION_GO_TO_CHILD -> {
|
||||||
|
if (click.data.type.isEpisodeBased()) {
|
||||||
|
val folder =
|
||||||
|
getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString())
|
||||||
activity?.navigate(
|
activity?.navigate(
|
||||||
R.id.global_to_navigation_player,
|
R.id.action_navigation_downloads_to_navigation_download_child,
|
||||||
GeneratorPlayer.newInstance(
|
DownloadChildFragment.newInstance(click.data.name, folder)
|
||||||
LinkGenerator(
|
|
||||||
listOf(BasicLink(url)),
|
|
||||||
extract = true,
|
|
||||||
referer = referer,
|
|
||||||
isM3u8 = binding.hlsSwitch.isChecked
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
dialog.dismissSafe(activity)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.cancelBtt.setOnClickListener {
|
DOWNLOAD_ACTION_LOAD_RESULT -> {
|
||||||
|
activity?.loadResult(click.data.url, click.data.apiName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSelectedChange(selected: MutableSet<Int>) {
|
||||||
|
if (selected.isNotEmpty()) {
|
||||||
|
binding?.downloadDeleteAppbar?.isVisible = true
|
||||||
|
binding?.downloadStorageAppbar?.isVisible = false
|
||||||
|
activity?.attachBackPressedCallback {
|
||||||
|
downloadsViewModel.setIsMultiDeleteState(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding?.btnDelete?.setOnClickListener {
|
||||||
|
context?.let { ctx ->
|
||||||
|
downloadsViewModel.handleMultiDelete(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding?.btnCancel?.setOnClickListener {
|
||||||
|
downloadsViewModel.setIsMultiDeleteState(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding?.btnToggleAll?.setOnClickListener {
|
||||||
|
val allSelected = downloadsViewModel.isAllSelected()
|
||||||
|
val adapter = binding?.downloadList?.adapter as? DownloadAdapter
|
||||||
|
if (allSelected) {
|
||||||
|
adapter?.notifySelectionStates()
|
||||||
|
downloadsViewModel.clearSelectedItems()
|
||||||
|
} else {
|
||||||
|
adapter?.notifyAllSelected()
|
||||||
|
downloadsViewModel.selectAllItems()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadsViewModel.setIsMultiDeleteState(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDeleteButton(count: Int, selectedBytes: Long) {
|
||||||
|
val formattedSize = formatShortFileSize(context, selectedBytes)
|
||||||
|
binding?.btnDelete?.text =
|
||||||
|
getString(R.string.delete_format).format(count, formattedSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateStorageInfo(
|
||||||
|
context: Context,
|
||||||
|
bytes: Long,
|
||||||
|
@StringRes stringRes: Int,
|
||||||
|
textView: TextView?,
|
||||||
|
view: View?
|
||||||
|
) {
|
||||||
|
textView?.text = getString(R.string.storage_size_format).format(
|
||||||
|
getString(stringRes),
|
||||||
|
formatShortFileSize(context, bytes)
|
||||||
|
)
|
||||||
|
view?.setLayoutWidth(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openLocalVideo() {
|
||||||
|
val intent = Intent()
|
||||||
|
.setAction(Intent.ACTION_GET_CONTENT)
|
||||||
|
.setType("video/*")
|
||||||
|
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
.addFlags(FLAG_GRANT_READ_URI_PERMISSION) // Request temporary access
|
||||||
|
normalSafeApiCall {
|
||||||
|
videoResultLauncher.launch(
|
||||||
|
Intent.createChooser(
|
||||||
|
intent,
|
||||||
|
getString(R.string.open_local_video)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showStreamInputDialog(context: Context) {
|
||||||
|
val dialog = Dialog(context, R.style.AlertDialogCustom)
|
||||||
|
val binding = StreamInputBinding.inflate(dialog.layoutInflater)
|
||||||
|
dialog.setContentView(binding.root)
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
var preventAutoSwitching = false
|
||||||
|
binding.hlsSwitch.setOnClickListener { preventAutoSwitching = true }
|
||||||
|
|
||||||
|
binding.streamReferer.doOnTextChanged { text, _, _, _ ->
|
||||||
|
if (!preventAutoSwitching) activateSwitchOnHls(text?.toString(), binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt(
|
||||||
|
0
|
||||||
|
)?.text?.toString()?.let { copy ->
|
||||||
|
val fixedText = copy.trim()
|
||||||
|
binding.streamUrl.setText(fixedText)
|
||||||
|
activateSwitchOnHls(fixedText, binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.applyBtt.setOnClickListener {
|
||||||
|
val url = binding.streamUrl.text?.toString()
|
||||||
|
if (url.isNullOrEmpty()) {
|
||||||
|
showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT)
|
||||||
|
} else {
|
||||||
|
val referer = binding.streamReferer.text?.toString()
|
||||||
|
activity?.navigate(
|
||||||
|
R.id.global_to_navigation_player,
|
||||||
|
GeneratorPlayer.newInstance(
|
||||||
|
LinkGenerator(
|
||||||
|
listOf(BasicLink(url)),
|
||||||
|
extract = true,
|
||||||
|
referer = referer,
|
||||||
|
isM3u8 = binding.hlsSwitch.isChecked
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
dialog.dismissSafe(activity)
|
dialog.dismissSafe(activity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
binding?.downloadList?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
|
||||||
val dy = scrollY - oldScrollY
|
|
||||||
if (dy > 0) { //check for scroll down
|
|
||||||
binding?.downloadStreamButton?.shrink() // hide
|
|
||||||
} else if (dy < -5) {
|
|
||||||
binding?.downloadStreamButton?.extend() // show
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
downloadsViewModel.updateList(requireContext())
|
|
||||||
|
|
||||||
fixPaddingStatusbar(binding?.downloadRoot)
|
binding.cancelBtt.setOnClickListener {
|
||||||
|
dialog.dismissSafe(activity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun activateSwitchOnHls(text: String?, binding: StreamInputBinding) {
|
||||||
|
binding.hlsSwitch.isChecked = normalSafeApiCall {
|
||||||
|
URI(text).path?.substringAfterLast(".")?.contains("m3u")
|
||||||
|
} == true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleScroll(dy: Int) {
|
||||||
|
if (dy > 0) {
|
||||||
|
binding?.downloadStreamButton?.shrink()
|
||||||
|
} else if (dy < -5) {
|
||||||
|
binding?.downloadStreamButton?.extend()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open local video from files using content provider x safeFile
|
||||||
|
private val videoResultLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||||
|
val selectedVideoUri = result?.data?.data ?: return@registerForActivityResult
|
||||||
|
playUri(activity ?: return@registerForActivityResult, selectedVideoUri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.ui.download
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.text.format.Formatter.formatShortFileSize
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.lagradost.cloudstream3.R
|
|
||||||
import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding
|
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
data class VisualDownloadHeaderCached(
|
|
||||||
val currentOngoingDownloads: Int,
|
|
||||||
val totalDownloads: Int,
|
|
||||||
val totalBytes: Long,
|
|
||||||
val currentBytes: Long,
|
|
||||||
val data: VideoDownloadHelper.DownloadHeaderCached,
|
|
||||||
val child: VideoDownloadHelper.DownloadEpisodeCached?,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class DownloadHeaderClickEvent(
|
|
||||||
val action: Int,
|
|
||||||
val data: VideoDownloadHelper.DownloadHeaderCached
|
|
||||||
)
|
|
||||||
|
|
||||||
class DownloadHeaderAdapter(
|
|
||||||
var cardList: List<VisualDownloadHeaderCached>,
|
|
||||||
private val clickCallback: (DownloadHeaderClickEvent) -> Unit,
|
|
||||||
private val movieClickCallback: (DownloadClickEvent) -> Unit,
|
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
|
||||||
return DownloadHeaderViewHolder(
|
|
||||||
DownloadHeaderEpisodeBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
),
|
|
||||||
clickCallback,
|
|
||||||
movieClickCallback
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
|
||||||
when (holder) {
|
|
||||||
is DownloadHeaderViewHolder -> {
|
|
||||||
holder.bind(cardList[position])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return cardList.size
|
|
||||||
}
|
|
||||||
|
|
||||||
class DownloadHeaderViewHolder
|
|
||||||
constructor(
|
|
||||||
val binding: DownloadHeaderEpisodeBinding,
|
|
||||||
private val clickCallback: (DownloadHeaderClickEvent) -> Unit,
|
|
||||||
private val movieClickCallback: (DownloadClickEvent) -> Unit,
|
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
/*private val poster: ImageView? = itemView.download_header_poster
|
|
||||||
private val title: TextView = itemView.download_header_title
|
|
||||||
private val extraInfo: TextView = itemView.download_header_info
|
|
||||||
private val holder: CardView = itemView.episode_holder
|
|
||||||
|
|
||||||
private val downloadBar: ContentLoadingProgressBar = itemView.download_header_progress_downloaded
|
|
||||||
private val downloadImage: ImageView = itemView.download_header_episode_download
|
|
||||||
private val normalImage: ImageView = itemView.download_header_goto_child*/
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
fun bind(card: VisualDownloadHeaderCached) {
|
|
||||||
val d = card.data
|
|
||||||
|
|
||||||
binding.downloadHeaderPoster.apply {
|
|
||||||
setImage(d.poster)
|
|
||||||
setOnClickListener {
|
|
||||||
clickCallback.invoke(DownloadHeaderClickEvent(1, d))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.apply {
|
|
||||||
|
|
||||||
binding.downloadHeaderTitle.text = d.name
|
|
||||||
val mbString = formatShortFileSize(itemView.context, card.totalBytes)
|
|
||||||
|
|
||||||
//val isMovie = d.type.isMovieType()
|
|
||||||
if (card.child != null) {
|
|
||||||
//downloadHeaderProgressDownloaded.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
// downloadHeaderEpisodeDownload.visibility = View.VISIBLE
|
|
||||||
binding.downloadHeaderGotoChild.visibility = View.GONE
|
|
||||||
|
|
||||||
downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, movieClickCallback)
|
|
||||||
downloadButton.isVisible = true
|
|
||||||
/*setUpButton(
|
|
||||||
card.currentBytes,
|
|
||||||
card.totalBytes,
|
|
||||||
downloadBar,
|
|
||||||
downloadImage,
|
|
||||||
extraInfo,
|
|
||||||
card.child,
|
|
||||||
movieClickCallback
|
|
||||||
)*/
|
|
||||||
|
|
||||||
episodeHolder.setOnClickListener {
|
|
||||||
movieClickCallback.invoke(
|
|
||||||
DownloadClickEvent(
|
|
||||||
DOWNLOAD_ACTION_PLAY_FILE,
|
|
||||||
card.child
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
downloadButton.isVisible = false
|
|
||||||
// downloadHeaderProgressDownloaded.visibility = View.GONE
|
|
||||||
// downloadHeaderEpisodeDownload.visibility = View.GONE
|
|
||||||
binding.downloadHeaderGotoChild.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
try {
|
|
||||||
downloadHeaderInfo.text =
|
|
||||||
downloadHeaderInfo.context.getString(R.string.extra_info_format).format(
|
|
||||||
card.totalDownloads,
|
|
||||||
if (card.totalDownloads == 1) downloadHeaderInfo.context.getString(R.string.episode) else downloadHeaderInfo.context.getString(
|
|
||||||
R.string.episodes
|
|
||||||
),
|
|
||||||
mbString
|
|
||||||
)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
// you probably formatted incorrectly
|
|
||||||
downloadHeaderInfo.text = "Error"
|
|
||||||
logError(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
episodeHolder.setOnClickListener {
|
|
||||||
clickCallback.invoke(DownloadHeaderClickEvent(0, d))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,122 +1,439 @@
|
||||||
package com.lagradost.cloudstream3.ui.download
|
package com.lagradost.cloudstream3.ui.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.StatFs
|
import android.os.StatFs
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.lagradost.cloudstream3.isMovieType
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.isEpisodeBased
|
||||||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
|
||||||
import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE
|
import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE
|
||||||
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
|
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getFolderName
|
import com.lagradost.cloudstream3.utils.DataStore.getFolderName
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.deleteFilesAndUpdateSettings
|
||||||
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadFileInfoAndUpdateSettings
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class DownloadViewModel : ViewModel() {
|
class DownloadViewModel : ViewModel() {
|
||||||
private val _noDownloadsText = MutableLiveData<String>().apply {
|
|
||||||
value = ""
|
|
||||||
}
|
|
||||||
val noDownloadsText: LiveData<String> = _noDownloadsText
|
|
||||||
|
|
||||||
private val _headerCards =
|
private val _headerCards = MutableLiveData<List<VisualDownloadCached.Header>>()
|
||||||
MutableLiveData<List<VisualDownloadHeaderCached>>().apply { listOf<VisualDownloadHeaderCached>() }
|
val headerCards: LiveData<List<VisualDownloadCached.Header>> = _headerCards
|
||||||
val headerCards: LiveData<List<VisualDownloadHeaderCached>> = _headerCards
|
|
||||||
|
private val _childCards = MutableLiveData<List<VisualDownloadCached.Child>>()
|
||||||
|
val childCards: LiveData<List<VisualDownloadCached.Child>> = _childCards
|
||||||
|
|
||||||
private val _usedBytes = MutableLiveData<Long>()
|
private val _usedBytes = MutableLiveData<Long>()
|
||||||
private val _availableBytes = MutableLiveData<Long>()
|
|
||||||
private val _downloadBytes = MutableLiveData<Long>()
|
|
||||||
|
|
||||||
val usedBytes: LiveData<Long> = _usedBytes
|
val usedBytes: LiveData<Long> = _usedBytes
|
||||||
|
|
||||||
|
private val _availableBytes = MutableLiveData<Long>()
|
||||||
val availableBytes: LiveData<Long> = _availableBytes
|
val availableBytes: LiveData<Long> = _availableBytes
|
||||||
|
|
||||||
|
private val _downloadBytes = MutableLiveData<Long>()
|
||||||
val downloadBytes: LiveData<Long> = _downloadBytes
|
val downloadBytes: LiveData<Long> = _downloadBytes
|
||||||
|
|
||||||
fun updateList(context: Context) = viewModelScope.launchSafe {
|
private val _selectedBytes = MutableLiveData<Long>(0)
|
||||||
val children = withContext(Dispatchers.IO) {
|
val selectedBytes: LiveData<Long> = _selectedBytes
|
||||||
val headers = context.getKeys(DOWNLOAD_EPISODE_CACHE)
|
|
||||||
headers.mapNotNull { context.getKey<VideoDownloadHelper.DownloadEpisodeCached>(it) }
|
|
||||||
.distinctBy { it.id } // Remove duplicates
|
|
||||||
}
|
|
||||||
|
|
||||||
// parentId : bytes
|
private val _isMultiDeleteState = MutableLiveData(false)
|
||||||
val totalBytesUsedByChild = HashMap<Int, Long>()
|
val isMultiDeleteState: LiveData<Boolean> = _isMultiDeleteState
|
||||||
// parentId : bytes
|
|
||||||
val currentBytesUsedByChild = HashMap<Int, Long>()
|
|
||||||
// parentId : downloadsCount
|
|
||||||
val totalDownloads = HashMap<Int, Int>()
|
|
||||||
|
|
||||||
|
private val _selectedItemIds = MutableLiveData<MutableSet<Int>>(mutableSetOf())
|
||||||
|
val selectedItemIds: LiveData<MutableSet<Int>> = _selectedItemIds
|
||||||
|
|
||||||
// Gets all children downloads
|
private var previousVisual: List<VisualDownloadCached>? = null
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
for (c in children) {
|
|
||||||
val childFile = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(context, c.id) ?: continue
|
|
||||||
|
|
||||||
if (childFile.fileLength <= 1) continue
|
fun setIsMultiDeleteState(value: Boolean) {
|
||||||
val len = childFile.totalBytes
|
_isMultiDeleteState.postValue(value)
|
||||||
val flen = childFile.fileLength
|
}
|
||||||
|
|
||||||
totalBytesUsedByChild[c.parentId] = totalBytesUsedByChild[c.parentId]?.plus(len) ?: len
|
fun addSelected(itemId: Int) {
|
||||||
currentBytesUsedByChild[c.parentId] = currentBytesUsedByChild[c.parentId]?.plus(flen) ?: flen
|
updateSelectedItems { it.add(itemId) }
|
||||||
totalDownloads[c.parentId] = totalDownloads[c.parentId]?.plus(1) ?: 1
|
}
|
||||||
|
|
||||||
|
fun removeSelected(itemId: Int) {
|
||||||
|
updateSelectedItems { it.remove(itemId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectAllItems() {
|
||||||
|
val items = headerCards.value.orEmpty() + childCards.value.orEmpty()
|
||||||
|
updateSelectedItems { it.addAll(items.map { item -> item.data.id }) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearSelectedItems() {
|
||||||
|
// We need this to be done immediately
|
||||||
|
// so we can't use postValue
|
||||||
|
_selectedItemIds.value = mutableSetOf()
|
||||||
|
updateSelectedItems { it.clear() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isAllSelected(): Boolean {
|
||||||
|
val currentSelected = selectedItemIds.value ?: return false
|
||||||
|
val items = headerCards.value.orEmpty() + childCards.value.orEmpty()
|
||||||
|
return items.count() == currentSelected.count() && items.all { it.data.id in currentSelected }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSelectedItems(action: (MutableSet<Int>) -> Unit) {
|
||||||
|
val currentSelected = selectedItemIds.value ?: mutableSetOf()
|
||||||
|
action(currentSelected)
|
||||||
|
_selectedItemIds.postValue(currentSelected)
|
||||||
|
updateSelectedBytes()
|
||||||
|
updateSelectedCards()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSelectedBytes() = viewModelScope.launchSafe {
|
||||||
|
val selectedItemsList = getSelectedItemsData() ?: return@launchSafe
|
||||||
|
val totalSelectedBytes = selectedItemsList.sumOf { it.totalBytes }
|
||||||
|
_selectedBytes.postValue(totalSelectedBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSelectedCards() = viewModelScope.launchSafe {
|
||||||
|
val currentSelected = selectedItemIds.value ?: return@launchSafe
|
||||||
|
|
||||||
|
headerCards.value?.let { headers ->
|
||||||
|
headers.forEach { header ->
|
||||||
|
header.isSelected = header.data.id in currentSelected
|
||||||
}
|
}
|
||||||
|
_headerCards.postValue(headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
val cached = withContext(Dispatchers.IO) { // wont fetch useless keys
|
childCards.value?.let { children ->
|
||||||
totalDownloads.entries.filter { it.value > 0 }.mapNotNull {
|
children.forEach { child ->
|
||||||
context.getKey<VideoDownloadHelper.DownloadHeaderCached>(
|
child.isSelected = child.data.id in currentSelected
|
||||||
DOWNLOAD_HEADER_CACHE,
|
|
||||||
it.key.toString()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
_childCards.postValue(children)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateHeaderList(context: Context) = viewModelScope.launchSafe {
|
||||||
val visual = withContext(Dispatchers.IO) {
|
val visual = withContext(Dispatchers.IO) {
|
||||||
cached.mapNotNull { // TODO FIX
|
val children = context.getKeys(DOWNLOAD_EPISODE_CACHE)
|
||||||
val downloads = totalDownloads[it.id] ?: 0
|
.mapNotNull { context.getKey<VideoDownloadHelper.DownloadEpisodeCached>(it) }
|
||||||
val bytes = totalBytesUsedByChild[it.id] ?: 0
|
.distinctBy { it.id } // Remove duplicates
|
||||||
val currentBytes = currentBytesUsedByChild[it.id] ?: 0
|
|
||||||
if (bytes <= 0 || downloads <= 0) return@mapNotNull null
|
val (totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads) =
|
||||||
val movieEpisode =
|
calculateDownloadStats(context, children)
|
||||||
if (!it.type.isMovieType()) null
|
|
||||||
else context.getKey<VideoDownloadHelper.DownloadEpisodeCached>(
|
val cached = context.getKeys(DOWNLOAD_HEADER_CACHE)
|
||||||
DOWNLOAD_EPISODE_CACHE,
|
.mapNotNull { context.getKey<VideoDownloadHelper.DownloadHeaderCached>(it) }
|
||||||
getFolderName(it.id.toString(), it.id.toString())
|
|
||||||
)
|
createVisualDownloadList(
|
||||||
VisualDownloadHeaderCached(
|
context, cached, totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads
|
||||||
0,
|
)
|
||||||
downloads,
|
|
||||||
bytes,
|
|
||||||
currentBytes,
|
|
||||||
it,
|
|
||||||
movieEpisode
|
|
||||||
)
|
|
||||||
}.sortedBy {
|
|
||||||
(it.child?.episode ?: 0) + (it.child?.season?.times(10000) ?: 0)
|
|
||||||
} // episode sorting by episode, lowest to highest
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (visual != previousVisual) {
|
||||||
|
previousVisual = visual
|
||||||
|
updateStorageStats(visual)
|
||||||
|
_headerCards.postValue(visual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateDownloadStats(
|
||||||
|
context: Context,
|
||||||
|
children: List<VideoDownloadHelper.DownloadEpisodeCached>
|
||||||
|
): Triple<Map<Int, Long>, Map<Int, Long>, Map<Int, Int>> {
|
||||||
|
// parentId : bytes
|
||||||
|
val totalBytesUsedByChild = mutableMapOf<Int, Long>()
|
||||||
|
// parentId : bytes
|
||||||
|
val currentBytesUsedByChild = mutableMapOf<Int, Long>()
|
||||||
|
// parentId : downloadsCount
|
||||||
|
val totalDownloads = mutableMapOf<Int, Int>()
|
||||||
|
|
||||||
|
children.forEach { child ->
|
||||||
|
val childFile = getDownloadFileInfoAndUpdateSettings(context, child.id) ?: return@forEach
|
||||||
|
if (childFile.fileLength <= 1) return@forEach
|
||||||
|
|
||||||
|
val len = childFile.totalBytes
|
||||||
|
val flen = childFile.fileLength
|
||||||
|
|
||||||
|
totalBytesUsedByChild.merge(child.parentId, len, Long::plus)
|
||||||
|
currentBytesUsedByChild.merge(child.parentId, flen, Long::plus)
|
||||||
|
totalDownloads.merge(child.parentId, 1, Int::plus)
|
||||||
|
}
|
||||||
|
return Triple(totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createVisualDownloadList(
|
||||||
|
context: Context,
|
||||||
|
cached: List<VideoDownloadHelper.DownloadHeaderCached>,
|
||||||
|
totalBytesUsedByChild: Map<Int, Long>,
|
||||||
|
currentBytesUsedByChild: Map<Int, Long>,
|
||||||
|
totalDownloads: Map<Int, Int>
|
||||||
|
): List<VisualDownloadCached.Header> {
|
||||||
|
return cached.mapNotNull {
|
||||||
|
val downloads = totalDownloads[it.id] ?: 0
|
||||||
|
val bytes = totalBytesUsedByChild[it.id] ?: 0
|
||||||
|
val currentBytes = currentBytesUsedByChild[it.id] ?: 0
|
||||||
|
if (bytes <= 0 || downloads <= 0) return@mapNotNull null
|
||||||
|
|
||||||
|
val isSelected = selectedItemIds.value?.contains(it.id) ?: false
|
||||||
|
val movieEpisode = if (it.type.isEpisodeBased()) null else context.getKey<VideoDownloadHelper.DownloadEpisodeCached>(
|
||||||
|
DOWNLOAD_EPISODE_CACHE,
|
||||||
|
getFolderName(it.id.toString(), it.id.toString())
|
||||||
|
)
|
||||||
|
|
||||||
|
VisualDownloadCached.Header(
|
||||||
|
currentBytes = currentBytes,
|
||||||
|
totalBytes = bytes,
|
||||||
|
data = it,
|
||||||
|
child = movieEpisode,
|
||||||
|
currentOngoingDownloads = 0,
|
||||||
|
totalDownloads = downloads,
|
||||||
|
isSelected = isSelected,
|
||||||
|
)
|
||||||
|
// Prevent order being almost completely random,
|
||||||
|
// making things difficult to find.
|
||||||
|
}.sortedWith(compareBy<VisualDownloadCached.Header> {
|
||||||
|
// Sort by isEpisodeBased() ascending. We put those that
|
||||||
|
// are episode based at the bottom for UI purposes and to
|
||||||
|
// make it easier to find by grouping them together.
|
||||||
|
it.data.type.isEpisodeBased()
|
||||||
|
}.thenBy {
|
||||||
|
// Then we sort alphabetically by name (case-insensitive).
|
||||||
|
// Again, we do this to make things easier to find.
|
||||||
|
it.data.name.lowercase()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateChildList(context: Context, folder: String) = viewModelScope.launchSafe {
|
||||||
|
val visual = withContext(Dispatchers.IO) {
|
||||||
|
context.getKeys(folder).mapNotNull { key ->
|
||||||
|
context.getKey<VideoDownloadHelper.DownloadEpisodeCached>(key)
|
||||||
|
}.mapNotNull {
|
||||||
|
val isSelected = selectedItemIds.value?.contains(it.id) ?: false
|
||||||
|
val info = getDownloadFileInfoAndUpdateSettings(context, it.id) ?: return@mapNotNull null
|
||||||
|
VisualDownloadCached.Child(
|
||||||
|
currentBytes = info.fileLength,
|
||||||
|
totalBytes = info.totalBytes,
|
||||||
|
isSelected = isSelected,
|
||||||
|
data = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.sortedWith(compareBy(
|
||||||
|
// Sort by season first, and then by episode number,
|
||||||
|
// to ensure sorting is consistent.
|
||||||
|
{ it.data.season ?: 0 },
|
||||||
|
{ it.data.episode }
|
||||||
|
))
|
||||||
|
|
||||||
|
if (previousVisual != visual) {
|
||||||
|
previousVisual = visual
|
||||||
|
_childCards.postValue(visual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeItems(idsToRemove: Set<Int>) = viewModelScope.launchSafe {
|
||||||
|
val updatedHeaders = headerCards.value.orEmpty().filter { it.data.id !in idsToRemove }
|
||||||
|
val updatedChildren = childCards.value.orEmpty().filter { it.data.id !in idsToRemove }
|
||||||
|
_headerCards.postValue(updatedHeaders)
|
||||||
|
_childCards.postValue(updatedChildren)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateStorageStats(visual: List<VisualDownloadCached.Header>) {
|
||||||
try {
|
try {
|
||||||
val stat = StatFs(Environment.getExternalStorageDirectory().path)
|
val stat = StatFs(Environment.getExternalStorageDirectory().path)
|
||||||
|
val localBytesAvailable = stat.availableBytes
|
||||||
val localBytesAvailable = stat.availableBytes//stat.blockSizeLong * stat.blockCountLong
|
|
||||||
val localTotalBytes = stat.blockSizeLong * stat.blockCountLong
|
val localTotalBytes = stat.blockSizeLong * stat.blockCountLong
|
||||||
val localDownloadedBytes = visual.sumOf { it.totalBytes }
|
val localDownloadedBytes = visual.sumOf { it.totalBytes }
|
||||||
|
val localUsedBytes = localTotalBytes - localBytesAvailable
|
||||||
_usedBytes.postValue(localTotalBytes - localBytesAvailable - localDownloadedBytes)
|
_usedBytes.postValue(localUsedBytes)
|
||||||
_availableBytes.postValue(localBytesAvailable)
|
_availableBytes.postValue(localBytesAvailable)
|
||||||
_downloadBytes.postValue(localDownloadedBytes)
|
_downloadBytes.postValue(localDownloadedBytes)
|
||||||
} catch (t : Throwable) {
|
} catch (t: Throwable) {
|
||||||
_downloadBytes.postValue(0)
|
_downloadBytes.postValue(0)
|
||||||
logError(t)
|
logError(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
_headerCards.postValue(visual)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
fun handleMultiDelete(context: Context) = viewModelScope.launchSafe {
|
||||||
|
val selectedItemsList = getSelectedItemsData().orEmpty()
|
||||||
|
val deleteData = processSelectedItems(context, selectedItemsList)
|
||||||
|
val message = buildDeleteMessage(context, deleteData)
|
||||||
|
showDeleteConfirmationDialog(context, message, deleteData.ids, deleteData.parentIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleSingleDelete(
|
||||||
|
context: Context,
|
||||||
|
itemId: Int
|
||||||
|
) = viewModelScope.launchSafe {
|
||||||
|
val itemData = getItemDataFromId(itemId)
|
||||||
|
val deleteData = processSelectedItems(context, itemData)
|
||||||
|
val message = buildDeleteMessage(context, deleteData)
|
||||||
|
showDeleteConfirmationDialog(context, message, deleteData.ids, deleteData.parentIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processSelectedItems(
|
||||||
|
context: Context,
|
||||||
|
selectedItemsList: List<VisualDownloadCached>
|
||||||
|
): DeleteData {
|
||||||
|
val names = mutableListOf<String>()
|
||||||
|
val seriesNames = mutableListOf<String>()
|
||||||
|
|
||||||
|
val ids = mutableSetOf<Int>()
|
||||||
|
val parentIds = mutableSetOf<Int>()
|
||||||
|
|
||||||
|
var parentName: String? = null
|
||||||
|
|
||||||
|
selectedItemsList.forEach { item ->
|
||||||
|
when (item) {
|
||||||
|
is VisualDownloadCached.Header -> {
|
||||||
|
if (item.data.type.isEpisodeBased()) {
|
||||||
|
val episodes = context.getKeys(DOWNLOAD_EPISODE_CACHE)
|
||||||
|
.mapNotNull {
|
||||||
|
context.getKey<VideoDownloadHelper.DownloadEpisodeCached>(
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.filter { it.parentId == item.data.id }
|
||||||
|
.map { it.id }
|
||||||
|
ids.addAll(episodes)
|
||||||
|
parentIds.add(item.data.id)
|
||||||
|
|
||||||
|
val episodeInfo = "${item.data.name} (${item.totalDownloads} ${
|
||||||
|
context.resources.getQuantityString(
|
||||||
|
R.plurals.episodes,
|
||||||
|
item.totalDownloads
|
||||||
|
).lowercase()
|
||||||
|
})"
|
||||||
|
seriesNames.add(episodeInfo)
|
||||||
|
} else {
|
||||||
|
ids.add(item.data.id)
|
||||||
|
names.add(item.data.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is VisualDownloadCached.Child -> {
|
||||||
|
ids.add(item.data.id)
|
||||||
|
val parent = context.getKey<VideoDownloadHelper.DownloadHeaderCached>(
|
||||||
|
DOWNLOAD_HEADER_CACHE,
|
||||||
|
item.data.parentId.toString()
|
||||||
|
)
|
||||||
|
parentName = parent?.name
|
||||||
|
names.add(
|
||||||
|
context.getNameFull(
|
||||||
|
item.data.name,
|
||||||
|
item.data.episode,
|
||||||
|
item.data.season
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DeleteData(ids, parentIds, seriesNames, names, parentName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildDeleteMessage(
|
||||||
|
context: Context,
|
||||||
|
data: DeleteData
|
||||||
|
): String {
|
||||||
|
val formattedNames = data.names.sortedBy { it.lowercase() }
|
||||||
|
.joinToString(separator = "\n") { "• $it" }
|
||||||
|
val formattedSeriesNames = data.seriesNames.sortedBy { it.lowercase() }
|
||||||
|
.joinToString(separator = "\n") { "• $it" }
|
||||||
|
|
||||||
|
return when {
|
||||||
|
data.ids.count() == 1 -> {
|
||||||
|
context.getString(R.string.delete_message).format(
|
||||||
|
data.names.firstOrNull()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data.seriesNames.isNotEmpty() && data.names.isEmpty() -> {
|
||||||
|
context.getString(R.string.delete_message_series_only).format(formattedSeriesNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
data.parentName != null && data.names.isNotEmpty() -> {
|
||||||
|
context.getString(R.string.delete_message_series_episodes)
|
||||||
|
.format(data.parentName, formattedNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
data.seriesNames.isNotEmpty() -> {
|
||||||
|
val seriesSection = context.getString(R.string.delete_message_series_section)
|
||||||
|
.format(formattedSeriesNames)
|
||||||
|
context.getString(R.string.delete_message_multiple)
|
||||||
|
.format(formattedNames) + "\n\n" + seriesSection
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> context.getString(R.string.delete_message_multiple).format(formattedNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDeleteConfirmationDialog(
|
||||||
|
context: Context,
|
||||||
|
message: String,
|
||||||
|
ids: Set<Int>,
|
||||||
|
parentIds: Set<Int>
|
||||||
|
) {
|
||||||
|
val builder = AlertDialog.Builder(context)
|
||||||
|
val dialogClickListener =
|
||||||
|
DialogInterface.OnClickListener { _, which ->
|
||||||
|
when (which) {
|
||||||
|
DialogInterface.BUTTON_POSITIVE -> {
|
||||||
|
viewModelScope.launchSafe {
|
||||||
|
setIsMultiDeleteState(false)
|
||||||
|
deleteFilesAndUpdateSettings(context, ids, this) { successfulIds ->
|
||||||
|
// We always remove parent because if we are deleting from here
|
||||||
|
// and we have it as non-empty, it was triggered on
|
||||||
|
// parent header card
|
||||||
|
removeItems(successfulIds + parentIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogInterface.BUTTON_NEGATIVE -> {
|
||||||
|
// Do nothing on cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val title = if (ids.count() == 1) {
|
||||||
|
R.string.delete_file
|
||||||
|
} else R.string.delete_files
|
||||||
|
builder.setTitle(title)
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton(R.string.delete, dialogClickListener)
|
||||||
|
.setNegativeButton(R.string.cancel, dialogClickListener)
|
||||||
|
.show().setDefaultFocus()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSelectedItemsData(): List<VisualDownloadCached>? {
|
||||||
|
val headers = headerCards.value.orEmpty()
|
||||||
|
val children = childCards.value.orEmpty()
|
||||||
|
|
||||||
|
return selectedItemIds.value?.mapNotNull { id ->
|
||||||
|
headers.find { it.data.id == id } ?: children.find { it.data.id == id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getItemDataFromId(itemId: Int): List<VisualDownloadCached> {
|
||||||
|
val headers = headerCards.value.orEmpty()
|
||||||
|
val children = childCards.value.orEmpty()
|
||||||
|
|
||||||
|
return (headers + children).filter { it.data.id == itemId }
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class DeleteData(
|
||||||
|
val ids: Set<Int>,
|
||||||
|
val parentIds: Set<Int>,
|
||||||
|
val seriesNames: List<String>,
|
||||||
|
val names: List<String>,
|
||||||
|
val parentName: String?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.lagradost.cloudstream3.ui.download.button
|
package com.lagradost.cloudstream3.ui.download.button
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter.formatShortFileSize
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
|
@ -9,6 +9,8 @@ import androidx.annotation.LayoutRes
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.ContentLoadingProgressBar
|
import androidx.core.widget.ContentLoadingProgressBar
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
|
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||||
|
|
||||||
typealias DownloadStatusTell = VideoDownloadManager.DownloadType
|
typealias DownloadStatusTell = VideoDownloadManager.DownloadType
|
||||||
|
|
@ -34,7 +36,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
lateinit var progressBar: ContentLoadingProgressBar
|
lateinit var progressBar: ContentLoadingProgressBar
|
||||||
var progressText: TextView? = null
|
var progressText: TextView? = null
|
||||||
|
|
||||||
/*val gid: String? get() = sessionIdToGid[persistentId]
|
/* val gid: String? get() = sessionIdToGid[persistentId]
|
||||||
|
|
||||||
// used for resuming data
|
// used for resuming data
|
||||||
var _lastRequestOverride: UriRequest? = null
|
var _lastRequestOverride: UriRequest? = null
|
||||||
|
|
@ -44,7 +46,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
_lastRequestOverride = value
|
_lastRequestOverride = value
|
||||||
}
|
}
|
||||||
|
|
||||||
var files: List<AbstractClient.JsonFile> = emptyList()*/
|
var files: List<AbstractClient.JsonFile> = emptyList() */
|
||||||
protected var isZeroBytes: Boolean = true
|
protected var isZeroBytes: Boolean = true
|
||||||
|
|
||||||
fun inflate(@LayoutRes layout: Int) {
|
fun inflate(@LayoutRes layout: Int) {
|
||||||
|
|
@ -52,12 +54,16 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@Suppress("LeakingThis")
|
||||||
resetViewData()
|
resetViewData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var doSetProgress = true
|
||||||
|
|
||||||
open fun resetViewData() {
|
open fun resetViewData() {
|
||||||
// lastRequest = null
|
// lastRequest = null
|
||||||
isZeroBytes = true
|
isZeroBytes = true
|
||||||
|
doSetProgress = true
|
||||||
persistentId = null
|
persistentId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,37 +74,45 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
persistentId = id
|
persistentId = id
|
||||||
currentMetaData.id = id
|
currentMetaData.id = id
|
||||||
|
|
||||||
VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(context, id)?.let { savedData ->
|
if (!doSetProgress) return
|
||||||
val downloadedBytes = savedData.fileLength
|
|
||||||
val totalBytes = savedData.totalBytes
|
|
||||||
|
|
||||||
/*lastRequest = savedData.uriRequest
|
ioSafe {
|
||||||
files = savedData.files
|
val savedData = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(context, id)
|
||||||
|
|
||||||
var totalBytes: Long = 0
|
mainWork {
|
||||||
var downloadedBytes: Long = 0
|
if (savedData != null) {
|
||||||
for (file in savedData.files) {
|
val downloadedBytes = savedData.fileLength
|
||||||
downloadedBytes += file.completedLength
|
val totalBytes = savedData.totalBytes
|
||||||
totalBytes += file.length
|
|
||||||
}*/
|
setProgress(downloadedBytes, totalBytes)
|
||||||
setProgress(downloadedBytes, totalBytes)
|
applyMetaData(id, downloadedBytes, totalBytes)
|
||||||
// some extra padding for just in case
|
} else run { resetView() }
|
||||||
val status = VideoDownloadManager.downloadStatus[id]
|
|
||||||
?: if (downloadedBytes > 1024L && downloadedBytes + 1024L >= totalBytes) DownloadStatusTell.IsDone else DownloadStatusTell.IsPaused
|
|
||||||
currentMetaData.apply {
|
|
||||||
this.id = id
|
|
||||||
this.downloadedLength = downloadedBytes
|
|
||||||
this.totalLength = totalBytes
|
|
||||||
this.status = status
|
|
||||||
}
|
}
|
||||||
setStatus(status)
|
|
||||||
} ?: run {
|
|
||||||
resetView()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun setStatus(status: VideoDownloadManager.DownloadType?)
|
abstract fun setStatus(status: VideoDownloadManager.DownloadType?)
|
||||||
|
|
||||||
|
fun getStatus(id: Int, downloadedBytes: Long, totalBytes: Long): DownloadStatusTell {
|
||||||
|
// some extra padding for just in case
|
||||||
|
return VideoDownloadManager.downloadStatus[id]
|
||||||
|
?: if (downloadedBytes > 1024L && downloadedBytes + 1024L >= totalBytes) {
|
||||||
|
DownloadStatusTell.IsDone
|
||||||
|
} else DownloadStatusTell.IsPaused
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyMetaData(id: Int, downloadedBytes: Long, totalBytes: Long) {
|
||||||
|
val status = getStatus(id, downloadedBytes, totalBytes)
|
||||||
|
|
||||||
|
currentMetaData.apply {
|
||||||
|
this.id = id
|
||||||
|
this.downloadedLength = downloadedBytes
|
||||||
|
this.totalLength = totalBytes
|
||||||
|
this.status = status
|
||||||
|
}
|
||||||
|
setStatus(status)
|
||||||
|
}
|
||||||
|
|
||||||
open fun setProgress(downloadedBytes: Long, totalBytes: Long) {
|
open fun setProgress(downloadedBytes: Long, totalBytes: Long) {
|
||||||
isZeroBytes = downloadedBytes == 0L
|
isZeroBytes = downloadedBytes == 0L
|
||||||
progressBar.post {
|
progressBar.post {
|
||||||
|
|
@ -124,13 +138,16 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
if (isZeroBytes) {
|
if (isZeroBytes) {
|
||||||
progressText?.isVisible = false
|
progressText?.isVisible = false
|
||||||
} else {
|
} else {
|
||||||
progressText?.apply {
|
if (doSetProgress) {
|
||||||
val currentMbString = Formatter.formatShortFileSize(context, downloadedBytes)
|
progressText?.apply {
|
||||||
val totalMbString = Formatter.formatShortFileSize(context, totalBytes)
|
val currentFormattedSizeString =
|
||||||
text =
|
formatShortFileSize(context, downloadedBytes)
|
||||||
//if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else
|
val totalFormattedSizeString = formatShortFileSize(context, totalBytes)
|
||||||
context?.getString(R.string.download_size_format)
|
text =
|
||||||
?.format(currentMbString, totalMbString)
|
// if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else
|
||||||
|
context?.getString(R.string.download_size_format)
|
||||||
|
?.format(currentFormattedSizeString, totalFormattedSizeString)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,8 +184,8 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
VideoDownloadManager.downloadStatusEvent += ::downloadStatusEvent
|
VideoDownloadManager.downloadStatusEvent += ::downloadStatusEvent
|
||||||
//VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent
|
// VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent
|
||||||
//VideoDownloadManager.downloadEvent += ::downloadEvent
|
// VideoDownloadManager.downloadEvent += ::downloadEvent
|
||||||
VideoDownloadManager.downloadProgressEvent += ::downloadProgressEvent
|
VideoDownloadManager.downloadProgressEvent += ::downloadProgressEvent
|
||||||
|
|
||||||
val pid = persistentId
|
val pid = persistentId
|
||||||
|
|
@ -182,8 +199,8 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
|
|
||||||
override fun onDetachedFromWindow() {
|
override fun onDetachedFromWindow() {
|
||||||
VideoDownloadManager.downloadStatusEvent -= ::downloadStatusEvent
|
VideoDownloadManager.downloadStatusEvent -= ::downloadStatusEvent
|
||||||
//VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent
|
// VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent
|
||||||
//VideoDownloadManager.downloadEvent -= ::downloadEvent
|
// VideoDownloadManager.downloadEvent -= ::downloadEvent
|
||||||
VideoDownloadManager.downloadProgressEvent -= ::downloadProgressEvent
|
VideoDownloadManager.downloadProgressEvent -= ::downloadProgressEvent
|
||||||
|
|
||||||
super.onDetachedFromWindow()
|
super.onDetachedFromWindow()
|
||||||
|
|
@ -198,5 +215,4 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
* Get a clean slate again, might be useful in recyclerview?
|
* Get a clean slate again, might be useful in recyclerview?
|
||||||
* */
|
* */
|
||||||
abstract fun resetView()
|
abstract fun resetView()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
class DownloadButton(context: Context, attributeSet: AttributeSet) :
|
class DownloadButton(context: Context, attributeSet: AttributeSet) :
|
||||||
PieFetchButton(context, attributeSet) {
|
PieFetchButton(context, attributeSet) {
|
||||||
|
|
||||||
var mainText: TextView? = null
|
private var mainText: TextView? = null
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
progressText = findViewById(R.id.result_movie_download_text_precentage)
|
progressText = findViewById(R.id.result_movie_download_text_precentage)
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,20 @@
|
||||||
package com.lagradost.cloudstream3.ui.download.button
|
package com.lagradost.cloudstream3.ui.download.button
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.os.Looper
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.animation.AnimationUtils
|
import android.view.animation.AnimationUtils
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.MainThread
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DELETE_FILE
|
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DELETE_FILE
|
||||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
|
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
|
||||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK
|
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK
|
||||||
|
|
@ -22,7 +25,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
|
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||||
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.KEY_RESUME_PACKAGES
|
||||||
|
|
||||||
open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
BaseFetchButton(context, attributeSet) {
|
BaseFetchButton(context, attributeSet) {
|
||||||
|
|
@ -41,6 +44,8 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
private var iconPaused: Int = 0
|
private var iconPaused: Int = 0
|
||||||
private var hideWhenIcon: Boolean = true
|
private var hideWhenIcon: Boolean = true
|
||||||
|
|
||||||
|
var progressDrawable: Int = 0
|
||||||
|
|
||||||
var overrideLayout: Int? = null
|
var overrideLayout: Int? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
@ -53,7 +58,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
}
|
}
|
||||||
|
|
||||||
private var progressBarBackground: View
|
private var progressBarBackground: View
|
||||||
private var statusView: ImageView
|
var statusView: ImageView
|
||||||
|
|
||||||
open fun onInflate() {}
|
open fun onInflate() {}
|
||||||
|
|
||||||
|
|
@ -111,10 +116,10 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
R.styleable.PieFetchButton_download_icon_complete, R.drawable.download_icon_done
|
R.styleable.PieFetchButton_download_icon_complete, R.drawable.download_icon_done
|
||||||
)
|
)
|
||||||
iconPaused = getResourceId(
|
iconPaused = getResourceId(
|
||||||
R.styleable.PieFetchButton_download_icon_paused, 0//R.drawable.download_icon_pause
|
R.styleable.PieFetchButton_download_icon_paused, 0 // R.drawable.download_icon_pause
|
||||||
)
|
)
|
||||||
iconActive = getResourceId(
|
iconActive = getResourceId(
|
||||||
R.styleable.PieFetchButton_download_icon_active, 0 //R.drawable.download_icon_load
|
R.styleable.PieFetchButton_download_icon_active, 0 // R.drawable.download_icon_load
|
||||||
)
|
)
|
||||||
iconWaiting = getResourceId(
|
iconWaiting = getResourceId(
|
||||||
R.styleable.PieFetchButton_download_icon_waiting, 0
|
R.styleable.PieFetchButton_download_icon_waiting, 0
|
||||||
|
|
@ -125,7 +130,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
|
|
||||||
val fillIndex = getInt(R.styleable.PieFetchButton_download_fill, 0)
|
val fillIndex = getInt(R.styleable.PieFetchButton_download_fill, 0)
|
||||||
|
|
||||||
val progressDrawable = getResourceId(
|
progressDrawable = getResourceId(
|
||||||
R.styleable.PieFetchButton_download_fill_override, fillArray[fillIndex]
|
R.styleable.PieFetchButton_download_fill_override, fillArray[fillIndex]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -164,8 +169,9 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
this.setPersistentId(card.id)
|
this.setPersistentId(card.id)
|
||||||
view.setOnClickListener {
|
view.setOnClickListener {
|
||||||
if (isZeroBytes) {
|
if (isZeroBytes) {
|
||||||
|
removeKey(KEY_RESUME_PACKAGES, card.id.toString())
|
||||||
callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card))
|
callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card))
|
||||||
//callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data))
|
// callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data))
|
||||||
} else {
|
} else {
|
||||||
val list = arrayListOf(
|
val list = arrayListOf(
|
||||||
Pair(DOWNLOAD_ACTION_PLAY_FILE, R.string.popup_play_file),
|
Pair(DOWNLOAD_ACTION_PLAY_FILE, R.string.popup_play_file),
|
||||||
|
|
@ -192,7 +198,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
list
|
list
|
||||||
) {
|
) {
|
||||||
callback(DownloadClickEvent(itemId, card))
|
callback(DownloadClickEvent(itemId, card))
|
||||||
//callback.invoke(DownloadClickEvent(itemId, data))
|
// callback.invoke(DownloadClickEvent(itemId, data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -200,7 +206,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
view.setOnLongClickListener {
|
view.setOnLongClickListener {
|
||||||
callback(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, card))
|
callback(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, card))
|
||||||
|
|
||||||
//clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, data))
|
// clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, data))
|
||||||
return@setOnLongClickListener true
|
return@setOnLongClickListener true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -213,7 +219,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
setDefaultClickListener(this, textView, card, callback)
|
setDefaultClickListener(this, textView, card, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*open fun setDefaultClickListener(requestGetter: suspend BaseFetchButton.() -> List<UriRequest>) {
|
/* open fun setDefaultClickListener(requestGetter: suspend BaseFetchButton.() -> List<UriRequest>) {
|
||||||
this.setOnClickListener {
|
this.setOnClickListener {
|
||||||
when (this.currentStatus) {
|
when (this.currentStatus) {
|
||||||
null -> {
|
null -> {
|
||||||
|
|
@ -239,42 +245,57 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}*/
|
} */
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
private fun setStatusInternal(status: DownloadStatusTell?) {
|
||||||
|
val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading
|
||||||
|
if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) {
|
||||||
|
val animation = AnimationUtils.loadAnimation(context, waitingAnimation)
|
||||||
|
progressBarBackground.startAnimation(animation)
|
||||||
|
} else {
|
||||||
|
progressBarBackground.clearAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
val progressDrawable =
|
||||||
|
if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline
|
||||||
|
|
||||||
|
progressBarBackground.background =
|
||||||
|
ContextCompat.getDrawable(context, progressDrawable)
|
||||||
|
|
||||||
|
val drawable =
|
||||||
|
getDrawableFromStatus(status)?.let { ContextCompat.getDrawable(this.context, it) }
|
||||||
|
statusView.setImageDrawable(drawable)
|
||||||
|
val isDrawable = drawable != null
|
||||||
|
|
||||||
|
statusView.isVisible = isDrawable
|
||||||
|
val hide = hideWhenIcon && isDrawable
|
||||||
|
if (hide) {
|
||||||
|
progressBar.clearAnimation()
|
||||||
|
progressBarBackground.clearAnimation()
|
||||||
|
}
|
||||||
|
progressBarBackground.isGone = hide
|
||||||
|
progressBar.isGone = hide
|
||||||
|
}
|
||||||
|
|
||||||
/** Also sets currentStatus */
|
/** Also sets currentStatus */
|
||||||
override fun setStatus(status: DownloadStatusTell?) {
|
override fun setStatus(status: DownloadStatusTell?) {
|
||||||
currentStatus = status
|
currentStatus = status
|
||||||
|
|
||||||
//progressBar.isVisible =
|
// Runs on the main thread, but also instant if it already is
|
||||||
// status != null && status != DownloadStatusTell.Complete && status != DownloadStatusTell.Error
|
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||||
//progressBarBackground.isVisible = status != null && status != DownloadStatusTell.Complete
|
try {
|
||||||
progressBarBackground.post {
|
setStatusInternal(status)
|
||||||
val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading
|
} catch (t: Throwable) {
|
||||||
if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) {
|
logError(t) // Just in case setStatusInternal throws because thread
|
||||||
val animation = AnimationUtils.loadAnimation(context, waitingAnimation)
|
progressBarBackground.post {
|
||||||
progressBarBackground.startAnimation(animation)
|
setStatusInternal(status)
|
||||||
} else {
|
}
|
||||||
progressBarBackground.clearAnimation()
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
val progressDrawable =
|
progressBarBackground.post {
|
||||||
if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline
|
setStatusInternal(status)
|
||||||
|
|
||||||
progressBarBackground.background =
|
|
||||||
ContextCompat.getDrawable(context, progressDrawable)
|
|
||||||
|
|
||||||
val drawable = getDrawableFromStatus(status)
|
|
||||||
statusView.setImageDrawable(drawable)
|
|
||||||
val isDrawable = drawable != null
|
|
||||||
|
|
||||||
statusView.isVisible = isDrawable
|
|
||||||
val hide = hideWhenIcon && isDrawable
|
|
||||||
if (hide) {
|
|
||||||
progressBar.clearAnimation()
|
|
||||||
progressBarBackground.clearAnimation()
|
|
||||||
}
|
}
|
||||||
progressBarBackground.isGone = hide
|
|
||||||
progressBar.isGone = hide
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -282,6 +303,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
setStatus(null)
|
setStatus(null)
|
||||||
currentMetaData = DownloadMetadata(0, 0, 0, null)
|
currentMetaData = DownloadMetadata(0, 0, 0, null)
|
||||||
isZeroBytes = true
|
isZeroBytes = true
|
||||||
|
doSetProgress = true
|
||||||
progressBar.progress = 0
|
progressBar.progress = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -305,19 +327,13 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getDrawableFromStatus(status: DownloadStatusTell?): Drawable? {
|
open fun getDrawableFromStatus(status: DownloadStatusTell?): Int? = when (status) {
|
||||||
val drawableInt = when (status) {
|
DownloadStatusTell.IsPaused -> iconPaused
|
||||||
DownloadStatusTell.IsPaused -> iconPaused
|
DownloadStatusTell.IsPending -> iconWaiting
|
||||||
DownloadStatusTell.IsPending -> iconWaiting
|
DownloadStatusTell.IsDownloading -> iconActive
|
||||||
DownloadStatusTell.IsDownloading -> iconActive
|
DownloadStatusTell.IsFailed -> iconError
|
||||||
DownloadStatusTell.IsFailed -> iconError
|
DownloadStatusTell.IsDone -> iconComplete
|
||||||
DownloadStatusTell.IsDone -> iconComplete
|
DownloadStatusTell.IsStopped -> iconRemoved
|
||||||
DownloadStatusTell.IsStopped -> iconRemoved
|
else -> iconInit
|
||||||
null -> iconInit
|
}.takeIf { it != 0 }
|
||||||
}
|
|
||||||
if (drawableInt == 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return ContextCompat.getDrawable(this.context, drawableInt)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -2,32 +2,59 @@ package com.lagradost.cloudstream3.ui.home
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.SearchResponse
|
import com.lagradost.cloudstream3.SearchResponse
|
||||||
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
|
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
|
||||||
import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding
|
import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding
|
||||||
|
import com.lagradost.cloudstream3.ui.BaseAdapter
|
||||||
|
import com.lagradost.cloudstream3.ui.ViewHolderState
|
||||||
|
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.isBottomLayout
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
|
|
||||||
class HomeChildItemAdapter(
|
class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState<Boolean>(view) {
|
||||||
val cardList: MutableList<SearchResponse>,
|
/*private fun recursive(view : View) : Boolean {
|
||||||
|
if (view.isFocused) {
|
||||||
|
println("VIEW: $view | id=${view.id}")
|
||||||
|
}
|
||||||
|
return (view as? ViewGroup)?.children?.any { recursive(it) } ?: false
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// very shitty that we cant store the state when the view clears,
|
||||||
|
// but this is because the focus clears before the view is removed
|
||||||
|
// so we have to manually store it
|
||||||
|
var wasFocused: Boolean = false
|
||||||
|
override fun save(): Boolean = wasFocused
|
||||||
|
override fun restore(state: Boolean) {
|
||||||
|
if (state) {
|
||||||
|
wasFocused = false
|
||||||
|
// only refocus if tv
|
||||||
|
if(isLayout(TV)) {
|
||||||
|
itemView.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomeChildItemAdapter(
|
||||||
|
fragment: Fragment,
|
||||||
|
id: Int,
|
||||||
private val nextFocusUp: Int? = null,
|
private val nextFocusUp: Int? = null,
|
||||||
private val nextFocusDown: Int? = null,
|
private val nextFocusDown: Int? = null,
|
||||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
private val clickCallback: (SearchClickCallback) -> Unit,
|
||||||
) :
|
) :
|
||||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
BaseAdapter<SearchResponse, Boolean>(fragment, id) {
|
||||||
var isHorizontal: Boolean = false
|
var isHorizontal: Boolean = false
|
||||||
var hasNext: Boolean = false
|
var hasNext: Boolean = false
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Boolean> {
|
||||||
val expanded = parent.context.IsBottomLayout()
|
val expanded = parent.context.isBottomLayout()
|
||||||
/* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid
|
/* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid
|
||||||
|
|
||||||
val root = LayoutInflater.from(parent.context).inflate(layout, parent, false)
|
val root = LayoutInflater.from(parent.context).inflate(layout, parent, false)
|
||||||
|
|
@ -39,164 +66,77 @@ class HomeChildItemAdapter(
|
||||||
parent,
|
parent,
|
||||||
false
|
false
|
||||||
) else HomeResultGridBinding.inflate(inflater, parent, false)
|
) else HomeResultGridBinding.inflate(inflater, parent, false)
|
||||||
|
return HomeScrollViewHolderState(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindContent(
|
||||||
|
holder: ViewHolderState<Boolean>,
|
||||||
|
item: SearchResponse,
|
||||||
|
position: Int
|
||||||
|
) {
|
||||||
|
when (val binding = holder.view) {
|
||||||
|
is HomeResultGridBinding -> {
|
||||||
|
binding.backgroundCard.apply {
|
||||||
|
val min = 114.toPx
|
||||||
|
val max = 180.toPx
|
||||||
|
|
||||||
return CardViewHolder(
|
layoutParams =
|
||||||
binding,
|
layoutParams.apply {
|
||||||
clickCallback,
|
width = if (!isHorizontal) {
|
||||||
itemCount,
|
min
|
||||||
|
} else {
|
||||||
|
max
|
||||||
|
}
|
||||||
|
height = if (!isHorizontal) {
|
||||||
|
max
|
||||||
|
} else {
|
||||||
|
min
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is HomeResultGridExpandedBinding -> {
|
||||||
|
binding.backgroundCard.apply {
|
||||||
|
val min = 114.toPx
|
||||||
|
val max = 180.toPx
|
||||||
|
|
||||||
|
layoutParams =
|
||||||
|
layoutParams.apply {
|
||||||
|
width = if (!isHorizontal) {
|
||||||
|
min
|
||||||
|
} else {
|
||||||
|
max
|
||||||
|
}
|
||||||
|
height = if (!isHorizontal) {
|
||||||
|
max
|
||||||
|
} else {
|
||||||
|
min
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position == 0) { // to fix tv
|
||||||
|
binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchResultBuilder.bind(
|
||||||
|
clickCallback = { click ->
|
||||||
|
// ok, so here we hijack the callback to fix the focus
|
||||||
|
when (click.action) {
|
||||||
|
SEARCH_ACTION_LOAD -> (holder as? HomeScrollViewHolderState)?.wasFocused = true
|
||||||
|
}
|
||||||
|
clickCallback(click)
|
||||||
|
},
|
||||||
|
item,
|
||||||
|
position,
|
||||||
|
holder.itemView,
|
||||||
nextFocusUp,
|
nextFocusUp,
|
||||||
nextFocusDown,
|
nextFocusDown
|
||||||
isHorizontal,
|
|
||||||
parent.isRtl()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
|
||||||
when (holder) {
|
|
||||||
is CardViewHolder -> {
|
|
||||||
holder.itemCount = itemCount // i know ugly af
|
|
||||||
holder.bind(cardList[position], position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return cardList.size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return (cardList[position].id ?: position).toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateList(newList: List<SearchResponse>) {
|
|
||||||
val diffResult = DiffUtil.calculateDiff(
|
|
||||||
HomeChildDiffCallback(this.cardList, newList)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
cardList.clear()
|
holder.itemView.tag = position
|
||||||
cardList.addAll(newList)
|
|
||||||
|
|
||||||
diffResult.dispatchUpdatesTo(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
class CardViewHolder
|
|
||||||
constructor(
|
|
||||||
val binding: ViewBinding,
|
|
||||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
|
||||||
var itemCount: Int,
|
|
||||||
private val nextFocusUp: Int? = null,
|
|
||||||
private val nextFocusDown: Int? = null,
|
|
||||||
private val isHorizontal: Boolean = false,
|
|
||||||
private val isRtl: Boolean
|
|
||||||
) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun bind(card: SearchResponse, position: Int) {
|
|
||||||
|
|
||||||
// TV focus fixing
|
|
||||||
/*val nextFocusBehavior = when (position) {
|
|
||||||
0 -> true
|
|
||||||
itemCount - 1 -> false
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position == 0) { // to fix tv
|
|
||||||
if (isRtl) {
|
|
||||||
itemView.nextFocusRightId = R.id.nav_rail_view
|
|
||||||
itemView.nextFocusLeftId = -1
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
itemView.nextFocusLeftId = R.id.nav_rail_view
|
|
||||||
itemView.nextFocusRightId = -1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
itemView.nextFocusRightId = -1
|
|
||||||
itemView.nextFocusLeftId = -1
|
|
||||||
}*/
|
|
||||||
|
|
||||||
|
|
||||||
when (binding) {
|
|
||||||
is HomeResultGridBinding -> {
|
|
||||||
binding.backgroundCard.apply {
|
|
||||||
val min = 114.toPx
|
|
||||||
val max = 180.toPx
|
|
||||||
|
|
||||||
layoutParams =
|
|
||||||
layoutParams.apply {
|
|
||||||
width = if (!isHorizontal) {
|
|
||||||
min
|
|
||||||
} else {
|
|
||||||
max
|
|
||||||
}
|
|
||||||
height = if (!isHorizontal) {
|
|
||||||
max
|
|
||||||
} else {
|
|
||||||
min
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
is HomeResultGridExpandedBinding -> {
|
|
||||||
binding.backgroundCard.apply {
|
|
||||||
val min = 114.toPx
|
|
||||||
val max = 180.toPx
|
|
||||||
|
|
||||||
layoutParams =
|
|
||||||
layoutParams.apply {
|
|
||||||
width = if (!isHorizontal) {
|
|
||||||
min
|
|
||||||
} else {
|
|
||||||
max
|
|
||||||
}
|
|
||||||
height = if (!isHorizontal) {
|
|
||||||
max
|
|
||||||
} else {
|
|
||||||
min
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position == 0) { // to fix tv
|
|
||||||
binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchResultBuilder.bind(
|
|
||||||
clickCallback,
|
|
||||||
card,
|
|
||||||
position,
|
|
||||||
itemView,
|
|
||||||
null, // nextFocusBehavior,
|
|
||||||
nextFocusUp,
|
|
||||||
nextFocusDown
|
|
||||||
)
|
|
||||||
itemView.tag = position
|
|
||||||
|
|
||||||
//val ani = ScaleAnimation(0.9f, 1.0f, 0.9f, 1f)
|
|
||||||
//ani.fillAfter = true
|
|
||||||
//ani.duration = 200
|
|
||||||
//itemView.startAnimation(ani)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeChildDiffCallback(
|
|
||||||
private val oldList: List<SearchResponse>,
|
|
||||||
private val newList: List<SearchResponse>
|
|
||||||
) :
|
|
||||||
DiffUtil.Callback() {
|
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
|
||||||
oldList[oldItemPosition].name == newList[newItemPosition].name
|
|
||||||
|
|
||||||
override fun getOldListSize() = oldList.size
|
|
||||||
|
|
||||||
override fun getNewListSize() = newList.size
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
|
||||||
oldList[oldItemPosition] == newList[newItemPosition] && oldItemPosition < oldList.size - 1 // always update the last item
|
|
||||||
}
|
|
||||||
|
|
@ -17,7 +17,6 @@ import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.*
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
|
@ -25,8 +24,6 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.APIHolder.apis
|
import com.lagradost.cloudstream3.APIHolder.apis
|
||||||
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
|
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
|
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
|
||||||
import com.lagradost.cloudstream3.databinding.HomeEpisodesExpandedBinding
|
import com.lagradost.cloudstream3.databinding.HomeEpisodesExpandedBinding
|
||||||
|
|
@ -42,13 +39,17 @@ import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLine
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.ui.search.*
|
import com.lagradost.cloudstream3.ui.search.*
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback
|
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.ownHide
|
import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.ownShow
|
import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
import com.lagradost.cloudstream3.utils.AppContextUtils.isRecyclerScrollable
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.ownHide
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.ownShow
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
import com.lagradost.cloudstream3.utils.Event
|
import com.lagradost.cloudstream3.utils.Event
|
||||||
|
|
@ -232,7 +233,7 @@ class HomeFragment : Fragment() {
|
||||||
return bottomSheetDialogBuilder
|
return bottomSheetDialogBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPairList(
|
private fun getPairList(
|
||||||
anime: Chip?,
|
anime: Chip?,
|
||||||
cartoons: Chip?,
|
cartoons: Chip?,
|
||||||
tvs: Chip?,
|
tvs: Chip?,
|
||||||
|
|
@ -311,7 +312,7 @@ class HomeFragment : Fragment() {
|
||||||
button?.isVisible = isValid
|
button?.isVisible = isValid
|
||||||
button?.isChecked = isValid && selectedTypes.any { types.contains(it) }
|
button?.isChecked = isValid && selectedTypes.any { types.contains(it) }
|
||||||
button?.isFocusable = true
|
button?.isFocusable = true
|
||||||
if (isTrueTvSettings()) {
|
if (isLayout(TV)) {
|
||||||
button?.isFocusableInTouchMode = true
|
button?.isFocusableInTouchMode = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -435,7 +436,7 @@ class HomeFragment : Fragment() {
|
||||||
|
|
||||||
bottomSheetDialog?.ownShow()
|
bottomSheetDialog?.ownShow()
|
||||||
val layout =
|
val layout =
|
||||||
if (isTvSettings()) R.layout.fragment_home_tv else R.layout.fragment_home
|
if (isLayout(TV or EMULATOR)) R.layout.fragment_home_tv else R.layout.fragment_home
|
||||||
val root = inflater.inflate(layout, container, false)
|
val root = inflater.inflate(layout, container, false)
|
||||||
binding = try {
|
binding = try {
|
||||||
FragmentHomeBinding.bind(root)
|
FragmentHomeBinding.bind(root)
|
||||||
|
|
@ -449,6 +450,7 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
|
||||||
bottomSheetDialog?.ownHide()
|
bottomSheetDialog?.ownHide()
|
||||||
binding = null
|
binding = null
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
|
@ -485,6 +487,10 @@ class HomeFragment : Fragment() {
|
||||||
|
|
||||||
private var bottomSheetDialog: BottomSheetDialog? = null
|
private var bottomSheetDialog: BottomSheetDialog? = null
|
||||||
|
|
||||||
|
// https://github.com/vivchar/RendererRecyclerViewAdapter/blob/185251ee9d94fb6eb3e063b00d646b745186c365/example/src/main/java/com/github/vivchar/example/pages/github/GithubFragment.kt#L32
|
||||||
|
// cry about it, but this is android we are talking about, we cant do the most simple shit without making a global variable
|
||||||
|
private var instanceState: Bundle = Bundle()
|
||||||
|
private var homeMasterAdapter: HomeParentItemAdapterPreview? = null
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
|
@ -505,15 +511,14 @@ class HomeFragment : Fragment() {
|
||||||
activity.loadSearchResult(listHomepageItems.random())
|
activity.loadSearchResult(listHomepageItems.random())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
homeMasterAdapter = HomeParentItemAdapterPreview(
|
||||||
homeMasterRecycler.adapter =
|
fragment = this@HomeFragment,
|
||||||
HomeParentItemAdapterPreview(
|
homeViewModel,
|
||||||
mutableListOf(),
|
)
|
||||||
homeViewModel
|
homeMasterRecycler.adapter = homeMasterAdapter
|
||||||
)
|
|
||||||
//fixPaddingStatusbar(homeLoadingStatusbar)
|
//fixPaddingStatusbar(homeLoadingStatusbar)
|
||||||
|
|
||||||
homeApiFab.isVisible = !isTvSettings()
|
homeApiFab.isVisible = isLayout(PHONE)
|
||||||
|
|
||||||
homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
|
@ -521,7 +526,7 @@ class HomeFragment : Fragment() {
|
||||||
homeApiFab.shrink() // hide
|
homeApiFab.shrink() // hide
|
||||||
homeRandom.shrink()
|
homeRandom.shrink()
|
||||||
} else if (dy < -5) {
|
} else if (dy < -5) {
|
||||||
if (!isTvSettings()) {
|
if (isLayout(PHONE)) {
|
||||||
homeApiFab.extend() // show
|
homeApiFab.extend() // show
|
||||||
homeRandom.extend()
|
homeRandom.extend()
|
||||||
}
|
}
|
||||||
|
|
@ -529,6 +534,7 @@ class HomeFragment : Fragment() {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -539,7 +545,7 @@ class HomeFragment : Fragment() {
|
||||||
settingsManager.getBoolean(
|
settingsManager.getBoolean(
|
||||||
getString(R.string.random_button_key),
|
getString(R.string.random_button_key),
|
||||||
false
|
false
|
||||||
) && !isTvSettings()
|
) && isLayout(PHONE)
|
||||||
binding?.homeRandom?.visibility = View.GONE
|
binding?.homeRandom?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -559,10 +565,11 @@ class HomeFragment : Fragment() {
|
||||||
val mutableListOfResponse = mutableListOf<SearchResponse>()
|
val mutableListOfResponse = mutableListOf<SearchResponse>()
|
||||||
listHomepageItems.clear()
|
listHomepageItems.clear()
|
||||||
|
|
||||||
(homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(
|
(homeMasterRecycler.adapter as? ParentItemAdapter)?.submitList(d.values.map {
|
||||||
d.values.toMutableList(),
|
it.copy(
|
||||||
homeMasterRecycler
|
list = it.list.copy(list = it.list.list.toMutableList())
|
||||||
)
|
)
|
||||||
|
}.toMutableList())
|
||||||
|
|
||||||
homeLoading.isVisible = false
|
homeLoading.isVisible = false
|
||||||
homeLoadingError.isVisible = false
|
homeLoadingError.isVisible = false
|
||||||
|
|
@ -611,7 +618,7 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
is Resource.Loading -> {
|
is Resource.Loading -> {
|
||||||
(homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(listOf())
|
(homeMasterRecycler.adapter as? ParentItemAdapter)?.submitList(listOf())
|
||||||
homeLoadingShimmer.startShimmer()
|
homeLoadingShimmer.startShimmer()
|
||||||
homeLoading.isVisible = true
|
homeLoading.isVisible = true
|
||||||
homeLoadingError.isVisible = false
|
homeLoadingError.isVisible = false
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,30 @@
|
||||||
package com.lagradost.cloudstream3.ui.home
|
package com.lagradost.cloudstream3.ui.home
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.ListUpdateCallback
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
import com.lagradost.cloudstream3.HomePageList
|
import com.lagradost.cloudstream3.HomePageList
|
||||||
import com.lagradost.cloudstream3.LoadResponse
|
import com.lagradost.cloudstream3.LoadResponse
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
|
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
|
||||||
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
import com.lagradost.cloudstream3.ui.BaseAdapter
|
||||||
|
import com.lagradost.cloudstream3.ui.BaseDiffCallback
|
||||||
|
import com.lagradost.cloudstream3.ui.ViewHolderState
|
||||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.isRecyclerScrollable
|
||||||
|
|
||||||
class LoadClickCallback(
|
class LoadClickCallback(
|
||||||
val action: Int = 0,
|
val action: Int = 0,
|
||||||
|
|
@ -27,193 +34,89 @@ class LoadClickCallback(
|
||||||
)
|
)
|
||||||
|
|
||||||
open class ParentItemAdapter(
|
open class ParentItemAdapter(
|
||||||
private var items: MutableList<HomeViewModel.ExpandableHomepageList>,
|
open val fragment: Fragment,
|
||||||
//private val viewModel: HomeViewModel,
|
id: Int,
|
||||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
private val clickCallback: (SearchClickCallback) -> Unit,
|
||||||
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
|
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
|
||||||
private val expandCallback: ((String) -> Unit)? = null,
|
private val expandCallback: ((String) -> Unit)? = null,
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : BaseAdapter<HomeViewModel.ExpandableHomepageList, Bundle>(
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
fragment,
|
||||||
|
id,
|
||||||
val layoutResId = when {
|
diffCallback = BaseDiffCallback(
|
||||||
isTrueTvSettings() -> R.layout.homepage_parent_tv
|
itemSame = { a, b -> a.list.name == b.list.name },
|
||||||
parent.context.isEmulatorSettings() -> R.layout.homepage_parent_emulator
|
contentSame = { a, b ->
|
||||||
else -> R.layout.homepage_parent
|
a.list.list == b.list.list
|
||||||
}
|
|
||||||
|
|
||||||
val root = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
|
|
||||||
|
|
||||||
val binding = HomepageParentBinding.bind(root)
|
|
||||||
|
|
||||||
return ParentViewHolder(
|
|
||||||
binding,
|
|
||||||
clickCallback,
|
|
||||||
moreInfoClickCallback,
|
|
||||||
expandCallback
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
|
||||||
when (holder) {
|
|
||||||
is ParentViewHolder -> {
|
|
||||||
holder.bind(items[position])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return items.size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return items[position].list.name.hashCode().toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmName("updateListHomePageList")
|
|
||||||
fun updateList(newList: List<HomePageList>) {
|
|
||||||
updateList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) }
|
|
||||||
.toMutableList())
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmName("updateListExpandableHomepageList")
|
|
||||||
fun updateList(
|
|
||||||
newList: MutableList<HomeViewModel.ExpandableHomepageList>,
|
|
||||||
recyclerView: RecyclerView? = null
|
|
||||||
) {
|
|
||||||
// this
|
|
||||||
// 1. prevents deep copy that makes this.items == newList
|
|
||||||
// 2. filters out undesirable results
|
|
||||||
// 3. moves empty results to the bottom (sortedBy is a stable sort)
|
|
||||||
val new =
|
|
||||||
newList.map { it.copy(list = it.list.copy(list = it.list.list.filterSearchResponse())) }
|
|
||||||
.sortedBy { it.list.list.isEmpty() }
|
|
||||||
|
|
||||||
val diffResult = DiffUtil.calculateDiff(
|
|
||||||
SearchDiffCallback(items, new)
|
|
||||||
)
|
|
||||||
items.clear()
|
|
||||||
items.addAll(new)
|
|
||||||
|
|
||||||
//val mAdapter = this
|
|
||||||
val delta = if (this@ParentItemAdapter is HomeParentItemAdapterPreview) {
|
|
||||||
headItems
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
diffResult.dispatchUpdatesTo(object : ListUpdateCallback {
|
|
||||||
override fun onInserted(position: Int, count: Int) {
|
|
||||||
//notifyItemRangeChanged(position + delta, count)
|
|
||||||
notifyItemRangeInserted(position + delta, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRemoved(position: Int, count: Int) {
|
|
||||||
notifyItemRangeRemoved(position + delta, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
|
||||||
notifyItemMoved(fromPosition + delta, toPosition + delta)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChanged(_position: Int, count: Int, payload: Any?) {
|
|
||||||
|
|
||||||
val position = _position + delta
|
|
||||||
|
|
||||||
// I know kinda messy, what this does is using the update or bind instead of onCreateViewHolder -> bind
|
|
||||||
recyclerView?.apply {
|
|
||||||
// this loops every viewHolder in the recycle view and checks the position to see if it is within the update range
|
|
||||||
val missingUpdates = (position until (position + count)).toMutableSet()
|
|
||||||
for (i in 0 until itemCount) {
|
|
||||||
val child = getChildAt(i) ?: continue
|
|
||||||
val viewHolder = getChildViewHolder(child) ?: continue
|
|
||||||
if (viewHolder !is ParentViewHolder) continue
|
|
||||||
|
|
||||||
val absolutePosition = viewHolder.bindingAdapterPosition
|
|
||||||
if (absolutePosition >= position && absolutePosition < position + count) {
|
|
||||||
val expand = items.getOrNull(absolutePosition - delta) ?: continue
|
|
||||||
missingUpdates -= absolutePosition
|
|
||||||
//println("Updating ${viewHolder.title.text} ($absolutePosition $position) -> ${expand.list.name}")
|
|
||||||
if (viewHolder.title.text == expand.list.name) {
|
|
||||||
viewHolder.update(expand)
|
|
||||||
} else {
|
|
||||||
viewHolder.bind(expand)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// just in case some item did not get updated
|
|
||||||
for (i in missingUpdates) {
|
|
||||||
notifyItemChanged(i, payload)
|
|
||||||
}
|
|
||||||
} ?: run {
|
|
||||||
// in case we don't have a nice
|
|
||||||
notifyItemRangeChanged(position, count, payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
) {
|
||||||
//diffResult.dispatchUpdatesTo(this)
|
data class ParentItemHolder(val binding: ViewBinding) : ViewHolderState<Bundle>(binding) {
|
||||||
}
|
override fun save(): Bundle = Bundle().apply {
|
||||||
|
val recyclerView = (binding as? HomepageParentBinding)?.homeChildRecyclerview
|
||||||
class ParentViewHolder
|
putParcelable(
|
||||||
constructor(
|
"value",
|
||||||
val binding: HomepageParentBinding,
|
recyclerView?.layoutManager?.onSaveInstanceState()
|
||||||
// val viewModel: HomeViewModel,
|
)
|
||||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
(recyclerView?.adapter as? BaseAdapter<*, *>)?.save(recyclerView)
|
||||||
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
|
|
||||||
private val expandCallback: ((String) -> Unit)? = null,
|
|
||||||
) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
val title: TextView = binding.homeChildMoreInfo
|
|
||||||
private val recyclerView: RecyclerView = binding.homeChildRecyclerview
|
|
||||||
private val startFocus = R.id.nav_rail_view
|
|
||||||
private val endFocus = FOCUS_SELF
|
|
||||||
fun update(expand: HomeViewModel.ExpandableHomepageList) {
|
|
||||||
val info = expand.list
|
|
||||||
(recyclerView.adapter as? HomeChildItemAdapter?)?.apply {
|
|
||||||
updateList(info.list.toMutableList())
|
|
||||||
hasNext = expand.hasNext
|
|
||||||
} ?: run {
|
|
||||||
recyclerView.adapter = HomeChildItemAdapter(
|
|
||||||
info.list.toMutableList(),
|
|
||||||
clickCallback = clickCallback,
|
|
||||||
nextFocusUp = recyclerView.nextFocusUpId,
|
|
||||||
nextFocusDown = recyclerView.nextFocusDownId,
|
|
||||||
).apply {
|
|
||||||
isHorizontal = info.isHorizontalImages
|
|
||||||
hasNext = expand.hasNext
|
|
||||||
}
|
|
||||||
recyclerView.setLinearListLayout(
|
|
||||||
isHorizontal = true,
|
|
||||||
nextLeft = startFocus,
|
|
||||||
nextRight = endFocus,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(expand: HomeViewModel.ExpandableHomepageList) {
|
override fun restore(state: Bundle) {
|
||||||
val info = expand.list
|
(binding as? HomepageParentBinding)?.homeChildRecyclerview?.layoutManager?.onRestoreInstanceState(
|
||||||
recyclerView.adapter = HomeChildItemAdapter(
|
state.getSafeParcelable<Parcelable>("value")
|
||||||
info.list.toMutableList(),
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun submitList(list: List<HomeViewModel.ExpandableHomepageList>?) {
|
||||||
|
super.submitList(list?.sortedBy { it.list.list.isEmpty() })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUpdateContent(
|
||||||
|
holder: ViewHolderState<Bundle>,
|
||||||
|
item: HomeViewModel.ExpandableHomepageList,
|
||||||
|
position: Int
|
||||||
|
) {
|
||||||
|
val binding = holder.view
|
||||||
|
if (binding !is HomepageParentBinding) return
|
||||||
|
(binding.homeChildRecyclerview.adapter as? HomeChildItemAdapter)?.submitList(item.list.list)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindContent(
|
||||||
|
holder: ViewHolderState<Bundle>,
|
||||||
|
item: HomeViewModel.ExpandableHomepageList,
|
||||||
|
position: Int
|
||||||
|
) {
|
||||||
|
val startFocus = R.id.nav_rail_view
|
||||||
|
val endFocus = FOCUS_SELF
|
||||||
|
val binding = holder.view
|
||||||
|
if (binding !is HomepageParentBinding) return
|
||||||
|
val info = item.list
|
||||||
|
binding.apply {
|
||||||
|
homeChildRecyclerview.adapter = HomeChildItemAdapter(
|
||||||
|
fragment = fragment,
|
||||||
|
id = id + position + 100,
|
||||||
clickCallback = clickCallback,
|
clickCallback = clickCallback,
|
||||||
nextFocusUp = recyclerView.nextFocusUpId,
|
nextFocusUp = homeChildRecyclerview.nextFocusUpId,
|
||||||
nextFocusDown = recyclerView.nextFocusDownId,
|
nextFocusDown = homeChildRecyclerview.nextFocusDownId,
|
||||||
).apply {
|
).apply {
|
||||||
isHorizontal = info.isHorizontalImages
|
isHorizontal = info.isHorizontalImages
|
||||||
hasNext = expand.hasNext
|
hasNext = item.hasNext
|
||||||
|
submitList(item.list.list)
|
||||||
}
|
}
|
||||||
recyclerView.setLinearListLayout(
|
homeChildRecyclerview.setLinearListLayout(
|
||||||
isHorizontal = true,
|
isHorizontal = true,
|
||||||
nextLeft = startFocus,
|
nextLeft = startFocus,
|
||||||
nextRight = endFocus,
|
nextRight = endFocus,
|
||||||
)
|
)
|
||||||
title.text = info.name
|
homeChildMoreInfo.text = info.name
|
||||||
|
|
||||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
homeChildRecyclerview.addOnScrollListener(object :
|
||||||
|
RecyclerView.OnScrollListener() {
|
||||||
var expandCount = 0
|
var expandCount = 0
|
||||||
val name = expand.list.name
|
val name = item.list.name
|
||||||
|
|
||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
override fun onScrollStateChanged(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
newState: Int
|
||||||
|
) {
|
||||||
super.onScrollStateChanged(recyclerView, newState)
|
super.onScrollStateChanged(recyclerView, newState)
|
||||||
|
|
||||||
val adapter = recyclerView.adapter
|
val adapter = recyclerView.adapter
|
||||||
|
|
@ -237,27 +140,40 @@ open class ParentItemAdapter(
|
||||||
})
|
})
|
||||||
|
|
||||||
//(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
|
//(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
|
||||||
if (!isTrueTvSettings()) {
|
if (isLayout(PHONE)) {
|
||||||
title.setOnClickListener {
|
homeChildMoreInfo.setOnClickListener {
|
||||||
moreInfoClickCallback.invoke(expand)
|
moreInfoClickCallback.invoke(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateContent(parent: ViewGroup): ParentItemHolder {
|
||||||
|
val layoutResId = when {
|
||||||
|
isLayout(TV) -> R.layout.homepage_parent_tv
|
||||||
|
isLayout(EMULATOR) -> R.layout.homepage_parent_emulator
|
||||||
|
else -> R.layout.homepage_parent
|
||||||
|
}
|
||||||
|
|
||||||
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
val binding = try {
|
||||||
|
HomepageParentBinding.bind(inflater.inflate(layoutResId, parent, false))
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
logError(t)
|
||||||
|
// just in case someone forgot we don't want to crash
|
||||||
|
HomepageParentBinding.inflate(inflater)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParentItemHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateList(newList: List<HomePageList>) {
|
||||||
|
submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) }
|
||||||
|
.toMutableList())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchDiffCallback(
|
@Suppress("DEPRECATION")
|
||||||
private val oldList: List<HomeViewModel.ExpandableHomepageList>,
|
inline fun <reified T> Bundle.getSafeParcelable(key: String): T? =
|
||||||
private val newList: List<HomeViewModel.ExpandableHomepageList>
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getParcelable(key)
|
||||||
) :
|
else getParcelable(key, T::class.java)
|
||||||
DiffUtil.Callback() {
|
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
|
||||||
oldList[oldItemPosition].list.name == newList[newItemPosition].list.name
|
|
||||||
|
|
||||||
override fun getOldListSize() = oldList.size
|
|
||||||
|
|
||||||
override fun getNewListSize() = newList.size
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
|
||||||
oldList[oldItemPosition] == newList[newItemPosition]
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package com.lagradost.cloudstream3.ui.home
|
package com.lagradost.cloudstream3.ui.home
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
|
@ -7,13 +9,13 @@ import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.google.android.material.chip.ChipGroup
|
import com.google.android.material.chip.ChipGroup
|
||||||
import com.lagradost.cloudstream3.APIHolder.getId
|
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
|
||||||
import com.lagradost.cloudstream3.CommonActivity.activity
|
import com.lagradost.cloudstream3.CommonActivity.activity
|
||||||
import com.lagradost.cloudstream3.HomePageList
|
import com.lagradost.cloudstream3.HomePageList
|
||||||
|
|
@ -26,18 +28,21 @@ import com.lagradost.cloudstream3.databinding.FragmentHomeHeadTvBinding
|
||||||
import com.lagradost.cloudstream3.mvvm.Resource
|
import com.lagradost.cloudstream3.mvvm.Resource
|
||||||
import com.lagradost.cloudstream3.mvvm.debugException
|
import com.lagradost.cloudstream3.mvvm.debugException
|
||||||
import com.lagradost.cloudstream3.mvvm.observe
|
import com.lagradost.cloudstream3.mvvm.observe
|
||||||
|
import com.lagradost.cloudstream3.ui.ViewHolderState
|
||||||
import com.lagradost.cloudstream3.ui.WatchType
|
import com.lagradost.cloudstream3.ui.WatchType
|
||||||
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear
|
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear
|
||||||
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage
|
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage
|
||||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
|
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
|
||||||
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
|
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
|
||||||
|
import com.lagradost.cloudstream3.ui.result.getId
|
||||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
|
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
|
||||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
|
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes
|
||||||
|
|
@ -46,113 +51,84 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.populateChips
|
import com.lagradost.cloudstream3.utils.UIHelper.populateChips
|
||||||
|
|
||||||
class HomeParentItemAdapterPreview(
|
class HomeParentItemAdapterPreview(
|
||||||
items: MutableList<HomeViewModel.ExpandableHomepageList>,
|
override val fragment: Fragment,
|
||||||
private val viewModel: HomeViewModel,
|
private val viewModel: HomeViewModel,
|
||||||
) : ParentItemAdapter(items, clickCallback = {
|
) : ParentItemAdapter(fragment, id = "HomeParentItemAdapterPreview".hashCode(),
|
||||||
viewModel.click(it)
|
clickCallback = {
|
||||||
}, moreInfoClickCallback = {
|
viewModel.click(it)
|
||||||
viewModel.popup(it)
|
}, moreInfoClickCallback = {
|
||||||
}, expandCallback = {
|
viewModel.popup(it)
|
||||||
viewModel.expand(it)
|
}, expandCallback = {
|
||||||
}) {
|
viewModel.expand(it)
|
||||||
val headItems = 1
|
}) {
|
||||||
|
override val headers = 1
|
||||||
|
override fun onCreateHeader(parent: ViewGroup): ViewHolderState<Bundle> {
|
||||||
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
val binding = if (isLayout(TV or EMULATOR)) FragmentHomeHeadTvBinding.inflate(
|
||||||
|
inflater,
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
) else FragmentHomeHeadBinding.inflate(inflater, parent, false)
|
||||||
|
|
||||||
companion object {
|
if (binding is FragmentHomeHeadTvBinding && isLayout(EMULATOR)) {
|
||||||
private const val VIEW_TYPE_HEADER = 2
|
binding.homeBookmarkParentItemMoreInfo.isVisible = true
|
||||||
private const val VIEW_TYPE_ITEM = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) = when (position) {
|
val marginInDp = 50
|
||||||
0 -> VIEW_TYPE_HEADER
|
val density = binding.horizontalScrollChips.context.resources.displayMetrics.density
|
||||||
else -> VIEW_TYPE_ITEM
|
val marginInPixels = (marginInDp * density).toInt()
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
when (holder) {
|
params.marginEnd = marginInPixels
|
||||||
is HeaderViewHolder -> {}
|
binding.horizontalScrollChips.layoutParams = params
|
||||||
else -> super.onBindViewHolder(holder, position - headItems)
|
binding.homeWatchParentItemTitle.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
ContextCompat.getDrawable(
|
||||||
|
parent.context,
|
||||||
|
R.drawable.ic_baseline_arrow_forward_24
|
||||||
|
),
|
||||||
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return HeaderViewHolder(binding, viewModel, fragment = fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onBindHeader(holder: ViewHolderState<Bundle>) {
|
||||||
return when (viewType) {
|
(holder as? HeaderViewHolder)?.bind()
|
||||||
VIEW_TYPE_HEADER -> {
|
}
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
|
||||||
val binding = if (isTvSettings()) FragmentHomeHeadTvBinding.inflate(
|
|
||||||
inflater,
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
) else FragmentHomeHeadBinding.inflate(inflater, parent, false)
|
|
||||||
|
|
||||||
if (binding is FragmentHomeHeadTvBinding && parent.context.isEmulatorSettings()) {
|
private class HeaderViewHolder(
|
||||||
binding.homeBookmarkParentItemMoreInfo.isVisible = true
|
val binding: ViewBinding, val viewModel: HomeViewModel, fragment: Fragment,
|
||||||
|
) :
|
||||||
|
ViewHolderState<Bundle>(binding) {
|
||||||
|
|
||||||
val marginInDp = 50
|
override fun save(): Bundle =
|
||||||
val density = binding.horizontalScrollChips.context.resources.displayMetrics.density
|
Bundle().apply {
|
||||||
val marginInPixels = (marginInDp * density).toInt()
|
putParcelable(
|
||||||
|
"resumeRecyclerView",
|
||||||
val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams
|
resumeRecyclerView.layoutManager?.onSaveInstanceState()
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
|
putParcelable(
|
||||||
|
"bookmarkRecyclerView",
|
||||||
|
bookmarkRecyclerView.layoutManager?.onSaveInstanceState()
|
||||||
|
)
|
||||||
|
//putInt("previewViewpager", previewViewpager.currentItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
VIEW_TYPE_ITEM -> super.onCreateViewHolder(parent, viewType)
|
override fun restore(state: Bundle) {
|
||||||
else -> error("Unhandled viewType=$viewType")
|
state.getSafeParcelable<Parcelable>("resumeRecyclerView")?.let { recycle ->
|
||||||
}
|
resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return super.getItemCount() + headItems
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
if (position == 0) return 0//previewData.hashCode().toLong()
|
|
||||||
return super.getItemId(position - headItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
|
|
||||||
when (holder) {
|
|
||||||
is HeaderViewHolder -> {
|
|
||||||
holder.onViewDetachedFromWindow()
|
|
||||||
}
|
}
|
||||||
|
state.getSafeParcelable<Parcelable>("bookmarkRecyclerView")?.let { recycle ->
|
||||||
else -> super.onViewDetachedFromWindow(holder)
|
bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
|
|
||||||
when (holder) {
|
|
||||||
is HeaderViewHolder -> {
|
|
||||||
holder.onViewAttachedToWindow()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onViewAttachedToWindow(holder)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class HeaderViewHolder
|
val previewAdapter = HomeScrollAdapter(fragment = fragment)
|
||||||
constructor(
|
private val resumeAdapter = HomeChildItemAdapter(
|
||||||
val binding: ViewBinding,
|
fragment,
|
||||||
val viewModel: HomeViewModel,
|
id = "resumeAdapter".hashCode(),
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
private var previewAdapter: HomeScrollAdapter = HomeScrollAdapter()
|
|
||||||
private var resumeAdapter: HomeChildItemAdapter = HomeChildItemAdapter(
|
|
||||||
ArrayList(),
|
|
||||||
nextFocusUp = itemView.nextFocusUpId,
|
nextFocusUp = itemView.nextFocusUpId,
|
||||||
nextFocusDown = itemView.nextFocusDownId
|
nextFocusDown = itemView.nextFocusDownId
|
||||||
) { callback ->
|
) { callback ->
|
||||||
|
|
@ -207,8 +183,9 @@ class HomeParentItemAdapterPreview(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var bookmarkAdapter: HomeChildItemAdapter = HomeChildItemAdapter(
|
private val bookmarkAdapter = HomeChildItemAdapter(
|
||||||
ArrayList(),
|
fragment,
|
||||||
|
id = "bookmarkAdapter".hashCode(),
|
||||||
nextFocusUp = itemView.nextFocusUpId,
|
nextFocusUp = itemView.nextFocusUpId,
|
||||||
nextFocusDown = itemView.nextFocusDownId
|
nextFocusDown = itemView.nextFocusDownId
|
||||||
) { callback ->
|
) { callback ->
|
||||||
|
|
@ -217,7 +194,10 @@ class HomeParentItemAdapterPreview(
|
||||||
return@HomeChildItemAdapter
|
return@HomeChildItemAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
(callback.view.context?.getActivity() as? MainActivity)?.loadPopup(callback.card, load = false)
|
(callback.view.context?.getActivity() as? MainActivity)?.loadPopup(
|
||||||
|
callback.card,
|
||||||
|
load = false
|
||||||
|
)
|
||||||
/*
|
/*
|
||||||
callback.view.context?.getActivity()?.showOptionSelectStringRes(
|
callback.view.context?.getActivity()?.showOptionSelectStringRes(
|
||||||
callback.view,
|
callback.view,
|
||||||
|
|
@ -267,7 +247,6 @@ class HomeParentItemAdapterPreview(
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val previewViewpager: ViewPager2 =
|
private val previewViewpager: ViewPager2 =
|
||||||
itemView.findViewById(R.id.home_preview_viewpager)
|
itemView.findViewById(R.id.home_preview_viewpager)
|
||||||
|
|
||||||
|
|
@ -275,38 +254,24 @@ class HomeParentItemAdapterPreview(
|
||||||
itemView.findViewById(R.id.home_preview_viewpager_text)
|
itemView.findViewById(R.id.home_preview_viewpager_text)
|
||||||
|
|
||||||
// private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview)
|
// private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview)
|
||||||
private var resumeHolder: View = itemView.findViewById(R.id.home_watch_holder)
|
private val resumeHolder: View = itemView.findViewById(R.id.home_watch_holder)
|
||||||
private var resumeRecyclerView: RecyclerView =
|
private val resumeRecyclerView: RecyclerView =
|
||||||
itemView.findViewById(R.id.home_watch_child_recyclerview)
|
itemView.findViewById(R.id.home_watch_child_recyclerview)
|
||||||
private var bookmarkHolder: View = itemView.findViewById(R.id.home_bookmarked_holder)
|
private val bookmarkHolder: View = itemView.findViewById(R.id.home_bookmarked_holder)
|
||||||
private var bookmarkRecyclerView: RecyclerView =
|
private val bookmarkRecyclerView: RecyclerView =
|
||||||
itemView.findViewById(R.id.home_bookmarked_child_recyclerview)
|
itemView.findViewById(R.id.home_bookmarked_child_recyclerview)
|
||||||
|
|
||||||
private var homeAccount: View? =
|
private val homeAccount: View? = itemView.findViewById(R.id.home_preview_switch_account)
|
||||||
itemView.findViewById(R.id.home_preview_switch_account)
|
private val alternativeHomeAccount: View? =
|
||||||
private var alternativeHomeAccount: View? =
|
|
||||||
itemView.findViewById(R.id.alternative_switch_account)
|
itemView.findViewById(R.id.alternative_switch_account)
|
||||||
|
|
||||||
private var topPadding: View? = itemView.findViewById(R.id.home_padding)
|
private val topPadding: View? = itemView.findViewById(R.id.home_padding)
|
||||||
|
|
||||||
private var alternativeAccountPadding: View? = itemView.findViewById(R.id.alternative_account_padding)
|
private val alternativeAccountPadding: View? =
|
||||||
|
itemView.findViewById(R.id.alternative_account_padding)
|
||||||
|
|
||||||
private val homeNonePadding: View = itemView.findViewById(R.id.home_none_padding)
|
private val homeNonePadding: View = itemView.findViewById(R.id.home_none_padding)
|
||||||
|
|
||||||
private val previewCallback: ViewPager2.OnPageChangeCallback =
|
|
||||||
object : ViewPager2.OnPageChangeCallback() {
|
|
||||||
override fun onPageSelected(position: Int) {
|
|
||||||
previewAdapter.apply {
|
|
||||||
if (position >= itemCount - 1 && hasMoreItems) {
|
|
||||||
hasMoreItems = false // don't make two requests
|
|
||||||
viewModel.loadMoreHomeScrollResponses()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val item = previewAdapter.getItem(position) ?: return
|
|
||||||
onSelect(item, position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSelect(item: LoadResponse, position: Int) {
|
fun onSelect(item: LoadResponse, position: Int) {
|
||||||
(binding as? FragmentHomeHeadTvBinding)?.apply {
|
(binding as? FragmentHomeHeadTvBinding)?.apply {
|
||||||
homePreviewDescription.isGone =
|
homePreviewDescription.isGone =
|
||||||
|
|
@ -379,14 +344,14 @@ class HomeParentItemAdapterPreview(
|
||||||
|
|
||||||
homePreviewBookmark.setOnClickListener { fab ->
|
homePreviewBookmark.setOnClickListener { fab ->
|
||||||
fab.context.getActivity()?.showBottomDialog(
|
fab.context.getActivity()?.showBottomDialog(
|
||||||
WatchType.values()
|
WatchType.entries
|
||||||
.map { fab.context.getString(it.stringRes) }
|
.map { fab.context.getString(it.stringRes) }
|
||||||
.toList(),
|
.toList(),
|
||||||
DataStoreHelper.getResultWatchState(id).ordinal,
|
DataStoreHelper.getResultWatchState(id).ordinal,
|
||||||
fab.context.getString(R.string.action_add_to_bookmarks),
|
fab.context.getString(R.string.action_add_to_bookmarks),
|
||||||
showApply = false,
|
showApply = false,
|
||||||
{}) {
|
{}) {
|
||||||
val newValue = WatchType.values()[it]
|
val newValue = WatchType.entries[it]
|
||||||
|
|
||||||
ResultViewModel2().updateWatchStatus(
|
ResultViewModel2().updateWatchStatus(
|
||||||
newValue,
|
newValue,
|
||||||
|
|
@ -411,38 +376,22 @@ class HomeParentItemAdapterPreview(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onViewDetachedFromWindow() {
|
private val previewCallback: ViewPager2.OnPageChangeCallback =
|
||||||
previewViewpager.unregisterOnPageChangeCallback(previewCallback)
|
object : ViewPager2.OnPageChangeCallback() {
|
||||||
}
|
override fun onPageSelected(position: Int) {
|
||||||
|
previewAdapter.apply {
|
||||||
fun onViewAttachedToWindow() {
|
if (position >= itemCount - 1 && hasMoreItems) {
|
||||||
previewViewpager.registerOnPageChangeCallback(previewCallback)
|
hasMoreItems = false // don't make two requests
|
||||||
|
viewModel.loadMoreHomeScrollResponses()
|
||||||
binding.root.findViewTreeLifecycleOwner()?.apply {
|
|
||||||
observe(viewModel.preview) {
|
|
||||||
updatePreview(it)
|
|
||||||
}
|
|
||||||
if (binding is FragmentHomeHeadTvBinding) {
|
|
||||||
observe(viewModel.apiName) { name ->
|
|
||||||
binding.homePreviewChangeApi.text = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
observe(viewModel.resumeWatching) {
|
|
||||||
updateResume(it)
|
|
||||||
}
|
|
||||||
observe(viewModel.bookmarks) {
|
|
||||||
updateBookmarks(it)
|
|
||||||
}
|
|
||||||
observe(viewModel.availableWatchStatusTypes) { (checked, visible) ->
|
|
||||||
for ((chip, watch) in toggleList) {
|
|
||||||
chip.apply {
|
|
||||||
isVisible = visible.contains(watch)
|
|
||||||
isChecked = checked.contains(watch)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toggleListHolder?.isGone = visible.isEmpty()
|
val item = previewAdapter.getItemOrNull(position) ?: return
|
||||||
|
onSelect(item, position)
|
||||||
}
|
}
|
||||||
} ?: debugException { "Expected findViewTreeLifecycleOwner" }
|
}
|
||||||
|
|
||||||
|
override fun onViewDetachedFromWindow() {
|
||||||
|
previewViewpager.unregisterOnPageChangeCallback(previewCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val toggleList = listOf<Pair<Chip, WatchType>>(
|
private val toggleList = listOf<Pair<Chip, WatchType>>(
|
||||||
|
|
@ -455,6 +404,8 @@ class HomeParentItemAdapterPreview(
|
||||||
|
|
||||||
private val toggleListHolder: ChipGroup? = itemView.findViewById(R.id.home_type_holder)
|
private val toggleListHolder: ChipGroup? = itemView.findViewById(R.id.home_type_holder)
|
||||||
|
|
||||||
|
fun bind() = Unit
|
||||||
|
|
||||||
init {
|
init {
|
||||||
previewViewpager.setPageTransformer(HomeScrollTransformer())
|
previewViewpager.setPageTransformer(HomeScrollTransformer())
|
||||||
|
|
||||||
|
|
@ -561,7 +512,9 @@ class HomeParentItemAdapterPreview(
|
||||||
|
|
||||||
when (preview) {
|
when (preview) {
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
if (!previewAdapter.setItems(
|
previewAdapter.submitList(preview.value.second)
|
||||||
|
previewAdapter.hasMoreItems = preview.value.first
|
||||||
|
/*if (!.setItems(
|
||||||
preview.value.second,
|
preview.value.second,
|
||||||
preview.value.first
|
preview.value.first
|
||||||
)
|
)
|
||||||
|
|
@ -573,15 +526,16 @@ class HomeParentItemAdapterPreview(
|
||||||
previewViewpager.fakeDragBy(1f)
|
previewViewpager.fakeDragBy(1f)
|
||||||
previewViewpager.endFakeDrag()
|
previewViewpager.endFakeDrag()
|
||||||
previewCallback.onPageSelected(0)
|
previewCallback.onPageSelected(0)
|
||||||
previewViewpager.isVisible = true
|
|
||||||
previewViewpagerText.isVisible = true
|
|
||||||
alternativeAccountPadding?.isVisible = false
|
|
||||||
//previewHeader.isVisible = true
|
//previewHeader.isVisible = true
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
previewViewpager.isVisible = true
|
||||||
|
previewViewpagerText.isVisible = true
|
||||||
|
alternativeAccountPadding?.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
previewAdapter.setItems(listOf(), false)
|
previewAdapter.submitList(listOf())
|
||||||
previewViewpager.setCurrentItem(0, false)
|
previewViewpager.setCurrentItem(0, false)
|
||||||
previewViewpager.isVisible = false
|
previewViewpager.isVisible = false
|
||||||
previewViewpagerText.isVisible = false
|
previewViewpagerText.isVisible = false
|
||||||
|
|
@ -593,12 +547,12 @@ class HomeParentItemAdapterPreview(
|
||||||
|
|
||||||
private fun updateResume(resumeWatching: List<SearchResponse>) {
|
private fun updateResume(resumeWatching: List<SearchResponse>) {
|
||||||
resumeHolder.isVisible = resumeWatching.isNotEmpty()
|
resumeHolder.isVisible = resumeWatching.isNotEmpty()
|
||||||
resumeAdapter.updateList(resumeWatching)
|
resumeAdapter.submitList(resumeWatching)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
binding is FragmentHomeHeadBinding ||
|
binding is FragmentHomeHeadBinding ||
|
||||||
binding is FragmentHomeHeadTvBinding &&
|
binding is FragmentHomeHeadTvBinding &&
|
||||||
binding.root.context.isEmulatorSettings()
|
isLayout(EMULATOR)
|
||||||
) {
|
) {
|
||||||
val title = (binding as? FragmentHomeHeadBinding)?.homeWatchParentItemTitle
|
val title = (binding as? FragmentHomeHeadBinding)?.homeWatchParentItemTitle
|
||||||
?: (binding as? FragmentHomeHeadTvBinding)?.homeWatchParentItemTitle
|
?: (binding as? FragmentHomeHeadTvBinding)?.homeWatchParentItemTitle
|
||||||
|
|
@ -623,12 +577,12 @@ class HomeParentItemAdapterPreview(
|
||||||
private fun updateBookmarks(data: Pair<Boolean, List<SearchResponse>>) {
|
private fun updateBookmarks(data: Pair<Boolean, List<SearchResponse>>) {
|
||||||
val (visible, list) = data
|
val (visible, list) = data
|
||||||
bookmarkHolder.isVisible = visible
|
bookmarkHolder.isVisible = visible
|
||||||
bookmarkAdapter.updateList(list)
|
bookmarkAdapter.submitList(list)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
binding is FragmentHomeHeadBinding ||
|
binding is FragmentHomeHeadBinding ||
|
||||||
binding is FragmentHomeHeadTvBinding &&
|
binding is FragmentHomeHeadTvBinding &&
|
||||||
binding.root.context.isEmulatorSettings()
|
isLayout(EMULATOR)
|
||||||
) {
|
) {
|
||||||
val title = (binding as? FragmentHomeHeadBinding)?.homeBookmarkParentItemTitle
|
val title = (binding as? FragmentHomeHeadBinding)?.homeBookmarkParentItemTitle
|
||||||
?: (binding as? FragmentHomeHeadTvBinding)?.homeBookmarkParentItemTitle
|
?: (binding as? FragmentHomeHeadTvBinding)?.homeBookmarkParentItemTitle
|
||||||
|
|
@ -653,5 +607,35 @@ class HomeParentItemAdapterPreview(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onViewAttachedToWindow() {
|
||||||
|
previewViewpager.registerOnPageChangeCallback(previewCallback)
|
||||||
|
|
||||||
|
binding.root.findViewTreeLifecycleOwner()?.apply {
|
||||||
|
observe(viewModel.preview) {
|
||||||
|
updatePreview(it)
|
||||||
|
}
|
||||||
|
if (binding is FragmentHomeHeadTvBinding) {
|
||||||
|
observe(viewModel.apiName) { name ->
|
||||||
|
binding.homePreviewChangeApi.text = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
observe(viewModel.resumeWatching) {
|
||||||
|
updateResume(it)
|
||||||
|
}
|
||||||
|
observe(viewModel.bookmarks) {
|
||||||
|
updateBookmarks(it)
|
||||||
|
}
|
||||||
|
observe(viewModel.availableWatchStatusTypes) { (checked, visible) ->
|
||||||
|
for ((chip, watch) in toggleList) {
|
||||||
|
chip.apply {
|
||||||
|
isVisible = visible.contains(watch)
|
||||||
|
isChecked = checked.contains(watch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toggleListHolder?.isGone = visible.isEmpty()
|
||||||
|
}
|
||||||
|
} ?: debugException { "Expected findViewTreeLifecycleOwner" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,112 +4,61 @@ import android.content.res.Configuration
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
import com.lagradost.cloudstream3.LoadResponse
|
import com.lagradost.cloudstream3.LoadResponse
|
||||||
import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding
|
import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding
|
||||||
import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding
|
import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
import com.lagradost.cloudstream3.ui.NoStateAdapter
|
||||||
|
import com.lagradost.cloudstream3.ui.ViewHolderState
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
|
|
||||||
class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
class HomeScrollAdapter(
|
||||||
private var items: MutableList<LoadResponse> = mutableListOf()
|
fragment: Fragment
|
||||||
|
) : NoStateAdapter<LoadResponse>(fragment) {
|
||||||
var hasMoreItems: Boolean = false
|
var hasMoreItems: Boolean = false
|
||||||
|
|
||||||
fun getItem(position: Int): LoadResponse? {
|
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Any> {
|
||||||
return items.getOrNull(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setItems(newItems: List<LoadResponse>, hasNext: Boolean): Boolean {
|
|
||||||
val isSame = newItems.firstOrNull()?.url == items.firstOrNull()?.url
|
|
||||||
hasMoreItems = hasNext
|
|
||||||
|
|
||||||
val diffResult = DiffUtil.calculateDiff(
|
|
||||||
HomeScrollDiffCallback(this.items, newItems)
|
|
||||||
)
|
|
||||||
|
|
||||||
items.clear()
|
|
||||||
items.addAll(newItems)
|
|
||||||
|
|
||||||
|
|
||||||
diffResult.dispatchUpdatesTo(this)
|
|
||||||
|
|
||||||
return isSame
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
val binding = if (isTvSettings()) {
|
val binding = if (isLayout(TV or EMULATOR)) {
|
||||||
HomeScrollViewTvBinding.inflate(inflater, parent, false)
|
HomeScrollViewTvBinding.inflate(inflater, parent, false)
|
||||||
} else {
|
} else {
|
||||||
HomeScrollViewBinding.inflate(inflater, parent, false)
|
HomeScrollViewBinding.inflate(inflater, parent, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return CardViewHolder(
|
return ViewHolderState(binding)
|
||||||
binding,
|
|
||||||
//forceHorizontalPosters
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindContent(
|
||||||
when (holder) {
|
holder: ViewHolderState<Any>,
|
||||||
is CardViewHolder -> {
|
item: LoadResponse,
|
||||||
holder.bind(items[position])
|
position: Int,
|
||||||
|
) {
|
||||||
|
val binding = holder.view
|
||||||
|
val itemView = holder.itemView
|
||||||
|
val isHorizontal =
|
||||||
|
binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
|
||||||
|
val posterUrl =
|
||||||
|
if (isHorizontal) item.backgroundPosterUrl ?: item.posterUrl else item.posterUrl
|
||||||
|
?: item.backgroundPosterUrl
|
||||||
|
|
||||||
|
when (binding) {
|
||||||
|
is HomeScrollViewBinding -> {
|
||||||
|
binding.homeScrollPreview.setImage(posterUrl)
|
||||||
|
binding.homeScrollPreviewTags.apply {
|
||||||
|
text = item.tags?.joinToString(" • ") ?: ""
|
||||||
|
isGone = item.tags.isNullOrEmpty()
|
||||||
|
maxLines = 2
|
||||||
|
}
|
||||||
|
binding.homeScrollPreviewTitle.text = item.name
|
||||||
|
}
|
||||||
|
|
||||||
|
is HomeScrollViewTvBinding -> {
|
||||||
|
binding.homeScrollPreview.setImage(posterUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CardViewHolder
|
|
||||||
constructor(
|
|
||||||
val binding: ViewBinding,
|
|
||||||
//private val forceHorizontalPosters: Boolean? = null
|
|
||||||
) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun bind(card: LoadResponse) {
|
|
||||||
val isHorizontal =
|
|
||||||
binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
|
||||||
|
|
||||||
val posterUrl =
|
|
||||||
if (isHorizontal) card.backgroundPosterUrl ?: card.posterUrl else card.posterUrl
|
|
||||||
?: card.backgroundPosterUrl
|
|
||||||
|
|
||||||
when (binding) {
|
|
||||||
is HomeScrollViewBinding -> {
|
|
||||||
binding.homeScrollPreview.setImage(posterUrl)
|
|
||||||
binding.homeScrollPreviewTags.apply {
|
|
||||||
text = card.tags?.joinToString(" • ") ?: ""
|
|
||||||
isGone = card.tags.isNullOrEmpty()
|
|
||||||
maxLines = 2
|
|
||||||
}
|
|
||||||
binding.homeScrollPreviewTitle.text = card.name
|
|
||||||
}
|
|
||||||
|
|
||||||
is HomeScrollViewTvBinding -> {
|
|
||||||
binding.homeScrollPreview.setImage(posterUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class HomeScrollDiffCallback(
|
|
||||||
private val oldList: List<LoadResponse>,
|
|
||||||
private val newList: List<LoadResponse>
|
|
||||||
) :
|
|
||||||
DiffUtil.Callback() {
|
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
|
||||||
oldList[oldItemPosition].url == newList[newItemPosition].url
|
|
||||||
|
|
||||||
override fun getOldListSize() = oldList.size
|
|
||||||
|
|
||||||
override fun getNewListSize() = newList.size
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
|
||||||
oldList[oldItemPosition] == newList[newItemPosition]
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return items.size
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -6,9 +6,6 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.lagradost.cloudstream3.APIHolder.apis
|
import com.lagradost.cloudstream3.APIHolder.apis
|
||||||
import com.lagradost.cloudstream3.APIHolder.filterHomePageListByFilmQuality
|
|
||||||
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
|
|
||||||
import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality
|
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
|
|
@ -34,9 +31,13 @@ import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
|
||||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
|
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
import com.lagradost.cloudstream3.utils.AppContextUtils.addProgramsToContinueWatching
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.filterHomePageListByFilmQuality
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.filterSearchResultByFilmQuality
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
|
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
|
|
@ -52,6 +53,7 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.EnumSet
|
import java.util.EnumSet
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
|
|
||||||
class HomeViewModel : ViewModel() {
|
class HomeViewModel : ViewModel() {
|
||||||
|
|
@ -124,7 +126,7 @@ class HomeViewModel : ViewModel() {
|
||||||
|
|
||||||
private val _resumeWatching = MutableLiveData<List<SearchResponse>>()
|
private val _resumeWatching = MutableLiveData<List<SearchResponse>>()
|
||||||
private val _preview = MutableLiveData<Resource<Pair<Boolean, List<LoadResponse>>>>()
|
private val _preview = MutableLiveData<Resource<Pair<Boolean, List<LoadResponse>>>>()
|
||||||
private val previewResponses = mutableListOf<LoadResponse>()
|
private val previewResponses = CopyOnWriteArrayList<LoadResponse>()
|
||||||
private val previewResponsesAdded = mutableSetOf<String>()
|
private val previewResponsesAdded = mutableSetOf<String>()
|
||||||
|
|
||||||
val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching
|
val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching
|
||||||
|
|
@ -132,7 +134,7 @@ class HomeViewModel : ViewModel() {
|
||||||
|
|
||||||
private fun loadResumeWatching() = viewModelScope.launchSafe {
|
private fun loadResumeWatching() = viewModelScope.launchSafe {
|
||||||
val resumeWatchingResult = getResumeWatching()
|
val resumeWatchingResult = getResumeWatching()
|
||||||
if (isTrueTvSettings() && resumeWatchingResult != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (isLayout(TV) && resumeWatchingResult != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
ioSafe {
|
ioSafe {
|
||||||
// this WILL crash on non tvs, so keep this inside a try catch
|
// this WILL crash on non tvs, so keep this inside a try catch
|
||||||
activity?.addProgramsToContinueWatching(resumeWatchingResult)
|
activity?.addProgramsToContinueWatching(resumeWatchingResult)
|
||||||
|
|
@ -150,7 +152,7 @@ class HomeViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}?.distinctBy { it.first } ?: return@launchSafe
|
}?.distinctBy { it.first } ?: return@launchSafe
|
||||||
|
|
||||||
val length = WatchType.values().size
|
val length = WatchType.entries.size
|
||||||
val currentWatchTypes = mutableSetOf<WatchType>()
|
val currentWatchTypes = mutableSetOf<WatchType>()
|
||||||
|
|
||||||
for (watch in watchStatusIds) {
|
for (watch in watchStatusIds) {
|
||||||
|
|
@ -326,7 +328,13 @@ class HomeViewModel : ViewModel() {
|
||||||
val filteredList =
|
val filteredList =
|
||||||
context?.filterHomePageListByFilmQuality(list) ?: list
|
context?.filterHomePageListByFilmQuality(list) ?: list
|
||||||
expandable[list.name] =
|
expandable[list.name] =
|
||||||
ExpandableHomepageList(filteredList, 1, home.hasNext)
|
ExpandableHomepageList(
|
||||||
|
filteredList.copy(
|
||||||
|
list = CopyOnWriteArrayList(
|
||||||
|
filteredList.list
|
||||||
|
)
|
||||||
|
), 1, home.hasNext
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -341,8 +349,7 @@ class HomeViewModel : ViewModel() {
|
||||||
val currentList =
|
val currentList =
|
||||||
items.shuffled().filter { it.list.isNotEmpty() }
|
items.shuffled().filter { it.list.isNotEmpty() }
|
||||||
.flatMap { it.list }
|
.flatMap { it.list }
|
||||||
.distinctBy { it.url }
|
.distinctBy { it.url }.toList()
|
||||||
.toList()
|
|
||||||
|
|
||||||
if (currentList.isNotEmpty()) {
|
if (currentList.isNotEmpty()) {
|
||||||
val randomItems =
|
val randomItems =
|
||||||
|
|
@ -380,7 +387,9 @@ class HomeViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
is Resource.Failure -> {
|
is Resource.Failure -> {
|
||||||
|
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
|
||||||
_page.postValue(data!!)
|
_page.postValue(data!!)
|
||||||
|
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
|
||||||
_preview.postValue(data!!)
|
_preview.postValue(data!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -390,9 +399,7 @@ class HomeViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun click(callback: SearchClickCallback) {
|
fun click(callback: SearchClickCallback) {
|
||||||
if (callback.action == SEARCH_ACTION_FOCUSED) {
|
if (callback.action != SEARCH_ACTION_FOCUSED) {
|
||||||
//focusCallback(callback.card)
|
|
||||||
} else {
|
|
||||||
SearchHelper.handleSearchClickCallback(callback)
|
SearchHelper.handleSearchClickCallback(callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -509,7 +516,7 @@ class HomeViewModel : ViewModel() {
|
||||||
} else {
|
} else {
|
||||||
_page.postValue(Resource.Loading())
|
_page.postValue(Resource.Loading())
|
||||||
if (preferredApiName != null)
|
if (preferredApiName != null)
|
||||||
_apiName.postValue(preferredApiName)
|
_apiName.postValue(preferredApiName!!)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if the api is found, then set it to it and save key
|
// if the api is found, then set it to it and save key
|
||||||
|
|
|
||||||
|
|
@ -49,14 +49,18 @@ import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
|
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
|
||||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
|
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.reduceDragSensitivity
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
const val LIBRARY_FOLDER = "library_folder"
|
const val LIBRARY_FOLDER = "library_folder"
|
||||||
|
|
@ -101,7 +105,7 @@ class LibraryFragment : Fragment() {
|
||||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
val layout =
|
val layout =
|
||||||
if (SettingsFragment.isTvSettings()) R.layout.fragment_library_tv else R.layout.fragment_library
|
if (isLayout(TV or EMULATOR)) R.layout.fragment_library_tv else R.layout.fragment_library
|
||||||
val root = inflater.inflate(layout, container, false)
|
val root = inflater.inflate(layout, container, false)
|
||||||
binding = try {
|
binding = try {
|
||||||
FragmentLibraryBinding.bind(root)
|
FragmentLibraryBinding.bind(root)
|
||||||
|
|
@ -131,6 +135,18 @@ class LibraryFragment : Fragment() {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateRandom() {
|
||||||
|
val position = libraryViewModel.currentPage.value ?: 0
|
||||||
|
val pages = (libraryViewModel.pages.value as? Resource.Success)?.value ?: return
|
||||||
|
if (toggleRandomButton) {
|
||||||
|
listLibraryItems.clear()
|
||||||
|
listLibraryItems.addAll(pages[position].items)
|
||||||
|
binding?.libraryRandom?.isVisible = listLibraryItems.isNotEmpty()
|
||||||
|
} else {
|
||||||
|
binding?.libraryRandom?.isGone = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("ResourceType", "CutPasteId")
|
@SuppressLint("ResourceType", "CutPasteId")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
@ -148,7 +164,8 @@ class LibraryFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the color for the search exit icon to the correct theme text color
|
// 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 searchExitIcon =
|
||||||
|
binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
|
||||||
val searchExitIconColor = TypedValue()
|
val searchExitIconColor = TypedValue()
|
||||||
|
|
||||||
activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true)
|
activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true)
|
||||||
|
|
@ -208,7 +225,7 @@ class LibraryFragment : Fragment() {
|
||||||
settingsManager.getBoolean(
|
settingsManager.getBoolean(
|
||||||
getString(R.string.random_button_key),
|
getString(R.string.random_button_key),
|
||||||
false
|
false
|
||||||
) && !SettingsFragment.isTvSettings()
|
) && isLayout(PHONE)
|
||||||
binding?.libraryRandom?.visibility = View.GONE
|
binding?.libraryRandom?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,7 +233,7 @@ class LibraryFragment : Fragment() {
|
||||||
if (listLibraryItems.isNotEmpty()) {
|
if (listLibraryItems.isNotEmpty()) {
|
||||||
val listLibraryItem = listLibraryItems.random()
|
val listLibraryItem = listLibraryItems.random()
|
||||||
libraryViewModel.currentSyncApi?.syncIdName?.let {
|
libraryViewModel.currentSyncApi?.syncIdName?.let {
|
||||||
loadLibraryItem(it, listLibraryItem.syncId,listLibraryItem)
|
loadLibraryItem(it, listLibraryItem.syncId, listLibraryItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -295,44 +312,46 @@ class LibraryFragment : Fragment() {
|
||||||
|
|
||||||
binding?.viewpager?.setPageTransformer(LibraryScrollTransformer())
|
binding?.viewpager?.setPageTransformer(LibraryScrollTransformer())
|
||||||
|
|
||||||
binding?.viewpager?.adapter =
|
binding?.viewpager?.adapter = ViewpagerAdapter(
|
||||||
binding?.viewpager?.adapter ?: ViewpagerAdapter(
|
fragment = this,
|
||||||
mutableListOf(),
|
{ isScrollingDown: Boolean ->
|
||||||
{ isScrollingDown: Boolean ->
|
if (isScrollingDown) {
|
||||||
if (isScrollingDown) {
|
binding?.sortFab?.shrink()
|
||||||
binding?.sortFab?.shrink()
|
binding?.libraryRandom?.shrink()
|
||||||
binding?.libraryRandom?.shrink()
|
} else {
|
||||||
} else {
|
binding?.sortFab?.extend()
|
||||||
binding?.sortFab?.extend()
|
binding?.libraryRandom?.extend()
|
||||||
binding?.libraryRandom?.extend()
|
}
|
||||||
}
|
}) callback@{ searchClickCallback ->
|
||||||
}) callback@{ searchClickCallback ->
|
// To prevent future accidents
|
||||||
// To prevent future accidents
|
debugAssert({
|
||||||
debugAssert({
|
searchClickCallback.card !is SyncAPI.LibraryItem
|
||||||
searchClickCallback.card !is SyncAPI.LibraryItem
|
}, {
|
||||||
}, {
|
"searchClickCallback ${searchClickCallback.card} is not a LibraryItem"
|
||||||
"searchClickCallback ${searchClickCallback.card} is not a LibraryItem"
|
})
|
||||||
})
|
|
||||||
|
|
||||||
val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId
|
val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId
|
||||||
val syncName =
|
val syncName =
|
||||||
libraryViewModel.currentSyncApi?.syncIdName ?: return@callback
|
libraryViewModel.currentSyncApi?.syncIdName ?: return@callback
|
||||||
|
|
||||||
when (searchClickCallback.action) {
|
when (searchClickCallback.action) {
|
||||||
SEARCH_ACTION_SHOW_METADATA -> {
|
SEARCH_ACTION_SHOW_METADATA -> {
|
||||||
(activity as? MainActivity)?.loadPopup(searchClickCallback.card, load = false)
|
(activity as? MainActivity)?.loadPopup(
|
||||||
|
searchClickCallback.card,
|
||||||
|
load = false
|
||||||
|
)
|
||||||
/*activity?.showPluginSelectionDialog(
|
/*activity?.showPluginSelectionDialog(
|
||||||
syncId,
|
syncId,
|
||||||
syncName,
|
syncName,
|
||||||
searchClickCallback.card.apiName
|
searchClickCallback.card.apiName
|
||||||
)*/
|
)*/
|
||||||
}
|
}
|
||||||
|
|
||||||
SEARCH_ACTION_LOAD -> {
|
SEARCH_ACTION_LOAD -> {
|
||||||
loadLibraryItem(syncName, syncId, searchClickCallback.card)
|
loadLibraryItem(syncName, syncId, searchClickCallback.card)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
binding?.apply {
|
binding?.apply {
|
||||||
viewpager.offscreenPageLimit = 2
|
viewpager.offscreenPageLimit = 2
|
||||||
|
|
@ -378,7 +397,11 @@ class LibraryFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(viewpager.adapter as? ViewpagerAdapter)?.pages = pages
|
(viewpager.adapter as? ViewpagerAdapter)?.submitList(pages.map {
|
||||||
|
it.copy(
|
||||||
|
items = CopyOnWriteArrayList(it.items)
|
||||||
|
)
|
||||||
|
})
|
||||||
//fix focus on the viewpager itself
|
//fix focus on the viewpager itself
|
||||||
(viewpager.getChildAt(0) as RecyclerView).apply {
|
(viewpager.getChildAt(0) as RecyclerView).apply {
|
||||||
tag = "tv_no_focus_tag"
|
tag = "tv_no_focus_tag"
|
||||||
|
|
@ -386,24 +409,16 @@ class LibraryFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Using notifyItemRangeChanged keeps the animations when sorting
|
// Using notifyItemRangeChanged keeps the animations when sorting
|
||||||
viewpager.adapter?.notifyItemRangeChanged(
|
/*viewpager.adapter?.notifyItemRangeChanged(
|
||||||
0,
|
0,
|
||||||
viewpager.adapter?.itemCount ?: 0
|
viewpager.adapter?.itemCount ?: 0
|
||||||
)
|
)*/
|
||||||
|
|
||||||
libraryViewModel.currentPage.value?.let { page ->
|
libraryViewModel.currentPage.value?.let { page ->
|
||||||
binding?.viewpager?.setCurrentItem(page, false)
|
binding?.viewpager?.setCurrentItem(page, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
observe(libraryViewModel.currentPage){
|
updateRandom()
|
||||||
if (toggleRandomButton) {
|
|
||||||
listLibraryItems.clear()
|
|
||||||
listLibraryItems.addAll(pages[it].items)
|
|
||||||
libraryRandom.isVisible = listLibraryItems.isNotEmpty()
|
|
||||||
} else {
|
|
||||||
libraryRandom.isGone = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only stop loading after 300ms to hide the fade effect the viewpager produces when updating
|
// Only stop loading after 300ms to hide the fade effect the viewpager produces when updating
|
||||||
// Without this there would be a flashing effect:
|
// Without this there would be a flashing effect:
|
||||||
|
|
@ -455,12 +470,14 @@ class LibraryFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}.attach()
|
}.attach()
|
||||||
|
|
||||||
binding?.libraryTabLayout?.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener {
|
binding?.libraryTabLayout?.addOnTabSelectedListener(object :
|
||||||
|
TabLayout.OnTabSelectedListener {
|
||||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||||
binding?.libraryTabLayout?.selectedTabPosition?.let { page ->
|
binding?.libraryTabLayout?.selectedTabPosition?.let { page ->
|
||||||
libraryViewModel.switchPage(page)
|
libraryViewModel.switchPage(page)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
|
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
|
||||||
override fun onTabReselected(tab: TabLayout.Tab?) = Unit
|
override fun onTabReselected(tab: TabLayout.Tab?) = Unit
|
||||||
})
|
})
|
||||||
|
|
@ -481,6 +498,7 @@ class LibraryFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
observe(libraryViewModel.currentPage) { position ->
|
observe(libraryViewModel.currentPage) { position ->
|
||||||
|
updateRandom()
|
||||||
val all = binding?.viewpager?.allViews?.toList()
|
val all = binding?.viewpager?.allViews?.toList()
|
||||||
?.filterIsInstance<AutofitRecyclerView>()
|
?.filterIsInstance<AutofitRecyclerView>()
|
||||||
|
|
||||||
|
|
@ -559,8 +577,9 @@ class LibraryFragment : Fragment() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
(binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind()
|
binding?.viewpager?.adapter?.notifyDataSetChanged()
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -581,8 +600,4 @@ class LibraryFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MenuSearchView(context: Context) : SearchView(context) {
|
class MenuSearchView(context: Context) : SearchView(context)
|
||||||
override fun onActionViewCollapsed() {
|
|
||||||
super.onActionViewCollapsed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -23,6 +23,8 @@ enum class ListSorting(@StringRes val stringRes: Int) {
|
||||||
UpdatedOld(R.string.sort_updated_old),
|
UpdatedOld(R.string.sort_updated_old),
|
||||||
AlphabeticalA(R.string.sort_alphabetical_a),
|
AlphabeticalA(R.string.sort_alphabetical_a),
|
||||||
AlphabeticalZ(R.string.sort_alphabetical_z),
|
AlphabeticalZ(R.string.sort_alphabetical_z),
|
||||||
|
ReleaseDateNew(R.string.sort_release_date_new),
|
||||||
|
ReleaseDateOld(R.string.sort_release_date_old),
|
||||||
}
|
}
|
||||||
|
|
||||||
const val LAST_SYNC_API_KEY = "last_sync_api"
|
const val LAST_SYNC_API_KEY = "last_sync_api"
|
||||||
|
|
@ -113,7 +115,7 @@ class LibraryViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
val desiredSortingMethod =
|
val desiredSortingMethod =
|
||||||
ListSorting.values().getOrNull(DataStoreHelper.librarySortingMode)
|
ListSorting.entries.getOrNull(DataStoreHelper.librarySortingMode)
|
||||||
if (desiredSortingMethod != null && library.supportedListSorting.contains(desiredSortingMethod)) {
|
if (desiredSortingMethod != null && library.supportedListSorting.contains(desiredSortingMethod)) {
|
||||||
sort(desiredSortingMethod, null, pages)
|
sort(desiredSortingMethod, null, pages)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -132,4 +134,4 @@ class LibraryViewModel : ViewModel() {
|
||||||
MainActivity.reloadLibraryEvent -= ::reloadPages
|
MainActivity.reloadLibraryEvent -= ::reloadPages
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||||
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
|
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils
|
import com.lagradost.cloudstream3.utils.AppContextUtils
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ class PageAdapter(
|
||||||
private val resView: AutofitRecyclerView,
|
private val resView: AutofitRecyclerView,
|
||||||
val clickCallback: (SearchClickCallback) -> Unit
|
val clickCallback: (SearchClickCallback) -> Unit
|
||||||
) :
|
) :
|
||||||
AppUtils.DiffAdapter<SyncAPI.LibraryItem>(items) {
|
AppContextUtils.DiffAdapter<SyncAPI.LibraryItem>(items) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return LibraryItemViewHolder(
|
return LibraryItemViewHolder(
|
||||||
|
|
|
||||||
|
|
@ -1,104 +1,124 @@
|
||||||
package com.lagradost.cloudstream3.ui.library
|
package com.lagradost.cloudstream3.ui.library
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.doOnAttach
|
import androidx.core.view.doOnAttach
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.recyclerview.widget.RecyclerView.OnFlingListener
|
import androidx.recyclerview.widget.RecyclerView.OnFlingListener
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.databinding.LibraryViewpagerPageBinding
|
import com.lagradost.cloudstream3.databinding.LibraryViewpagerPageBinding
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||||
|
import com.lagradost.cloudstream3.ui.BaseAdapter
|
||||||
|
import com.lagradost.cloudstream3.ui.BaseDiffCallback
|
||||||
|
import com.lagradost.cloudstream3.ui.ViewHolderState
|
||||||
|
import com.lagradost.cloudstream3.ui.home.getSafeParcelable
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
||||||
|
|
||||||
|
class ViewpagerAdapterViewHolderState(val binding: LibraryViewpagerPageBinding) :
|
||||||
|
ViewHolderState<Bundle>(binding) {
|
||||||
|
override fun save(): Bundle =
|
||||||
|
Bundle().apply {
|
||||||
|
putParcelable(
|
||||||
|
"pageRecyclerview",
|
||||||
|
binding.pageRecyclerview.layoutManager?.onSaveInstanceState()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun restore(state: Bundle) {
|
||||||
|
state.getSafeParcelable<Parcelable>("pageRecyclerview")?.let { recycle ->
|
||||||
|
binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ViewpagerAdapter(
|
class ViewpagerAdapter(
|
||||||
var pages: List<SyncAPI.Page>,
|
fragment: Fragment,
|
||||||
val scrollCallback: (isScrollingDown: Boolean) -> Unit,
|
val scrollCallback: (isScrollingDown: Boolean) -> Unit,
|
||||||
val clickCallback: (SearchClickCallback) -> Unit
|
val clickCallback: (SearchClickCallback) -> Unit
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
) : BaseAdapter<SyncAPI.Page, Bundle>(fragment,
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
id = "ViewpagerAdapter".hashCode(),
|
||||||
return PageViewHolder(
|
diffCallback = BaseDiffCallback(
|
||||||
|
itemSame = { a, b ->
|
||||||
|
a.title == b.title
|
||||||
|
},
|
||||||
|
contentSame = { a, b ->
|
||||||
|
a.items == b.items && a.title == b.title
|
||||||
|
}
|
||||||
|
)) {
|
||||||
|
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Bundle> {
|
||||||
|
return ViewpagerAdapterViewHolderState(
|
||||||
LibraryViewpagerPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
LibraryViewpagerPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onUpdateContent(
|
||||||
when (holder) {
|
holder: ViewHolderState<Bundle>,
|
||||||
is PageViewHolder -> {
|
item: SyncAPI.Page,
|
||||||
holder.bind(pages[position], position, unbound.remove(position))
|
position: Int
|
||||||
}
|
) {
|
||||||
}
|
val binding = holder.view
|
||||||
|
if (binding !is LibraryViewpagerPageBinding) return
|
||||||
|
(binding.pageRecyclerview.adapter as? PageAdapter)?.updateList(item.items)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val unbound = mutableSetOf<Int>()
|
override fun onBindContent(holder: ViewHolderState<Bundle>, item: SyncAPI.Page, position: Int) {
|
||||||
|
val binding = holder.view
|
||||||
|
if (binding !is LibraryViewpagerPageBinding) return
|
||||||
|
|
||||||
/**
|
binding.pageRecyclerview.tag = position
|
||||||
* Used to mark all pages for re-binding and forces all items to be refreshed
|
binding.pageRecyclerview.apply {
|
||||||
* Without this the pages will still use the same adapters
|
spanCount =
|
||||||
**/
|
binding.root.context.getSpanCount() ?: 3
|
||||||
fun rebind() {
|
if (adapter == null) { // || rebind
|
||||||
unbound.addAll(0..pages.size)
|
// Only add the items after it has been attached since the items rely on ItemWidth
|
||||||
this.notifyItemRangeChanged(0, pages.size)
|
// Which is only determined after the recyclerview is attached.
|
||||||
}
|
// If this fails then item height becomes 0 when there is only one item
|
||||||
|
doOnAttach {
|
||||||
inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) :
|
adapter = PageAdapter(
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
item.items.toMutableList(),
|
||||||
fun bind(page: SyncAPI.Page, position: Int, rebind: Boolean) {
|
this,
|
||||||
binding.pageRecyclerview.tag = position
|
clickCallback
|
||||||
binding.pageRecyclerview.apply {
|
)
|
||||||
spanCount =
|
|
||||||
this@PageViewHolder.itemView.context.getSpanCount() ?: 3
|
|
||||||
if (adapter == null || rebind) {
|
|
||||||
// Only add the items after it has been attached since the items rely on ItemWidth
|
|
||||||
// Which is only determined after the recyclerview is attached.
|
|
||||||
// If this fails then item height becomes 0 when there is only one item
|
|
||||||
doOnAttach {
|
|
||||||
adapter = PageAdapter(
|
|
||||||
page.items.toMutableList(),
|
|
||||||
this,
|
|
||||||
clickCallback
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(adapter as? PageAdapter)?.updateList(page.items)
|
|
||||||
scrollToPosition(0)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
(adapter as? PageAdapter)?.updateList(item.items)
|
||||||
|
// scrollToPosition(0)
|
||||||
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
||||||
val diff = scrollY - oldScrollY
|
val diff = scrollY - oldScrollY
|
||||||
|
|
||||||
//Expand the top Appbar based on scroll direction up/down, simulate phone behavior
|
//Expand the top Appbar based on scroll direction up/down, simulate phone behavior
|
||||||
if (SettingsFragment.isTvSettings()) {
|
if (isLayout(TV or EMULATOR)) {
|
||||||
binding.root.rootView.findViewById<AppBarLayout>(R.id.search_bar)
|
binding.root.rootView.findViewById<AppBarLayout>(R.id.search_bar)
|
||||||
.apply {
|
.apply {
|
||||||
if (diff <= 0)
|
if (diff <= 0)
|
||||||
setExpanded(true)
|
setExpanded(true)
|
||||||
else
|
else
|
||||||
setExpanded(false)
|
setExpanded(false)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (diff == 0) return@setOnScrollChangeListener
|
|
||||||
|
|
||||||
scrollCallback.invoke(diff > 0)
|
|
||||||
}
|
}
|
||||||
} else {
|
if (diff == 0) return@setOnScrollChangeListener
|
||||||
onFlingListener = object : OnFlingListener() {
|
|
||||||
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
|
scrollCallback.invoke(diff > 0)
|
||||||
scrollCallback.invoke(velocityY > 0)
|
}
|
||||||
return false
|
} else {
|
||||||
}
|
onFlingListener = object : OnFlingListener() {
|
||||||
|
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
|
||||||
|
scrollCallback.invoke(velocityY > 0)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return pages.size
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package com.lagradost.cloudstream3.ui.player
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.*
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.graphics.drawable.AnimatedImageDrawable
|
import android.graphics.drawable.AnimatedImageDrawable
|
||||||
import android.graphics.drawable.AnimatedVectorDrawable
|
import android.graphics.drawable.AnimatedVectorDrawable
|
||||||
import android.media.metrics.PlaybackErrorEvent
|
import android.media.metrics.PlaybackErrorEvent
|
||||||
|
|
@ -45,8 +48,8 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.unixTimeMs
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.unixTimeMs
|
||||||
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
||||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
|
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils
|
import com.lagradost.cloudstream3.utils.AppContextUtils
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus
|
import com.lagradost.cloudstream3.utils.AppContextUtils.requestLocalAudioFocus
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper
|
import com.lagradost.cloudstream3.utils.UIHelper
|
||||||
|
|
@ -217,7 +220,7 @@ abstract class AbstractPlayerFragment(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
player.handleEvent(
|
player.handleEvent(
|
||||||
CSPlayerEvent.values()[intent.getIntExtra(
|
CSPlayerEvent.entries[intent.getIntExtra(
|
||||||
EXTRA_CONTROL_TYPE,
|
EXTRA_CONTROL_TYPE,
|
||||||
0
|
0
|
||||||
)], source = PlayerEventSource.UI
|
)], source = PlayerEventSource.UI
|
||||||
|
|
@ -259,7 +262,7 @@ abstract class AbstractPlayerFragment(
|
||||||
|
|
||||||
private fun requestAudioFocus() {
|
private fun requestAudioFocus() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
activity?.requestLocalAudioFocus(AppUtils.getFocusRequest())
|
activity?.requestLocalAudioFocus(AppContextUtils.getFocusRequest())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -442,6 +445,9 @@ abstract class AbstractPlayerFragment(
|
||||||
|
|
||||||
is VideoEndedEvent -> {
|
is VideoEndedEvent -> {
|
||||||
context?.let { ctx ->
|
context?.let { ctx ->
|
||||||
|
// Resets subtitle delay on ended video
|
||||||
|
player.setSubtitleOffset(0)
|
||||||
|
|
||||||
// Only play next episode if autoplay is on (default)
|
// Only play next episode if autoplay is on (default)
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(ctx)
|
if (PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||||
?.getBoolean(
|
?.getBoolean(
|
||||||
|
|
@ -601,12 +607,12 @@ abstract class AbstractPlayerFragment(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun nextResize() {
|
fun nextResize() {
|
||||||
resizeMode = (resizeMode + 1) % PlayerResize.values().size
|
resizeMode = (resizeMode + 1) % PlayerResize.entries.size
|
||||||
resize(resizeMode, true)
|
resize(resizeMode, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resize(resize: Int, showToast: Boolean) {
|
fun resize(resize: Int, showToast: Boolean) {
|
||||||
resize(PlayerResize.values()[resize], showToast)
|
resize(PlayerResize.entries[resize], showToast)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@ import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Rational
|
import android.util.Rational
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.media3.common.C.*
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.C.TIME_UNSET
|
||||||
|
import androidx.media3.common.C.TRACK_TYPE_AUDIO
|
||||||
|
import androidx.media3.common.C.TRACK_TYPE_TEXT
|
||||||
|
import androidx.media3.common.C.TRACK_TYPE_VIDEO
|
||||||
import androidx.media3.common.Format
|
import androidx.media3.common.Format
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
|
|
@ -19,9 +23,10 @@ import androidx.media3.common.TrackGroup
|
||||||
import androidx.media3.common.TrackSelectionOverride
|
import androidx.media3.common.TrackSelectionOverride
|
||||||
import androidx.media3.common.Tracks
|
import androidx.media3.common.Tracks
|
||||||
import androidx.media3.common.VideoSize
|
import androidx.media3.common.VideoSize
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.database.StandaloneDatabaseProvider
|
import androidx.media3.database.StandaloneDatabaseProvider
|
||||||
import androidx.media3.datasource.DataSource
|
import androidx.media3.datasource.DataSource
|
||||||
import androidx.media3.datasource.DefaultDataSourceFactory
|
import androidx.media3.datasource.DefaultDataSource
|
||||||
import androidx.media3.datasource.DefaultHttpDataSource
|
import androidx.media3.datasource.DefaultHttpDataSource
|
||||||
import androidx.media3.datasource.HttpDataSource
|
import androidx.media3.datasource.HttpDataSource
|
||||||
import androidx.media3.datasource.cache.CacheDataSource
|
import androidx.media3.datasource.cache.CacheDataSource
|
||||||
|
|
@ -57,17 +62,15 @@ import com.lagradost.cloudstream3.mvvm.debugAssert
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
|
import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
|
||||||
import com.lagradost.cloudstream3.utils.DrmExtractorLink
|
import com.lagradost.cloudstream3.utils.DrmExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
|
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
|
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.IllegalArgumentException
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.net.ssl.HttpsURLConnection
|
import javax.net.ssl.HttpsURLConnection
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
|
|
@ -85,7 +88,7 @@ const val toleranceBeforeUs = 300_000L
|
||||||
* seek position, in microseconds. Must be non-negative.
|
* seek position, in microseconds. Must be non-negative.
|
||||||
*/
|
*/
|
||||||
const val toleranceAfterUs = 300_000L
|
const val toleranceAfterUs = 300_000L
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
class CS3IPlayer : IPlayer {
|
class CS3IPlayer : IPlayer {
|
||||||
private var isPlaying = false
|
private var isPlaying = false
|
||||||
private var exoPlayer: ExoPlayer? = null
|
private var exoPlayer: ExoPlayer? = null
|
||||||
|
|
@ -258,7 +261,6 @@ class CS3IPlayer : IPlayer {
|
||||||
|
|
||||||
private var currentSubtitles: SubtitleData? = null
|
private var currentSubtitles: SubtitleData? = null
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun List<Tracks.Group>.getTrack(id: String?): Pair<TrackGroup, Int>? {
|
private fun List<Tracks.Group>.getTrack(id: String?): Pair<TrackGroup, Int>? {
|
||||||
if (id == null) return null
|
if (id == null) return null
|
||||||
// This beast of an expression does:
|
// This beast of an expression does:
|
||||||
|
|
@ -343,7 +345,6 @@ class CS3IPlayer : IPlayer {
|
||||||
}.flatten()
|
}.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun Tracks.Group.getFormats(): List<Pair<Format, Int>> {
|
private fun Tracks.Group.getFormats(): List<Pair<Format, Int>> {
|
||||||
return (0 until this.mediaTrackGroup.length).mapNotNull { i ->
|
return (0 until this.mediaTrackGroup.length).mapNotNull { i ->
|
||||||
if (this.isSupported)
|
if (this.isSupported)
|
||||||
|
|
@ -372,7 +373,6 @@ class CS3IPlayer : IPlayer {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
override fun getVideoTracks(): CurrentTracks {
|
override fun getVideoTracks(): CurrentTracks {
|
||||||
val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList()
|
val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList()
|
||||||
val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO }
|
val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO }
|
||||||
|
|
@ -392,7 +392,6 @@ class CS3IPlayer : IPlayer {
|
||||||
/**
|
/**
|
||||||
* @return True if the player should be reloaded
|
* @return True if the player should be reloaded
|
||||||
* */
|
* */
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean {
|
override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean {
|
||||||
Log.i(TAG, "setPreferredSubtitles init $subtitle")
|
Log.i(TAG, "setPreferredSubtitles init $subtitle")
|
||||||
currentSubtitles = subtitle
|
currentSubtitles = subtitle
|
||||||
|
|
@ -452,7 +451,7 @@ class CS3IPlayer : IPlayer {
|
||||||
} ?: false
|
} ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentSubtitleOffset: Long = 0
|
private var currentSubtitleOffset: Long = 0
|
||||||
|
|
||||||
override fun setSubtitleOffset(offset: Long) {
|
override fun setSubtitleOffset(offset: Long) {
|
||||||
currentSubtitleOffset = offset
|
currentSubtitleOffset = offset
|
||||||
|
|
@ -460,7 +459,7 @@ class CS3IPlayer : IPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSubtitleOffset(): Long {
|
override fun getSubtitleOffset(): Long {
|
||||||
return currentSubtitleOffset //currentTextRenderer?.getRenderOffsetMs() ?: currentSubtitleOffset
|
return currentSubtitleOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentPreferredSubtitle(): SubtitleData? {
|
override fun getCurrentPreferredSubtitle(): SubtitleData? {
|
||||||
|
|
@ -471,7 +470,6 @@ class CS3IPlayer : IPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
override fun getAspectRatio(): Rational? {
|
override fun getAspectRatio(): Rational? {
|
||||||
return exoPlayer?.videoFormat?.let { format ->
|
return exoPlayer?.videoFormat?.let { format ->
|
||||||
Rational(format.width, format.height)
|
Rational(format.width, format.height)
|
||||||
|
|
@ -482,14 +480,13 @@ class CS3IPlayer : IPlayer {
|
||||||
subtitleHelper.setSubStyle(style)
|
subtitleHelper.setSubStyle(style)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
override fun saveData() {
|
override fun saveData() {
|
||||||
Log.i(TAG, "saveData")
|
Log.i(TAG, "saveData")
|
||||||
updatedTime()
|
updatedTime()
|
||||||
|
|
||||||
exoPlayer?.let { exo ->
|
exoPlayer?.let { exo ->
|
||||||
playbackPosition = exo.currentPosition
|
playbackPosition = exo.currentPosition
|
||||||
currentWindow = exo.currentWindowIndex
|
currentWindow = exo.currentMediaItemIndex
|
||||||
isPlaying = exo.isPlaying
|
isPlaying = exo.isPlaying
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -501,7 +498,7 @@ class CS3IPlayer : IPlayer {
|
||||||
updatedTime()
|
updatedTime()
|
||||||
|
|
||||||
exoPlayer?.apply {
|
exoPlayer?.apply {
|
||||||
setPlayWhenReady(false)
|
playWhenReady = false
|
||||||
stop()
|
stop()
|
||||||
release()
|
release()
|
||||||
}
|
}
|
||||||
|
|
@ -564,7 +561,6 @@ class CS3IPlayer : IPlayer {
|
||||||
|
|
||||||
var requestSubtitleUpdate: (() -> Unit)? = null
|
var requestSubtitleUpdate: (() -> Unit)? = null
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun createOnlineSource(headers: Map<String, String>): HttpDataSource.Factory {
|
private fun createOnlineSource(headers: Map<String, String>): HttpDataSource.Factory {
|
||||||
val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT)
|
val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT)
|
||||||
return source.apply {
|
return source.apply {
|
||||||
|
|
@ -572,7 +568,6 @@ class CS3IPlayer : IPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory {
|
private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory {
|
||||||
val provider = getApiFromNameNull(link.source)
|
val provider = getApiFromNameNull(link.source)
|
||||||
val interceptor = provider?.getVideoInterceptor(link)
|
val interceptor = provider?.getVideoInterceptor(link)
|
||||||
|
|
@ -605,53 +600,10 @@ class CS3IPlayer : IPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun Context.createOfflineSource(): DataSource.Factory {
|
private fun Context.createOfflineSource(): DataSource.Factory {
|
||||||
return DefaultDataSourceFactory(this, USER_AGENT)
|
return DefaultDataSource.Factory(this, DefaultHttpDataSource.Factory().setUserAgent(USER_AGENT))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*private fun getSubSources(
|
|
||||||
onlineSourceFactory: DataSource.Factory?,
|
|
||||||
offlineSourceFactory: DataSource.Factory?,
|
|
||||||
subHelper: PlayerSubtitleHelper,
|
|
||||||
): Pair<List<SingleSampleMediaSource>, List<SubtitleData>> {
|
|
||||||
val activeSubtitles = ArrayList<SubtitleData>()
|
|
||||||
val subSources = subHelper.getAllSubtitles().mapNotNull { sub ->
|
|
||||||
val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url))
|
|
||||||
.setMimeType(sub.mimeType)
|
|
||||||
.setLanguage("_${sub.name}")
|
|
||||||
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
|
|
||||||
.build()
|
|
||||||
when (sub.origin) {
|
|
||||||
SubtitleOrigin.DOWNLOADED_FILE -> {
|
|
||||||
if (offlineSourceFactory != null) {
|
|
||||||
activeSubtitles.add(sub)
|
|
||||||
SingleSampleMediaSource.Factory(offlineSourceFactory)
|
|
||||||
.createMediaSource(subConfig, C.TIME_UNSET)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SubtitleOrigin.URL -> {
|
|
||||||
if (onlineSourceFactory != null) {
|
|
||||||
activeSubtitles.add(sub)
|
|
||||||
SingleSampleMediaSource.Factory(onlineSourceFactory)
|
|
||||||
.createMediaSource(subConfig, C.TIME_UNSET)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SubtitleOrigin.OPEN_SUBTITLES -> {
|
|
||||||
// TODO
|
|
||||||
throw NotImplementedError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println("SUBSRC: ${subSources.size} activeSubtitles : ${activeSubtitles.size} of ${subHelper.getAllSubtitles().size} ")
|
|
||||||
return Pair(subSources, activeSubtitles)
|
|
||||||
}*/
|
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun getCache(context: Context, cacheSize: Long): SimpleCache? {
|
private fun getCache(context: Context, cacheSize: Long): SimpleCache? {
|
||||||
return try {
|
return try {
|
||||||
val databaseProvider = StandaloneDatabaseProvider(context)
|
val databaseProvider = StandaloneDatabaseProvider(context)
|
||||||
|
|
@ -683,7 +635,6 @@ class CS3IPlayer : IPlayer {
|
||||||
return getMediaItemBuilder(mimeType).setUri(url).build()
|
return getMediaItemBuilder(mimeType).setUri(url).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector {
|
private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector {
|
||||||
val trackSelector = DefaultTrackSelector(context)
|
val trackSelector = DefaultTrackSelector(context)
|
||||||
trackSelector.parameters = trackSelector.buildUponParameters()
|
trackSelector.parameters = trackSelector.buildUponParameters()
|
||||||
|
|
@ -697,7 +648,6 @@ class CS3IPlayer : IPlayer {
|
||||||
|
|
||||||
var currentTextRenderer: CustomTextRenderer? = null
|
var currentTextRenderer: CustomTextRenderer? = null
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun buildExoPlayer(
|
private fun buildExoPlayer(
|
||||||
context: Context,
|
context: Context,
|
||||||
mediaItemSlices: List<MediaItemSlice>,
|
mediaItemSlices: List<MediaItemSlice>,
|
||||||
|
|
@ -737,7 +687,7 @@ class CS3IPlayer : IPlayer {
|
||||||
textRendererOutput,
|
textRendererOutput,
|
||||||
eventHandler.looper,
|
eventHandler.looper,
|
||||||
CustomSubtitleDecoderFactory()
|
CustomSubtitleDecoderFactory()
|
||||||
).also { this.currentTextRenderer = it }
|
).also { renderer -> this.currentTextRenderer = renderer }
|
||||||
currentTextRenderer
|
currentTextRenderer
|
||||||
} else it
|
} else it
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
|
|
@ -913,7 +863,11 @@ class CS3IPlayer : IPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source)
|
CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source)
|
||||||
|
|
||||||
CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source)
|
CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source)
|
||||||
|
|
||||||
|
CSPlayerEvent.Restart -> seekTo(0, source)
|
||||||
|
|
||||||
CSPlayerEvent.NextEpisode -> event(
|
CSPlayerEvent.NextEpisode -> event(
|
||||||
EpisodeSeekEvent(
|
EpisodeSeekEvent(
|
||||||
offset = 1,
|
offset = 1,
|
||||||
|
|
@ -1030,7 +984,7 @@ class CS3IPlayer : IPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
//fixme: Use onPlaybackStateChanged(int) and onPlayWhenReadyChanged(boolean, int) instead.
|
||||||
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
||||||
exoPlayer?.let { exo ->
|
exoPlayer?.let { exo ->
|
||||||
event(
|
event(
|
||||||
|
|
@ -1118,6 +1072,9 @@ class CS3IPlayer : IPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
Player.STATE_ENDED -> {
|
Player.STATE_ENDED -> {
|
||||||
|
// Resets subtitle delay on ended video
|
||||||
|
setSubtitleOffset(0)
|
||||||
|
|
||||||
// Only play next episode if autoplay is on (default)
|
// Only play next episode if autoplay is on (default)
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(context)
|
if (PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
?.getBoolean(
|
?.getBoolean(
|
||||||
|
|
@ -1163,7 +1120,6 @@ class CS3IPlayer : IPlayer {
|
||||||
|
|
||||||
private var lastTimeStamps: List<EpisodeSkip.SkipStamp> = emptyList()
|
private var lastTimeStamps: List<EpisodeSkip.SkipStamp> = emptyList()
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
override fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>) {
|
override fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>) {
|
||||||
lastTimeStamps = timeStamps
|
lastTimeStamps = timeStamps
|
||||||
timeStamps.forEach { timestamp ->
|
timeStamps.forEach { timestamp ->
|
||||||
|
|
@ -1181,7 +1137,6 @@ class CS3IPlayer : IPlayer {
|
||||||
updatedTime(source = PlayerEventSource.Player)
|
updatedTime(source = PlayerEventSource.Player)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
fun onRenderFirst() {
|
fun onRenderFirst() {
|
||||||
if (hasUsedFirstRender) { // this insures that we only call this once per player load
|
if (hasUsedFirstRender) { // this insures that we only call this once per player load
|
||||||
return
|
return
|
||||||
|
|
@ -1248,7 +1203,6 @@ class CS3IPlayer : IPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun getSubSources(
|
private fun getSubSources(
|
||||||
onlineSourceFactory: HttpDataSource.Factory?,
|
onlineSourceFactory: HttpDataSource.Factory?,
|
||||||
offlineSourceFactory: DataSource.Factory?,
|
offlineSourceFactory: DataSource.Factory?,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.media3.common.Format
|
import androidx.media3.common.Format
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
|
|
@ -31,7 +32,7 @@ import java.nio.charset.Charset
|
||||||
* @param fallbackFormat used to create a decoder based on mimetype if the subtitle string is not
|
* @param fallbackFormat used to create a decoder based on mimetype if the subtitle string is not
|
||||||
* enough to identify the subtitle format.
|
* enough to identify the subtitle format.
|
||||||
**/
|
**/
|
||||||
@UnstableApi
|
@OptIn(UnstableApi::class)
|
||||||
class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
|
class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
|
||||||
companion object {
|
companion object {
|
||||||
fun updateForcedEncoding(context: Context) {
|
fun updateForcedEncoding(context: Context) {
|
||||||
|
|
@ -72,7 +73,7 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
|
||||||
RegexOption.IGNORE_CASE
|
RegexOption.IGNORE_CASE
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\d\s]*?[])}]\s*"""))
|
val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\s]*?[])}]\s*"""))
|
||||||
|
|
||||||
//https://emptycharacter.com/
|
//https://emptycharacter.com/
|
||||||
//https://www.fileformat.info/info/unicode/char/200b/index.htm
|
//https://www.fileformat.info/info/unicode/char/200b/index.htm
|
||||||
|
|
@ -262,7 +263,7 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** See https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java */
|
/** See https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java */
|
||||||
@UnstableApi
|
@OptIn(UnstableApi::class)
|
||||||
class CustomSubtitleDecoderFactory : SubtitleDecoderFactory {
|
class CustomSubtitleDecoderFactory : SubtitleDecoderFactory {
|
||||||
override fun supportsFormat(format: Format): Boolean {
|
override fun supportsFormat(format: Format): Boolean {
|
||||||
// return SubtitleDecoderFactory.DEFAULT.supportsFormat(format)
|
// return SubtitleDecoderFactory.DEFAULT.supportsFormat(format)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
package com.lagradost.cloudstream3.ui.player
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.text.SubtitleDecoderFactory
|
import androidx.media3.exoplayer.text.SubtitleDecoderFactory
|
||||||
import androidx.media3.exoplayer.text.TextOutput
|
import androidx.media3.exoplayer.text.TextOutput
|
||||||
|
|
||||||
@UnstableApi
|
@OptIn(UnstableApi::class)
|
||||||
class CustomTextRenderer(
|
class CustomTextRenderer(
|
||||||
offset: Long,
|
offset: Long,
|
||||||
output: TextOutput?,
|
output: TextOutput?,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
package com.lagradost.cloudstream3.ui.player
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
||||||
|
import com.lagradost.cloudstream3.CommonActivity.activity
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
|
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
import com.lagradost.cloudstream3.utils.SubtitleUtils.cleanDisplayName
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.SubtitleUtils.isMatchingSubtitle
|
||||||
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadFileInfoAndUpdateSettings
|
||||||
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFolder
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
@ -14,6 +18,7 @@ class DownloadFileGenerator(
|
||||||
private var currentIndex: Int = 0
|
private var currentIndex: Int = 0
|
||||||
) : IGenerator {
|
) : IGenerator {
|
||||||
override val hasCache = false
|
override val hasCache = false
|
||||||
|
override val canSkipLoading = false
|
||||||
|
|
||||||
override fun hasNext(): Boolean {
|
override fun hasNext(): Boolean {
|
||||||
return currentIndex < episodes.size - 1
|
return currentIndex < episodes.size - 1
|
||||||
|
|
@ -50,10 +55,6 @@ class DownloadFileGenerator(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cleanDisplayName(name: String): String {
|
|
||||||
return name.substringBeforeLast('.').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun generateLinks(
|
override suspend fun generateLinks(
|
||||||
clearCache: Boolean,
|
clearCache: Boolean,
|
||||||
type: LoadType,
|
type: LoadType,
|
||||||
|
|
@ -62,7 +63,21 @@ class DownloadFileGenerator(
|
||||||
offset: Int
|
offset: Int
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val meta = episodes[currentIndex + offset]
|
val meta = episodes[currentIndex + offset]
|
||||||
callback(null to meta)
|
|
||||||
|
if (meta.uri == Uri.EMPTY) {
|
||||||
|
// We do this here so that we only load it when
|
||||||
|
// we actually need it as it can be more expensive.
|
||||||
|
val info = meta.id?.let { id ->
|
||||||
|
activity?.let { act ->
|
||||||
|
getDownloadFileInfoAndUpdateSettings(act, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info != null) {
|
||||||
|
val newMeta = meta.copy(uri = info.path)
|
||||||
|
callback(null to newMeta)
|
||||||
|
} else callback(null to meta)
|
||||||
|
} else callback(null to meta)
|
||||||
|
|
||||||
val ctx = context ?: return true
|
val ctx = context ?: return true
|
||||||
val relative = meta.relativePath ?: return true
|
val relative = meta.relativePath ?: return true
|
||||||
|
|
@ -70,28 +85,9 @@ class DownloadFileGenerator(
|
||||||
|
|
||||||
val cleanDisplay = cleanDisplayName(display)
|
val cleanDisplay = cleanDisplayName(display)
|
||||||
|
|
||||||
VideoDownloadManager.getFolder(ctx, relative, meta.basePath)
|
getFolder(ctx, relative, meta.basePath)?.forEach { (name, uri) ->
|
||||||
?.forEach { (name, uri) ->
|
if (isMatchingSubtitle(name, display, cleanDisplay)) {
|
||||||
// only these files are allowed, so no videos as subtitles
|
|
||||||
if (listOf(
|
|
||||||
".vtt",
|
|
||||||
".srt",
|
|
||||||
".txt",
|
|
||||||
".ass",
|
|
||||||
".ttml",
|
|
||||||
".sbv",
|
|
||||||
".dfxp"
|
|
||||||
).none { name.contains(it, true) }
|
|
||||||
) return@forEach
|
|
||||||
|
|
||||||
// cant have the exact same file as a subtitle
|
|
||||||
if (name.equals(display, true)) return@forEach
|
|
||||||
|
|
||||||
val cleanName = cleanDisplayName(name)
|
val cleanName = cleanDisplayName(name)
|
||||||
|
|
||||||
// we only want files with the approx same name
|
|
||||||
if (!cleanName.startsWith(cleanDisplay, true)) return@forEach
|
|
||||||
|
|
||||||
val realName = cleanName.removePrefix(cleanDisplay)
|
val realName = cleanName.removePrefix(cleanDisplay)
|
||||||
|
|
||||||
subtitleCallback(
|
subtitleCallback(
|
||||||
|
|
@ -105,6 +101,7 @@ class DownloadFileGenerator(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,21 @@
|
||||||
package com.lagradost.cloudstream3.ui.player
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
import android.content.ContentUris
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.activity.OnBackPressedCallback
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.lagradost.cloudstream3.CommonActivity
|
import com.lagradost.cloudstream3.CommonActivity
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink
|
||||||
import com.lagradost.safefile.SafeFile
|
import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri
|
||||||
|
import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback
|
||||||
const val DTAG = "PlayerActivity"
|
|
||||||
|
|
||||||
class DownloadedPlayerActivity : AppCompatActivity() {
|
class DownloadedPlayerActivity : AppCompatActivity() {
|
||||||
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
|
private val dTAG = "DownloadedPlayerAct"
|
||||||
|
|
||||||
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
CommonActivity.dispatchKeyEvent(this, event)?.let {
|
CommonActivity.dispatchKeyEvent(this, event)?.let {
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
|
@ -35,53 +33,18 @@ class DownloadedPlayerActivity : AppCompatActivity() {
|
||||||
CommonActivity.onUserLeaveHint(this)
|
CommonActivity.onUserLeaveHint(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playLink(url: String) {
|
|
||||||
this.navigate(
|
|
||||||
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
|
|
||||||
LinkGenerator(
|
|
||||||
listOf(
|
|
||||||
BasicLink(url)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun playUri(uri: Uri) {
|
|
||||||
val name = SafeFile.fromUri(this, uri)?.name()
|
|
||||||
this.navigate(
|
|
||||||
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
|
|
||||||
DownloadFileGenerator(
|
|
||||||
listOf(
|
|
||||||
ExtractorUri(
|
|
||||||
uri = uri,
|
|
||||||
name = name ?: getString(R.string.downloaded_file),
|
|
||||||
// well not the same as a normal id, but we take it as users may want to
|
|
||||||
// play downloaded files and save the location
|
|
||||||
id = kotlin.runCatching { ContentUris.parseId(uri) }.getOrNull()?.hashCode()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Log.i(DTAG, "onCreate")
|
|
||||||
|
|
||||||
CommonActivity.loadThemes(this)
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
CommonActivity.loadThemes(this)
|
||||||
CommonActivity.init(this)
|
CommonActivity.init(this)
|
||||||
|
|
||||||
setContentView(R.layout.empty_layout)
|
setContentView(R.layout.empty_layout)
|
||||||
|
Log.i(dTAG, "onCreate")
|
||||||
|
|
||||||
val data = intent.data
|
val data = intent.data
|
||||||
|
|
||||||
if (intent?.action == Intent.ACTION_SEND) {
|
if (intent?.action == Intent.ACTION_SEND) {
|
||||||
val extraText = try { // I dont trust android
|
val extraText = normalSafeApiCall { // I dont trust android
|
||||||
intent.getStringExtra(Intent.EXTRA_TEXT)
|
intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
val cd = intent.clipData
|
val cd = intent.clipData
|
||||||
val item = if (cd != null && cd.itemCount > 0) cd.getItemAt(0) else null
|
val item = if (cd != null && cd.itemCount > 0) cd.getItemAt(0) else null
|
||||||
|
|
@ -89,32 +52,25 @@ class DownloadedPlayerActivity : AppCompatActivity() {
|
||||||
|
|
||||||
// idk what I am doing, just hope any of these work
|
// idk what I am doing, just hope any of these work
|
||||||
if (item?.uri != null)
|
if (item?.uri != null)
|
||||||
playUri(item.uri)
|
playUri(this, item.uri)
|
||||||
else if (url != null)
|
else if (url != null)
|
||||||
playLink(url)
|
playLink(this, url)
|
||||||
else if (data != null)
|
else if (data != null)
|
||||||
playUri(data)
|
playUri(this, data)
|
||||||
else if (extraText != null)
|
else if (extraText != null)
|
||||||
playLink(extraText)
|
playLink(this, extraText)
|
||||||
else {
|
else {
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if (data?.scheme == "content") {
|
} else if (data?.scheme == "content") {
|
||||||
playUri(data)
|
playUri(this, data)
|
||||||
} else {
|
} else {
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(
|
attachBackPressedCallback { finish() }
|
||||||
this,
|
|
||||||
object : OnBackPressedCallback(true) {
|
|
||||||
override fun handleOnBackPressed() {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
package com.lagradost.cloudstream3.ui.player
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
|
||||||
|
|
||||||
class ExtractorLinkGenerator(
|
class ExtractorLinkGenerator(
|
||||||
private val links: List<ExtractorLink>,
|
private val links: List<ExtractorLink>,
|
||||||
private val subtitles: List<SubtitleData>,
|
private val subtitles: List<SubtitleData>,
|
||||||
) : IGenerator {
|
) : IGenerator {
|
||||||
override val hasCache = false
|
override val hasCache = false
|
||||||
|
override val canSkipLoading = true
|
||||||
|
|
||||||
override fun getCurrentId(): Int? {
|
override fun getCurrentId(): Int? {
|
||||||
return null
|
return null
|
||||||
|
|
|
||||||
|
|
@ -25,19 +25,25 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHO
|
||||||
import android.view.animation.AlphaAnimation
|
import android.view.animation.AlphaAnimation
|
||||||
import android.view.animation.Animation
|
import android.view.animation.Animation
|
||||||
import android.view.animation.AnimationUtils
|
import android.view.animation.AnimationUtils
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import android.widget.LinearLayout
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.graphics.blue
|
import androidx.core.graphics.blue
|
||||||
import androidx.core.graphics.green
|
import androidx.core.graphics.green
|
||||||
import androidx.core.graphics.red
|
import androidx.core.graphics.red
|
||||||
|
import androidx.core.view.children
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.doOnTextChanged
|
import androidx.core.widget.doOnTextChanged
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
import com.lagradost.cloudstream3.CommonActivity.keyEventListener
|
import com.lagradost.cloudstream3.CommonActivity.keyEventListener
|
||||||
import com.lagradost.cloudstream3.CommonActivity.playerEventListener
|
import com.lagradost.cloudstream3.CommonActivity.playerEventListener
|
||||||
import com.lagradost.cloudstream3.CommonActivity.screenHeight
|
import com.lagradost.cloudstream3.CommonActivity.screenHeight
|
||||||
import com.lagradost.cloudstream3.CommonActivity.screenWidth
|
import com.lagradost.cloudstream3.CommonActivity.screenWidth
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
|
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
|
||||||
import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding
|
import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding
|
||||||
|
|
@ -46,8 +52,10 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvid
|
||||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
|
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
|
||||||
import com.lagradost.cloudstream3.ui.result.setText
|
import com.lagradost.cloudstream3.ui.result.setText
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||||
|
|
@ -60,7 +68,11 @@ import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import com.lagradost.cloudstream3.utils.UserPreferenceDelegate
|
import com.lagradost.cloudstream3.utils.UserPreferenceDelegate
|
||||||
import com.lagradost.cloudstream3.utils.Vector2
|
import com.lagradost.cloudstream3.utils.Vector2
|
||||||
import kotlin.math.*
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.round
|
||||||
|
|
||||||
const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking
|
const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking
|
||||||
const val MINIMUM_VERTICAL_SWIPE = 2.0f // in percentage
|
const val MINIMUM_VERTICAL_SWIPE = 2.0f // in percentage
|
||||||
|
|
@ -77,10 +89,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
private var isVerticalOrientation: Boolean = false
|
private var isVerticalOrientation: Boolean = false
|
||||||
protected open var lockRotation = true
|
protected open var lockRotation = true
|
||||||
protected open var isFullScreenPlayer = true
|
protected open var isFullScreenPlayer = true
|
||||||
protected open var isTv = false
|
|
||||||
protected var playerBinding: PlayerCustomLayoutBinding? = null
|
protected var playerBinding: PlayerCustomLayoutBinding? = null
|
||||||
|
|
||||||
private var durationMode : Boolean by UserPreferenceDelegate("duration_mode", false)
|
private var durationMode: Boolean by UserPreferenceDelegate("duration_mode", false)
|
||||||
|
|
||||||
// state of player UI
|
// state of player UI
|
||||||
protected var isShowing = false
|
protected var isShowing = false
|
||||||
protected var isLocked = false
|
protected var isLocked = false
|
||||||
|
|
@ -112,6 +124,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
protected var doubleTapPauseEnabled = true
|
protected var doubleTapPauseEnabled = true
|
||||||
protected var playerRotateEnabled = false
|
protected var playerRotateEnabled = false
|
||||||
protected var autoPlayerRotateEnabled = false
|
protected var autoPlayerRotateEnabled = false
|
||||||
|
private var hideControlsNames = false
|
||||||
|
|
||||||
protected var subtitleDelay
|
protected var subtitleDelay
|
||||||
set(value) = try {
|
set(value) = try {
|
||||||
|
|
@ -181,7 +194,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
|
|
||||||
open fun openOnlineSubPicker(
|
open fun openOnlineSubPicker(
|
||||||
context: Context,
|
context: Context,
|
||||||
imdbId: Long?,
|
loadResponse: LoadResponse?,
|
||||||
dismissCallback: (() -> Unit)
|
dismissCallback: (() -> Unit)
|
||||||
) {
|
) {
|
||||||
throw NotImplementedError()
|
throw NotImplementedError()
|
||||||
|
|
@ -233,6 +246,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
fadeAnimation.duration = 100
|
fadeAnimation.duration = 100
|
||||||
fadeAnimation.fillAfter = true
|
fadeAnimation.fillAfter = true
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
val sView = subView
|
val sView = subView
|
||||||
val sStyle = subStyle
|
val sStyle = subStyle
|
||||||
if (sView != null && sStyle != null) {
|
if (sView != null && sStyle != null) {
|
||||||
|
|
@ -246,7 +260,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
|
|
||||||
val playerSourceMove = if (isShowing) 0f else -50.toPx.toFloat()
|
val playerSourceMove = if (isShowing) 0f else -50.toPx.toFloat()
|
||||||
|
|
||||||
|
|
||||||
playerBinding?.apply {
|
playerBinding?.apply {
|
||||||
playerOpenSource.let {
|
playerOpenSource.let {
|
||||||
ObjectAnimator.ofFloat(it, "translationY", playerSourceMove).apply {
|
ObjectAnimator.ofFloat(it, "translationY", playerSourceMove).apply {
|
||||||
|
|
@ -287,44 +300,42 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
player.getCurrentPreferredSubtitle() == null
|
player.getCurrentPreferredSubtitle() == null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreOrientationWithSensor(activity: Activity){
|
private fun restoreOrientationWithSensor(activity: Activity) {
|
||||||
val currentOrientation = activity.resources.configuration.orientation
|
val currentOrientation = activity.resources.configuration.orientation
|
||||||
var orientation = 0
|
val orientation = when (currentOrientation) {
|
||||||
when (currentOrientation) {
|
|
||||||
Configuration.ORIENTATION_LANDSCAPE ->
|
Configuration.ORIENTATION_LANDSCAPE ->
|
||||||
orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||||
|
|
||||||
Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED ->
|
|
||||||
orientation = dynamicOrientation()
|
|
||||||
|
|
||||||
Configuration.ORIENTATION_PORTRAIT ->
|
Configuration.ORIENTATION_PORTRAIT ->
|
||||||
orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||||
|
|
||||||
|
else -> dynamicOrientation()
|
||||||
}
|
}
|
||||||
activity.requestedOrientation = orientation
|
activity.requestedOrientation = orientation
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleOrientationWithSensor(activity: Activity){
|
private fun toggleOrientationWithSensor(activity: Activity) {
|
||||||
val currentOrientation = activity.resources.configuration.orientation
|
val currentOrientation = activity.resources.configuration.orientation
|
||||||
var orientation = 0
|
val orientation: Int = when (currentOrientation) {
|
||||||
when (currentOrientation) {
|
|
||||||
Configuration.ORIENTATION_LANDSCAPE ->
|
Configuration.ORIENTATION_LANDSCAPE ->
|
||||||
orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||||
|
|
||||||
Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED ->
|
|
||||||
orientation = dynamicOrientation()
|
|
||||||
|
|
||||||
Configuration.ORIENTATION_PORTRAIT ->
|
Configuration.ORIENTATION_PORTRAIT ->
|
||||||
orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||||
|
|
||||||
|
else -> dynamicOrientation()
|
||||||
}
|
}
|
||||||
activity.requestedOrientation = orientation
|
activity.requestedOrientation = orientation
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun lockOrientation(activity: Activity) {
|
open fun lockOrientation(activity: Activity) {
|
||||||
val display =
|
@Suppress("DEPRECATION")
|
||||||
|
val display = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
|
||||||
(activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
|
(activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
|
||||||
|
else activity.display!!
|
||||||
val rotation = display.rotation
|
val rotation = display.rotation
|
||||||
val currentOrientation = activity.resources.configuration.orientation
|
val currentOrientation = activity.resources.configuration.orientation
|
||||||
var orientation = 0
|
val orientation: Int
|
||||||
when (currentOrientation) {
|
when (currentOrientation) {
|
||||||
Configuration.ORIENTATION_LANDSCAPE ->
|
Configuration.ORIENTATION_LANDSCAPE ->
|
||||||
orientation =
|
orientation =
|
||||||
|
|
@ -333,27 +344,25 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
else
|
else
|
||||||
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||||
|
|
||||||
Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED ->
|
|
||||||
orientation = dynamicOrientation()
|
|
||||||
|
|
||||||
Configuration.ORIENTATION_PORTRAIT ->
|
Configuration.ORIENTATION_PORTRAIT ->
|
||||||
orientation =
|
orientation =
|
||||||
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270)
|
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270)
|
||||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
else
|
else
|
||||||
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
||||||
|
|
||||||
|
else -> orientation = dynamicOrientation()
|
||||||
}
|
}
|
||||||
activity.requestedOrientation = orientation
|
activity.requestedOrientation = orientation
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateOrientation(ignoreDynamicOrientation: Boolean = false) {
|
private fun updateOrientation(ignoreDynamicOrientation: Boolean = false) {
|
||||||
activity?.apply {
|
activity?.apply {
|
||||||
if(lockRotation) {
|
if (lockRotation) {
|
||||||
if(isLocked) {
|
if (isLocked) {
|
||||||
lockOrientation(this)
|
lockOrientation(this)
|
||||||
}
|
} else {
|
||||||
else {
|
if (ignoreDynamicOrientation) {
|
||||||
if(ignoreDynamicOrientation){
|
|
||||||
// restore when lock is disabled
|
// restore when lock is disabled
|
||||||
restoreOrientationWithSensor(this)
|
restoreOrientationWithSensor(this)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -495,6 +504,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
dialog.dismissSafe(activity)
|
dialog.dismissSafe(activity)
|
||||||
player.seekTime(1L)
|
player.seekTime(1L)
|
||||||
}
|
}
|
||||||
|
resetBtt.setOnClickListener {
|
||||||
|
subtitleDelay = 0
|
||||||
|
dialog.dismissSafe(activity)
|
||||||
|
player.seekTime(1L)
|
||||||
|
}
|
||||||
cancelBtt.setOnClickListener {
|
cancelBtt.setOnClickListener {
|
||||||
subtitleDelay = beforeOffset
|
subtitleDelay = beforeOffset
|
||||||
dialog.dismissSafe(activity)
|
dialog.dismissSafe(activity)
|
||||||
|
|
@ -726,6 +740,15 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
private var currentTapIndex = 0
|
private var currentTapIndex = 0
|
||||||
protected fun autoHide() {
|
protected fun autoHide() {
|
||||||
currentTapIndex++
|
currentTapIndex++
|
||||||
|
delayHide()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun playerStatusChanged() {
|
||||||
|
super.playerStatusChanged()
|
||||||
|
delayHide()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun delayHide() {
|
||||||
val index = currentTapIndex
|
val index = currentTapIndex
|
||||||
playerBinding?.playerHolder?.postDelayed({
|
playerBinding?.playerHolder?.postDelayed({
|
||||||
if (!isCurrentTouchValid && isShowing && index == currentTapIndex && player.getIsPlaying()) {
|
if (!isCurrentTouchValid && isShowing && index == currentTapIndex && player.getIsPlaying()) {
|
||||||
|
|
@ -947,7 +970,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
player.handleEvent(CSPlayerEvent.PlayPauseToggle, PlayerEventSource.UI)
|
player.handleEvent(
|
||||||
|
CSPlayerEvent.PlayPauseToggle,
|
||||||
|
PlayerEventSource.UI
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (doubleTapEnabled && isFullScreenPlayer) {
|
} else if (doubleTapEnabled && isFullScreenPlayer) {
|
||||||
|
|
@ -1140,6 +1166,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("GestureBackNavigation")
|
||||||
private fun handleKeyEvent(event: KeyEvent, hasNavigated: Boolean): Boolean {
|
private fun handleKeyEvent(event: KeyEvent, hasNavigated: Boolean): Boolean {
|
||||||
if (hasNavigated) {
|
if (hasNavigated) {
|
||||||
autoHide()
|
autoHide()
|
||||||
|
|
@ -1156,6 +1183,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||||
KeyEvent.KEYCODE_DPAD_UP -> {
|
KeyEvent.KEYCODE_DPAD_UP -> {
|
||||||
if (!isShowing) {
|
if (!isShowing) {
|
||||||
onClickChange()
|
onClickChange()
|
||||||
|
|
@ -1204,7 +1232,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
|
|
||||||
// netflix capture back and hide ~monke
|
// netflix capture back and hide ~monke
|
||||||
KeyEvent.KEYCODE_BACK -> {
|
KeyEvent.KEYCODE_BACK -> {
|
||||||
if (isShowing && isTv) {
|
if (isShowing && isLayout(TV or EMULATOR)) {
|
||||||
onClickChange()
|
onClickChange()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -1222,6 +1250,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
// if nothing has loaded these buttons should not be visible
|
// if nothing has loaded these buttons should not be visible
|
||||||
playerBinding?.apply {
|
playerBinding?.apply {
|
||||||
playerSkipEpisode.isVisible = false
|
playerSkipEpisode.isVisible = false
|
||||||
|
playerGoForward.isVisible = false
|
||||||
playerTracksBtt.isVisible = false
|
playerTracksBtt.isVisible = false
|
||||||
playerSkipOp.isVisible = false
|
playerSkipOp.isVisible = false
|
||||||
shadowOverlay.isVisible = false
|
shadowOverlay.isVisible = false
|
||||||
|
|
@ -1295,6 +1324,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
player.handleEvent(CSPlayerEvent.SeekBack)
|
player.handleEvent(CSPlayerEvent.SeekBack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PlayerEventType.Restart -> {
|
||||||
|
player.handleEvent(CSPlayerEvent.Restart)
|
||||||
|
}
|
||||||
|
|
||||||
PlayerEventType.ToggleMute -> {
|
PlayerEventType.ToggleMute -> {
|
||||||
player.handleEvent(CSPlayerEvent.ToggleMute)
|
player.handleEvent(CSPlayerEvent.ToggleMute)
|
||||||
}
|
}
|
||||||
|
|
@ -1390,6 +1423,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
hideControlsNames = settingsManager.getBoolean(ctx.getString(R.string.hide_player_control_names_key), false)
|
||||||
|
|
||||||
val profiles = QualityDataHelper.getProfiles()
|
val profiles = QualityDataHelper.getProfiles()
|
||||||
val type = if (ctx.isUsingMobileData())
|
val type = if (ctx.isUsingMobileData())
|
||||||
QualityDataHelper.QualityProfileType.Data
|
QualityDataHelper.QualityProfileType.Data
|
||||||
|
|
@ -1410,12 +1445,34 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
playerSpeedBtt.isVisible = playBackSpeedEnabled
|
playerSpeedBtt.isVisible = playBackSpeedEnabled
|
||||||
playerResizeBtt.isVisible = playerResizeEnabled
|
playerResizeBtt.isVisible = playerResizeEnabled
|
||||||
playerRotateBtt.isVisible = playerRotateEnabled
|
playerRotateBtt.isVisible = playerRotateEnabled
|
||||||
|
if (hideControlsNames) {
|
||||||
|
hideControlsNames()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
playerBinding?.apply {
|
playerBinding?.apply {
|
||||||
|
|
||||||
|
if (isLayout(TV or EMULATOR)) {
|
||||||
|
mapOf(
|
||||||
|
playerGoBack to playerGoBackText,
|
||||||
|
playerRestart to playerRestartText,
|
||||||
|
playerGoForward to playerGoForwardText
|
||||||
|
).forEach { (button, text) ->
|
||||||
|
button.setOnFocusChangeListener { _, hasFocus ->
|
||||||
|
if (!hasFocus) {
|
||||||
|
text.isSelected = false
|
||||||
|
text.isVisible = false
|
||||||
|
return@setOnFocusChangeListener
|
||||||
|
}
|
||||||
|
text.isSelected = true
|
||||||
|
text.isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
playerPausePlay.setOnClickListener {
|
playerPausePlay.setOnClickListener {
|
||||||
autoHide()
|
autoHide()
|
||||||
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
|
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
|
||||||
|
|
@ -1459,6 +1516,16 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
player.handleEvent(CSPlayerEvent.NextEpisode)
|
player.handleEvent(CSPlayerEvent.NextEpisode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playerGoForward.setOnClickListener {
|
||||||
|
autoHide()
|
||||||
|
player.handleEvent(CSPlayerEvent.NextEpisode)
|
||||||
|
}
|
||||||
|
|
||||||
|
playerRestart.setOnClickListener {
|
||||||
|
autoHide()
|
||||||
|
player.handleEvent(CSPlayerEvent.Restart)
|
||||||
|
}
|
||||||
|
|
||||||
playerLock.setOnClickListener {
|
playerLock.setOnClickListener {
|
||||||
autoHide()
|
autoHide()
|
||||||
toggleLock()
|
toggleLock()
|
||||||
|
|
@ -1514,7 +1581,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// cs3 is peak media center
|
// cs3 is peak media center
|
||||||
setRemainingTimeCounter(durationMode || SettingsFragment.isTrueTvSettings())
|
setRemainingTimeCounter(durationMode || isLayout(TV))
|
||||||
playerBinding?.exoPosition?.doOnTextChanged { _, _, _, _ ->
|
playerBinding?.exoPosition?.doOnTextChanged { _, _, _, _ ->
|
||||||
updateRemainingTime()
|
updateRemainingTime()
|
||||||
}
|
}
|
||||||
|
|
@ -1533,6 +1600,22 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun PlayerCustomLayoutBinding.hideControlsNames() {
|
||||||
|
fun iterate(layout: LinearLayout) {
|
||||||
|
layout.children.forEach {
|
||||||
|
if (it is MaterialButton) {
|
||||||
|
it.textSize = 0f
|
||||||
|
it.iconPadding = 0
|
||||||
|
it.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START
|
||||||
|
it.setPadding(0,0,0,0)
|
||||||
|
} else if (it is LinearLayout) {
|
||||||
|
iterate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iterate(playerLockHolder.parent as LinearLayout)
|
||||||
|
}
|
||||||
|
|
||||||
override fun playerDimensionsLoaded(width: Int, height: Int) {
|
override fun playerDimensionsLoaded(width: Int, height: Int) {
|
||||||
isVerticalOrientation = height > width
|
isVerticalOrientation = height > width
|
||||||
updateOrientation()
|
updateOrientation()
|
||||||
|
|
@ -1552,7 +1635,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
|
|
||||||
private fun setRemainingTimeCounter(showRemaining: Boolean) {
|
private fun setRemainingTimeCounter(showRemaining: Boolean) {
|
||||||
durationMode = showRemaining
|
durationMode = showRemaining
|
||||||
playerBinding?.exoDuration?.isInvisible= showRemaining
|
playerBinding?.exoDuration?.isInvisible = showRemaining
|
||||||
playerBinding?.timeLeft?.isVisible = showRemaining
|
playerBinding?.timeLeft?.isVisible = showRemaining
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
|
@ -13,6 +14,7 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.animation.addListener
|
import androidx.core.animation.addListener
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
|
|
@ -21,10 +23,15 @@ import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.media3.common.Format.NO_VALUE
|
import androidx.media3.common.Format.NO_VALUE
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.getTMDbId
|
||||||
import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding
|
import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
||||||
import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding
|
import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding
|
||||||
|
|
@ -39,10 +46,14 @@ import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSub
|
||||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
|
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
|
||||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog
|
import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog
|
||||||
import com.lagradost.cloudstream3.ui.result.*
|
import com.lagradost.cloudstream3.ui.result.*
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY
|
import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY
|
||||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
|
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||||
|
|
@ -55,6 +66,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import com.lagradost.safefile.SafeFile
|
import com.lagradost.safefile.SafeFile
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import java.io.Serializable
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
|
@ -151,6 +163,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun playerStatusChanged() {
|
override fun playerStatusChanged() {
|
||||||
|
super.playerStatusChanged()
|
||||||
if (player.getIsPlaying()) {
|
if (player.getIsPlaying()) {
|
||||||
viewModel.forceClearCache = false
|
viewModel.forceClearCache = false
|
||||||
}
|
}
|
||||||
|
|
@ -225,7 +238,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
|
|
||||||
private fun closestQuality(target: Int?): Qualities {
|
private fun closestQuality(target: Int?): Qualities {
|
||||||
if (target == null) return Qualities.Unknown
|
if (target == null) return Qualities.Unknown
|
||||||
return Qualities.values().minBy { abs(it.value - target) }
|
return Qualities.entries.minBy { abs(it.value - target) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getLinkPriority(
|
private fun getLinkPriority(
|
||||||
|
|
@ -255,6 +268,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
var episode: Int? = null,
|
var episode: Int? = null,
|
||||||
var season: Int? = null,
|
var season: Int? = null,
|
||||||
var name: String? = null,
|
var name: String? = null,
|
||||||
|
var imdbId: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun getMetaData(): TempMetaData {
|
private fun getMetaData(): TempMetaData {
|
||||||
|
|
@ -281,7 +295,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openOnlineSubPicker(
|
override fun openOnlineSubPicker(
|
||||||
context: Context, imdbId: Long?, dismissCallback: (() -> Unit)
|
context: Context, loadResponse: LoadResponse?, dismissCallback: (() -> Unit)
|
||||||
) {
|
) {
|
||||||
val providers = subsProviders
|
val providers = subsProviders
|
||||||
val isSingleProvider = subsProviders.size == 1
|
val isSingleProvider = subsProviders.size == 1
|
||||||
|
|
@ -357,8 +371,6 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
|
|
||||||
binding.subtitleAdapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
binding.subtitleAdapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||||
binding.subtitleAdapter.adapter = arrayAdapter
|
binding.subtitleAdapter.adapter = arrayAdapter
|
||||||
val adapter =
|
|
||||||
binding.subtitleAdapter.adapter as? ArrayAdapter<AbstractSubtitleEntities.SubtitleEntity>
|
|
||||||
|
|
||||||
binding.subtitleAdapter.setOnItemClickListener { _, _, position, _ ->
|
binding.subtitleAdapter.setOnItemClickListener { _, _, position, _ ->
|
||||||
currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener
|
currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener
|
||||||
|
|
@ -369,11 +381,12 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
|
|
||||||
fun setSubtitlesList(list: List<AbstractSubtitleEntities.SubtitleEntity>) {
|
fun setSubtitlesList(list: List<AbstractSubtitleEntities.SubtitleEntity>) {
|
||||||
currentSubtitles = list
|
currentSubtitles = list
|
||||||
adapter?.clear()
|
arrayAdapter.clear()
|
||||||
adapter?.addAll(currentSubtitles)
|
arrayAdapter.addAll(currentSubtitles)
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentTempMeta = getMetaData()
|
val currentTempMeta = getMetaData()
|
||||||
|
|
||||||
// bruh idk why it is not correct
|
// bruh idk why it is not correct
|
||||||
val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent))
|
val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent))
|
||||||
binding.searchLoadingBar.progressTintList = color
|
binding.searchLoadingBar.progressTintList = color
|
||||||
|
|
@ -421,7 +434,10 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
val search =
|
val search =
|
||||||
AbstractSubtitleEntities.SubtitleSearch(
|
AbstractSubtitleEntities.SubtitleSearch(
|
||||||
query = query ?: return@ioSafe,
|
query = query ?: return@ioSafe,
|
||||||
imdb = imdbId,
|
imdbId = loadResponse?.getImdbId(),
|
||||||
|
tmdbId = loadResponse?.getTMDbId()?.toInt(),
|
||||||
|
malId = loadResponse?.getMalId()?.toInt(),
|
||||||
|
aniListId = loadResponse?.getAniListId()?.toInt(),
|
||||||
epNumber = currentTempMeta.episode,
|
epNumber = currentTempMeta.episode,
|
||||||
seasonNumber = currentTempMeta.season,
|
seasonNumber = currentTempMeta.season,
|
||||||
lang = currentLanguageTwoLetters.ifBlank { null },
|
lang = currentLanguageTwoLetters.ifBlank { null },
|
||||||
|
|
@ -508,7 +524,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
//TODO: Set year text from currently loaded movie on Player
|
//TODO: Set year text from currently loaded movie on Player
|
||||||
//dialog.subtitles_search_year?.setText(currentTempMeta.year)
|
//dialog.subtitles_search_year?.setText(currentTempMeta.year)
|
||||||
}
|
}
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
private fun openSubPicker() {
|
private fun openSubPicker() {
|
||||||
try {
|
try {
|
||||||
subsPathPicker.launch(
|
subsPathPicker.launch(
|
||||||
|
|
@ -630,6 +646,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subsProvidersIsActive) {
|
if (subsProvidersIsActive) {
|
||||||
|
val currentLoadResponse = viewModel.getLoadResponse()
|
||||||
|
|
||||||
val loadFromOpenSubsFooter: TextView = layoutInflater.inflate(
|
val loadFromOpenSubsFooter: TextView = layoutInflater.inflate(
|
||||||
R.layout.sort_bottom_footer_add_choice, null
|
R.layout.sort_bottom_footer_add_choice, null
|
||||||
) as TextView
|
) as TextView
|
||||||
|
|
@ -640,7 +658,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
loadFromOpenSubsFooter.setOnClickListener {
|
loadFromOpenSubsFooter.setOnClickListener {
|
||||||
shouldDismiss = false
|
shouldDismiss = false
|
||||||
sourceDialog.dismissSafe(activity)
|
sourceDialog.dismissSafe(activity)
|
||||||
openOnlineSubPicker(it.context, null) {
|
openOnlineSubPicker(it.context, currentLoadResponse) {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -779,7 +797,6 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
settingsManager.edit().putString(
|
settingsManager.edit().putString(
|
||||||
ctx.getString(R.string.subtitles_encoding_key), prefValues[it]
|
ctx.getString(R.string.subtitles_encoding_key), prefValues[it]
|
||||||
).apply()
|
).apply()
|
||||||
|
|
||||||
updateForcedEncoding(ctx)
|
updateForcedEncoding(ctx)
|
||||||
dismiss()
|
dismiss()
|
||||||
player.seekTime(-1) // to update subtitles, a dirty trick
|
player.seekTime(-1) // to update subtitles, a dirty trick
|
||||||
|
|
@ -1082,8 +1099,15 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
playerBinding?.playerSkipOp?.isVisible = isOpVisible
|
playerBinding?.playerSkipOp?.isVisible = isOpVisible
|
||||||
playerBinding?.playerSkipEpisode?.isVisible =
|
|
||||||
!isOpVisible && viewModel.hasNextEpisode() == true
|
when {
|
||||||
|
isLayout(PHONE) ->
|
||||||
|
playerBinding?.playerSkipEpisode?.isVisible =
|
||||||
|
!isOpVisible && viewModel.hasNextEpisode() == true
|
||||||
|
|
||||||
|
else ->
|
||||||
|
playerBinding?.playerGoForward?.isVisible = viewModel.hasNextEpisode() == true
|
||||||
|
}
|
||||||
|
|
||||||
if (percentage >= PRELOAD_NEXT_EPISODE_PERCENTAGE) {
|
if (percentage >= PRELOAD_NEXT_EPISODE_PERCENTAGE) {
|
||||||
viewModel.preLoadNextLinks()
|
viewModel.preLoadNextLinks()
|
||||||
|
|
@ -1239,7 +1263,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
fun setPlayerDimen(widthHeight: Pair<Int, Int>?) {
|
fun setPlayerDimen(widthHeight: Pair<Int, Int>?) {
|
||||||
val extra = if (widthHeight != null) {
|
val extra = if (widthHeight != null) {
|
||||||
val (width, height) = widthHeight
|
val (width, height) = widthHeight
|
||||||
"${width}x${height}"
|
"- ${width}x${height}"
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
@ -1250,7 +1274,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
0 -> ""
|
0 -> ""
|
||||||
1 -> extra
|
1 -> extra
|
||||||
2 -> source
|
2 -> source
|
||||||
3 -> "$source - $extra"
|
3 -> "$source $extra"
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
playerBinding?.playerVideoTitleRez?.apply {
|
playerBinding?.playerVideoTitleRez?.apply {
|
||||||
|
|
@ -1267,7 +1291,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
private fun unwrapBundle(savedInstanceState: Bundle?) {
|
private fun unwrapBundle(savedInstanceState: Bundle?) {
|
||||||
Log.i(TAG, "unwrapBundle = $savedInstanceState")
|
Log.i(TAG, "unwrapBundle = $savedInstanceState")
|
||||||
savedInstanceState?.let { bundle ->
|
savedInstanceState?.let { bundle ->
|
||||||
sync.addSyncs(bundle.getSerializable("syncData") as? HashMap<String, String>?)
|
sync.addSyncs(bundle.getSafeSerializable<HashMap<String, String>>("syncData"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1275,8 +1299,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
// this is used instead of layout-television to follow the settings and some TV devices are not classified as TV for some reason
|
// this is used instead of layout-television to follow the settings and some TV devices are not classified as TV for some reason
|
||||||
isTv = isTvSettings()
|
layout =
|
||||||
layout = if (isTv) R.layout.fragment_player_tv else R.layout.fragment_player
|
if (isLayout(TV or EMULATOR)) R.layout.fragment_player_tv else R.layout.fragment_player
|
||||||
|
|
||||||
viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java]
|
viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java]
|
||||||
sync = ViewModelProvider(this)[SyncViewModel::class.java]
|
sync = ViewModelProvider(this)[SyncViewModel::class.java]
|
||||||
|
|
@ -1438,7 +1462,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
|
|
||||||
observe(viewModel.currentLinks) {
|
observe(viewModel.currentLinks) {
|
||||||
currentLinks = it
|
currentLinks = it
|
||||||
val turnVisible = it.isNotEmpty()
|
val turnVisible = it.isNotEmpty() && lastUsedGenerator?.canSkipLoading == true
|
||||||
val wasGone = binding?.overlayLoadingSkipButton?.isGone == true
|
val wasGone = binding?.overlayLoadingSkipButton?.isGone == true
|
||||||
binding?.overlayLoadingSkipButton?.isVisible = turnVisible
|
binding?.overlayLoadingSkipButton?.isVisible = turnVisible
|
||||||
|
|
||||||
|
|
@ -1484,3 +1508,6 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
inline fun <reified T : Serializable> Bundle.getSafeSerializable(key: String) : T? = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getSerializable(key) as? T else getSerializable(key, T::class.java)
|
||||||
|
|
@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
|
||||||
|
|
||||||
enum class LoadType {
|
enum class LoadType {
|
||||||
Unknown,
|
Unknown,
|
||||||
|
|
@ -10,7 +9,8 @@ enum class LoadType {
|
||||||
InAppDownload,
|
InAppDownload,
|
||||||
ExternalApp,
|
ExternalApp,
|
||||||
Browser,
|
Browser,
|
||||||
Chromecast
|
Chromecast,
|
||||||
|
Fcast
|
||||||
}
|
}
|
||||||
|
|
||||||
fun LoadType.toSet() : Set<ExtractorLinkType> {
|
fun LoadType.toSet() : Set<ExtractorLinkType> {
|
||||||
|
|
@ -29,17 +29,23 @@ fun LoadType.toSet() : Set<ExtractorLinkType> {
|
||||||
ExtractorLinkType.VIDEO,
|
ExtractorLinkType.VIDEO,
|
||||||
ExtractorLinkType.M3U8
|
ExtractorLinkType.M3U8
|
||||||
)
|
)
|
||||||
LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet()
|
LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.entries.toSet()
|
||||||
LoadType.Chromecast -> setOf(
|
LoadType.Chromecast -> setOf(
|
||||||
ExtractorLinkType.VIDEO,
|
ExtractorLinkType.VIDEO,
|
||||||
ExtractorLinkType.DASH,
|
ExtractorLinkType.DASH,
|
||||||
ExtractorLinkType.M3U8
|
ExtractorLinkType.M3U8
|
||||||
)
|
)
|
||||||
|
LoadType.Fcast -> setOf(
|
||||||
|
ExtractorLinkType.VIDEO,
|
||||||
|
ExtractorLinkType.DASH,
|
||||||
|
ExtractorLinkType.M3U8
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGenerator {
|
interface IGenerator {
|
||||||
val hasCache: Boolean
|
val hasCache: Boolean
|
||||||
|
val canSkipLoading: Boolean
|
||||||
|
|
||||||
fun hasNext(): Boolean
|
fun hasNext(): Boolean
|
||||||
fun hasPrev(): Boolean
|
fun hasPrev(): Boolean
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,8 @@ import android.util.Rational
|
||||||
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
||||||
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
|
||||||
|
|
||||||
enum class PlayerEventType(val value: Int) {
|
enum class PlayerEventType(val value: Int) {
|
||||||
//Stop(-1),
|
|
||||||
Pause(0),
|
Pause(0),
|
||||||
Play(1),
|
Play(1),
|
||||||
SeekForward(2),
|
SeekForward(2),
|
||||||
|
|
@ -27,6 +25,7 @@ enum class PlayerEventType(val value: Int) {
|
||||||
Resize(13),
|
Resize(13),
|
||||||
SearchSubtitlesOnline(14),
|
SearchSubtitlesOnline(14),
|
||||||
SkipOp(15),
|
SkipOp(15),
|
||||||
|
Restart(16),
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class CSPlayerEvent(val value: Int) {
|
enum class CSPlayerEvent(val value: Int) {
|
||||||
|
|
@ -40,6 +39,7 @@ enum class CSPlayerEvent(val value: Int) {
|
||||||
PrevEpisode(6),
|
PrevEpisode(6),
|
||||||
PlayPauseToggle(7),
|
PlayPauseToggle(7),
|
||||||
ToggleMute(8),
|
ToggleMute(8),
|
||||||
|
Restart(9),
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class CSPlayerLoading {
|
enum class CSPlayerLoading {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,29 @@
|
||||||
package com.lagradost.cloudstream3.ui.player
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.lagradost.cloudstream3.TvType
|
||||||
import com.lagradost.cloudstream3.amap
|
import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
import java.net.URI
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
import com.lagradost.cloudstream3.utils.unshortenLinkSafe
|
||||||
|
|
||||||
|
data class ExtractorUri(
|
||||||
|
val uri: Uri,
|
||||||
|
val name: String,
|
||||||
|
|
||||||
|
val basePath: String? = null,
|
||||||
|
val relativePath: String? = null,
|
||||||
|
val displayName: String? = null,
|
||||||
|
|
||||||
|
val id: Int? = null,
|
||||||
|
val parentId: Int? = null,
|
||||||
|
val episode: Int? = null,
|
||||||
|
val season: Int? = null,
|
||||||
|
val headerName: String? = null,
|
||||||
|
val tvType: TvType? = null,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to open the player more easily with the LinkGenerator
|
* Used to open the player more easily with the LinkGenerator
|
||||||
|
|
@ -19,6 +39,7 @@ class LinkGenerator(
|
||||||
private val isM3u8: Boolean? = null
|
private val isM3u8: Boolean? = null
|
||||||
) : IGenerator {
|
) : IGenerator {
|
||||||
override val hasCache = false
|
override val hasCache = false
|
||||||
|
override val canSkipLoading = true
|
||||||
|
|
||||||
override fun getCurrentId(): Int? {
|
override fun getCurrentId(): Int? {
|
||||||
return null
|
return null
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import android.os.Message;
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.OptIn;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.text.Cue;
|
import androidx.media3.common.text.Cue;
|
||||||
|
|
@ -66,7 +67,7 @@ import java.util.stream.Collectors;
|
||||||
* obtained from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s
|
* obtained from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s
|
||||||
* is delegated to a {@link TextOutput}.
|
* is delegated to a {@link TextOutput}.
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
public class NonFinalTextRenderer extends BaseRenderer implements Callback {
|
public class NonFinalTextRenderer extends BaseRenderer implements Callback {
|
||||||
|
|
||||||
private static final String TAG = "TextRenderer";
|
private static final String TAG = "TextRenderer";
|
||||||
|
|
@ -74,7 +75,7 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback {
|
||||||
/**
|
/**
|
||||||
* @param trackType The track type that the renderer handles. One of the {@link C} {@code
|
* @param trackType The track type that the renderer handles. One of the {@link C} {@code
|
||||||
* TRACK_TYPE_*} constants.
|
* TRACK_TYPE_*} constants.
|
||||||
* @param outputHandler
|
* @param outputHandler todo description
|
||||||
*/
|
*/
|
||||||
public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) {
|
public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) {
|
||||||
super(trackType);
|
super(trackType);
|
||||||
|
|
@ -416,13 +417,11 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Override
|
@Override
|
||||||
public boolean handleMessage(Message msg) {
|
public boolean handleMessage(Message msg) {
|
||||||
switch (msg.what) {
|
if (msg.what == MSG_UPDATE_OUTPUT) {
|
||||||
case MSG_UPDATE_OUTPUT:
|
invokeUpdateOutputInternal((List<Cue>) msg.obj);
|
||||||
invokeUpdateOutputInternal((List<Cue>) msg.obj);
|
return true;
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
}
|
||||||
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void invokeUpdateOutputInternal(List<Cue> cues) {
|
private void invokeUpdateOutputInternal(List<Cue> cues) {
|
||||||
|
|
@ -441,7 +440,6 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback {
|
||||||
}
|
}
|
||||||
).collect(Collectors.toList());
|
).collect(Collectors.toList());
|
||||||
|
|
||||||
output.onCues(fixedCues);
|
|
||||||
output.onCues(new CueGroup(fixedCues, 0L));
|
output.onCues(new CueGroup(fixedCues, 0L));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ContentUris
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.content.ContextCompat.getString
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||||
|
import com.lagradost.safefile.SafeFile
|
||||||
|
|
||||||
|
object OfflinePlaybackHelper {
|
||||||
|
fun playLink(activity: Activity, url: String) {
|
||||||
|
activity.navigate(
|
||||||
|
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
|
||||||
|
LinkGenerator(
|
||||||
|
listOf(
|
||||||
|
BasicLink(url)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun playUri(activity: Activity, uri: Uri) {
|
||||||
|
val name = SafeFile.fromUri(activity, uri)?.name()
|
||||||
|
activity.navigate(
|
||||||
|
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
|
||||||
|
DownloadFileGenerator(
|
||||||
|
listOf(
|
||||||
|
ExtractorUri(
|
||||||
|
uri = uri,
|
||||||
|
name = name ?: getString(activity, R.string.downloaded_file),
|
||||||
|
// well not the same as a normal id, but we take it as users may want to
|
||||||
|
// play downloaded files and save the location
|
||||||
|
id = kotlin.runCatching { ContentUris.parseId(uri) }.getOrNull()?.hashCode()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse
|
||||||
import com.lagradost.cloudstream3.mvvm.Resource
|
import com.lagradost.cloudstream3.mvvm.Resource
|
||||||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
|
@ -14,13 +15,12 @@ import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class PlayerGeneratorViewModel : ViewModel() {
|
class PlayerGeneratorViewModel : ViewModel() {
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "PlayViewGen"
|
const val TAG = "PlayViewGen"
|
||||||
}
|
}
|
||||||
|
|
||||||
private var generator: IGenerator? = null
|
private var generator: IGenerator? = null
|
||||||
|
|
@ -111,6 +111,9 @@ class PlayerGeneratorViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun getLoadResponse(): LoadResponse? {
|
||||||
|
return normalSafeApiCall { (generator as? RepoLinkGenerator?)?.page }
|
||||||
|
}
|
||||||
|
|
||||||
fun getMeta(): Any? {
|
fun getMeta(): Any? {
|
||||||
return normalSafeApiCall { generator?.getCurrent() }
|
return normalSafeApiCall { generator?.getCurrent() }
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.ui.SubtitleView
|
import androidx.media3.ui.SubtitleView
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat
|
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat
|
||||||
|
|
@ -47,6 +49,7 @@ data class SubtitleData(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
class PlayerSubtitleHelper {
|
class PlayerSubtitleHelper {
|
||||||
private var activeSubtitles: Set<SubtitleData> = emptySet()
|
private var activeSubtitles: Set<SubtitleData> = emptySet()
|
||||||
private var allSubtitles: Set<SubtitleData> = emptySet()
|
private var allSubtitles: Set<SubtitleData> = emptySet()
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,15 @@ import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.core.graphics.scale
|
import androidx.core.graphics.scale
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication
|
||||||
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper2
|
import com.lagradost.cloudstream3.utils.M3u8Helper2
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -62,8 +65,12 @@ interface IPreviewGenerator {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun new(): IPreviewGenerator {
|
fun new(): IPreviewGenerator {
|
||||||
|
val userDisabled = AcraApplication.context?.let { ctx ->
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(ctx)?.getBoolean(
|
||||||
|
ctx.getString(R.string.preview_seekbar_key), true) == false
|
||||||
|
} ?: false
|
||||||
/** because TV has low ram + not show we disable this for now */
|
/** because TV has low ram + not show we disable this for now */
|
||||||
return if (SettingsFragment.isTrueTvSettings()) {
|
return if (isLayout(TV) || userDisabled) {
|
||||||
empty()
|
empty()
|
||||||
} else {
|
} else {
|
||||||
PreviewGenerator()
|
PreviewGenerator()
|
||||||
|
|
@ -239,7 +246,11 @@ private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewG
|
||||||
// generated images 1:1 to idx of hsl
|
// generated images 1:1 to idx of hsl
|
||||||
private var images: Array<Bitmap?> = arrayOf()
|
private var images: Array<Bitmap?> = arrayOf()
|
||||||
|
|
||||||
private val TAG = "PreviewImgM3u8"
|
companion object {
|
||||||
|
private const val TAG = "PreviewImgM3u8"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// prefixSum[i] = sum(hsl.ts[0..i].time)
|
// 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
|
// where [0] = 0, [1] = hsl.ts[0].time aka time at start of segment, do [b] - [a] for range a,b
|
||||||
|
|
@ -388,13 +399,6 @@ private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewG
|
||||||
logError(t)
|
logError(t)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
val buffer = hsl.resolveLinkSafe(index) ?: continue
|
|
||||||
tmpFile?.writeBytes(buffer)
|
|
||||||
val buff = FileOutputStream(tmpFile)
|
|
||||||
retriever.setDataSource(buff.fd)
|
|
||||||
val frame = retriever.getFrameAtTime(0L)*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -412,14 +416,16 @@ private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGe
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "PreviewImgMp4"
|
||||||
|
}
|
||||||
|
|
||||||
override fun hasPreview(): Boolean {
|
override fun hasPreview(): Boolean {
|
||||||
synchronized(images) {
|
synchronized(images) {
|
||||||
return loadedLod >= MIN_LOD
|
return loadedLod >= MIN_LOD
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val TAG = "PreviewImgMp4"
|
|
||||||
|
|
||||||
override fun getPreviewImage(fraction: Float): Bitmap? {
|
override fun getPreviewImage(fraction: Float): Bitmap? {
|
||||||
synchronized(images) {
|
synchronized(images) {
|
||||||
if (loadedLod < MIN_LOD) {
|
if (loadedLod < MIN_LOD) {
|
||||||
|
|
@ -524,7 +530,7 @@ private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGe
|
||||||
val fraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat()))
|
val fraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat()))
|
||||||
Log.i(TAG, "Generating preview for ${fraction * 100}%")
|
Log.i(TAG, "Generating preview for ${fraction * 100}%")
|
||||||
val frame = durationUs * fraction
|
val frame = durationUs * fraction
|
||||||
val img = retriever.image(frame.toLong(), params);
|
val img = retriever.image(frame.toLong(), params)
|
||||||
if (!scope.isActive) return
|
if (!scope.isActive) return
|
||||||
if (img == null || img.width <= 1 || img.height <= 1) continue
|
if (img == null || img.width <= 1 || img.height <= 1) continue
|
||||||
synchronized(images) {
|
synchronized(images) {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
package com.lagradost.cloudstream3.ui.player
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||||
import com.lagradost.cloudstream3.APIHolder.unixTime
|
import com.lagradost.cloudstream3.APIHolder.unixTime
|
||||||
import com.lagradost.cloudstream3.LoadResponse
|
import com.lagradost.cloudstream3.LoadResponse
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository
|
import com.lagradost.cloudstream3.ui.APIRepository
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
@ -29,6 +29,7 @@ class RepoLinkGenerator(
|
||||||
}
|
}
|
||||||
|
|
||||||
override val hasCache = true
|
override val hasCache = true
|
||||||
|
override val canSkipLoading = true
|
||||||
|
|
||||||
override fun hasNext(): Boolean {
|
override fun hasNext(): Boolean {
|
||||||
return currentIndex < episodes.size - 1
|
return currentIndex < episodes.size - 1
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.lagradost.cloudstream3.databinding.PlayerPrioritizeItemBinding
|
import com.lagradost.cloudstream3.databinding.PlayerPrioritizeItemBinding
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils
|
import com.lagradost.cloudstream3.utils.AppContextUtils
|
||||||
|
|
||||||
data class SourcePriority<T>(
|
data class SourcePriority<T>(
|
||||||
val data: T,
|
val data: T,
|
||||||
|
|
@ -13,11 +13,10 @@ data class SourcePriority<T>(
|
||||||
)
|
)
|
||||||
|
|
||||||
class PriorityAdapter<T>(override val items: MutableList<SourcePriority<T>>) :
|
class PriorityAdapter<T>(override val items: MutableList<SourcePriority<T>>) :
|
||||||
AppUtils.DiffAdapter<SourcePriority<T>>(items) {
|
AppContextUtils.DiffAdapter<SourcePriority<T>>(items) {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return PriorityViewHolder(
|
return PriorityViewHolder(
|
||||||
PlayerPrioritizeItemBinding.inflate(LayoutInflater.from(parent.context),parent,false),
|
PlayerPrioritizeItemBinding.inflate(LayoutInflater.from(parent.context),parent,false),
|
||||||
//LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,10 +30,6 @@ class PriorityAdapter<T>(override val items: MutableList<SourcePriority<T>>) :
|
||||||
val binding: PlayerPrioritizeItemBinding,
|
val binding: PlayerPrioritizeItemBinding,
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
fun <T> bind(item: SourcePriority<T>) {
|
fun <T> bind(item: SourcePriority<T>) {
|
||||||
/* val plusButton: ImageView = itemView.add_button
|
|
||||||
val subtractButton: ImageView = itemView.subtract_button
|
|
||||||
val priorityText: TextView = itemView.priority_text
|
|
||||||
val priorityNumber: TextView = itemView.priority_number*/
|
|
||||||
binding.priorityText.text = item.name
|
binding.priorityText.text = item.name
|
||||||
|
|
||||||
fun updatePriority() {
|
fun updatePriority() {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.databinding.PlayerQualityProfileItemBinding
|
import com.lagradost.cloudstream3.databinding.PlayerQualityProfileItemBinding
|
||||||
import com.lagradost.cloudstream3.ui.result.UiImage
|
import com.lagradost.cloudstream3.ui.result.UiImage
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils
|
import com.lagradost.cloudstream3.utils.AppContextUtils
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
|
|
||||||
class ProfilesAdapter(
|
class ProfilesAdapter(
|
||||||
|
|
@ -21,7 +21,7 @@ class ProfilesAdapter(
|
||||||
val usedProfile: Int,
|
val usedProfile: Int,
|
||||||
val clickCallback: (oldIndex: Int?, newIndex: Int) -> Unit,
|
val clickCallback: (oldIndex: Int?, newIndex: Int) -> Unit,
|
||||||
) :
|
) :
|
||||||
AppUtils.DiffAdapter<QualityDataHelper.QualityProfile>(
|
AppContextUtils.DiffAdapter<QualityDataHelper.QualityProfile>(
|
||||||
items,
|
items,
|
||||||
comparison = { first: QualityDataHelper.QualityProfile, second: QualityDataHelper.QualityProfile ->
|
comparison = { first: QualityDataHelper.QualityProfile, second: QualityDataHelper.QualityProfile ->
|
||||||
first.id == second.id
|
first.id == second.id
|
||||||
|
|
@ -29,8 +29,6 @@ class ProfilesAdapter(
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return ProfilesViewHolder(
|
return ProfilesViewHolder(
|
||||||
PlayerQualityProfileItemBinding.inflate(LayoutInflater.from(parent.context),parent,false)
|
PlayerQualityProfileItemBinding.inflate(LayoutInflater.from(parent.context),parent,false)
|
||||||
//LayoutInflater.from(parent.context)
|
|
||||||
// .inflate(R.layout.player_quality_profile_item, parent, false)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.lagradost.cloudstream3.ui.player.source_priority
|
package com.lagradost.cloudstream3.ui.player.source_priority
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
|
|
@ -104,7 +103,7 @@ object QualityDataHelper {
|
||||||
* Must under all circumstances at least return one profile
|
* Must under all circumstances at least return one profile
|
||||||
**/
|
**/
|
||||||
fun getProfiles(): List<QualityProfile> {
|
fun getProfiles(): List<QualityProfile> {
|
||||||
val availableTypes = QualityProfileType.values().toMutableList()
|
val availableTypes = QualityProfileType.entries.toMutableList()
|
||||||
val profiles = (1..PROFILE_COUNT).map { profileNumber ->
|
val profiles = (1..PROFILE_COUNT).map { profileNumber ->
|
||||||
// Get the real type
|
// Get the real type
|
||||||
val type = getQualityProfileType(profileNumber)
|
val type = getQualityProfileType(profileNumber)
|
||||||
|
|
@ -140,12 +139,12 @@ object QualityDataHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QualityProfileType.values().forEach {
|
QualityProfileType.entries.forEach {
|
||||||
if (it.unique) insertType(profiles, it)
|
if (it.unique) insertType(profiles, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
debugAssert({
|
debugAssert({
|
||||||
!QualityProfileType.values().all { type ->
|
!QualityProfileType.entries.all { type ->
|
||||||
!type.unique || profiles.any { it.type == type }
|
!type.unique || profiles.any { it.type == type }
|
||||||
}
|
}
|
||||||
}, { "All unique quality types do not exist" })
|
}, { "All unique quality types do not exist" })
|
||||||
|
|
|
||||||
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