From a95fcfc9dba211187470f4250db4ca1e96eb7564 Mon Sep 17 00:00:00 2001 From: Saksham Shekher <95137948+OshekharO@users.noreply.github.com> Date: Tue, 27 Jun 2023 18:24:05 +0530 Subject: [PATCH 001/156] SpeedoStream update (#493) --- .../com/lagradost/cloudstream3/extractors/SpeedoStream.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt index 8ef6c463..90104ace 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt @@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper class SpeedoStream1 : SpeedoStream() { - override val mainUrl = "https://speedostream.nl" + override val mainUrl = "https://speedostream.pm" } open class SpeedoStream : ExtractorApi() { @@ -39,4 +39,4 @@ open class SpeedoStream : ExtractorApi() { ) -} \ No newline at end of file +} From da0be63b7ce5241e69d1221f60d1a24f49d778e3 Mon Sep 17 00:00:00 2001 From: Nexus <79303560+Nexus-Gits@users.noreply.github.com> Date: Fri, 30 Jun 2023 21:49:52 +0530 Subject: [PATCH 002/156] Update ByteShare.kt (#494) * Update ByteShare.kt --- .../java/com/lagradost/cloudstream3/extractors/ByteShare.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt index 3e0a03c0..2d56fe1f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt @@ -4,7 +4,7 @@ import com.lagradost.cloudstream3.utils.* open class ByteShare : ExtractorApi() { override val name = "ByteShare" - override val mainUrl = "https://byteshare.net" + override val mainUrl = "https://byteshare.to" override val requiresReferer = false override suspend fun getUrl(url: String, referer: String?): List { @@ -20,4 +20,4 @@ open class ByteShare : ExtractorApi() { ) return sources } -} \ No newline at end of file +} From 51c10891626fee6e3a1638ad4c1e020f1fc6bf05 Mon Sep 17 00:00:00 2001 From: Nexus <79303560+Nexus-Gits@users.noreply.github.com> Date: Sun, 2 Jul 2023 23:11:19 +0530 Subject: [PATCH 003/156] Updated some of the extractor Domains (#495) * Update AsianLoad.kt Asianload domain changed * Update Hxfile.kt Some of the old url fix * Update Moviehab.kt * Update MultiQuality.kt * Update AsianLoad.kt * Update Hxfile.kt * Update MultiQuality.kt --- .../java/com/lagradost/cloudstream3/extractors/AsianLoad.kt | 4 ++-- .../java/com/lagradost/cloudstream3/extractors/Hxfile.kt | 6 +++--- .../java/com/lagradost/cloudstream3/extractors/Moviehab.kt | 4 ++-- .../com/lagradost/cloudstream3/extractors/MultiQuality.kt | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt index 7a62fb52..4bed3169 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt @@ -9,7 +9,7 @@ import java.net.URI open class AsianLoad : ExtractorApi() { override var name = "AsianLoad" - override var mainUrl = "https://asianembed.io" + override var mainUrl = "https://asianhdplay.pro" override val requiresReferer = true private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""") @@ -43,4 +43,4 @@ open class AsianLoad : ExtractorApi() { return extractedLinksList } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt index f5dde774..bfd7cae5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt @@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson class Neonime7n : Hxfile() { override val name = "Neonime7n" - override val mainUrl = "https://7njctn.neonime.watch" + override val mainUrl = "https://neonime.fun" override val redirect = false } @@ -19,7 +19,7 @@ class Neonime8n : Hxfile() { class KotakAnimeid : Hxfile() { override val name = "KotakAnimeid" - override val mainUrl = "https://kotakanimeid.com" + override val mainUrl = "https://nontonanimeid.bio" override val requiresReferer = true } @@ -97,4 +97,4 @@ open class Hxfile : ExtractorApi() { @JsonProperty("label") val label: String? ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt index aaa33ca1..51939cc2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt @@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper class MoviehabNet : Moviehab() { - override var mainUrl = "https://play.moviehab.net" + override var mainUrl = "https://play.moviehab.asia" } open class Moviehab : ExtractorApi() { @@ -41,4 +41,4 @@ open class Moviehab : ExtractorApi() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt index 44657196..c7f4ac76 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt @@ -9,7 +9,7 @@ import java.net.URI open class MultiQuality : ExtractorApi() { override var name = "MultiQuality" - override var mainUrl = "https://gogo-play.net" + override var mainUrl = "https://anihdplay.com" private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""") private val m3u8Regex = Regex(""".*?(\d*).m3u8""") private val urlRegex = Regex("""(.*?)([^/]+$)""") @@ -56,4 +56,4 @@ open class MultiQuality : ExtractorApi() { return extractedLinksList } } -} \ No newline at end of file +} From 847957362f498c7801ff3d92c4e2a914d3202a11 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Mon, 10 Jul 2023 06:52:03 +0700 Subject: [PATCH 004/156] Extractor: added Pixeldrain, Wibufile and fix some extractors (#502) Co-authored-by: Sofie99 --- .../cloudstream3/extractors/DoodExtractor.kt | 4 ++ .../cloudstream3/extractors/Filesim.kt | 19 +++++++++- .../cloudstream3/extractors/Pixeldrain.kt | 30 +++++++++++++++ .../cloudstream3/extractors/StreamSB.kt | 25 +++++++++++++ .../cloudstream3/extractors/Wibufile.kt | 37 +++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 11 ++++++ 6 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Pixeldrain.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt index 24495a40..8dcfb859 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt @@ -7,6 +7,10 @@ import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.getQualityFromName import kotlinx.coroutines.delay +class Dooood : DoodLaExtractor() { + override var mainUrl = "https://dooood.com" +} + class DoodWfExtractor : DoodLaExtractor() { override var mainUrl = "https://dood.wf" } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt index 4c1791a8..be0efd0c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt @@ -5,6 +5,16 @@ import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 +class Guccihide : Filesim() { + override val name = "Guccihide" + override var mainUrl = "https://guccihide.com" +} + +class Ahvsh : Filesim() { + override val name = "Ahvsh" + override var mainUrl = "https://ahvsh.com" +} + class Moviesm4u : Filesim() { override val mainUrl = "https://moviesm4u.com" override val name = "Moviesm4u" @@ -15,6 +25,11 @@ class FileMoonIn : Filesim() { override val name = "FileMoon" } +class StreamhideTo : Filesim() { + override val mainUrl = "https://streamhide.to" + override val name = "Streamhide" +} + class StreamhideCom : Filesim() { override var name: String = "Streamhide" override var mainUrl: String = "https://streamhide.com" @@ -42,7 +57,7 @@ class FileMoonSx : Filesim() { open class Filesim : ExtractorApi() { override val name = "Filesim" override val mainUrl = "https://files.im" - override val requiresReferer = false + override val requiresReferer = true override suspend fun getUrl( url: String, @@ -50,7 +65,7 @@ open class Filesim : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val response = app.get(url, referer = mainUrl).document + val response = app.get(url, referer = referer).document response.select("script[type=text/javascript]").map { script -> if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) { val unpackedscript = getAndUnpack(script.data()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Pixeldrain.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pixeldrain.kt new file mode 100644 index 00000000..9b481240 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pixeldrain.kt @@ -0,0 +1,30 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities + +open class Pixeldrain : ExtractorApi() { + override val name = "Pixeldrain" + override val mainUrl = "https://pixeldrain.com" + override val requiresReferer = false + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val mId = Regex("/([ul]/[\\da-zA-Z\\-]+)").find(url)?.groupValues?.get(1)?.split("/") + callback.invoke( + ExtractorLink( + this.name, + this.name, + "$mainUrl/api/file/${mId?.last() ?: return}?download", + url, + Qualities.Unknown.value, + ) + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt index 3d2a81b7..df050cf3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt @@ -8,6 +8,31 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper import kotlin.random.Random +class Sblona : StreamSB() { + override var name = "Sblona" + override var mainUrl = "https://sblona.com" +} + +class Lvturbo : StreamSB() { + override var name = "Lvturbo" + override var mainUrl = "https://lvturbo.com" +} + +class Sbrapid : StreamSB() { + override var name = "Sbrapid" + override var mainUrl = "https://sbrapid.com" +} + +class Sbface : StreamSB() { + override var name = "Sbface" + override var mainUrl = "https://sbface.com" +} + +class Sbsonic : StreamSB() { + override var name = "Sbsonic" + override var mainUrl = "https://sbsonic.com" +} + class Vidgomunimesb : StreamSB() { override var mainUrl = "https://vidgomunimesb.xyz" } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt new file mode 100644 index 00000000..ae1e872a --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt @@ -0,0 +1,37 @@ +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.Qualities +import java.net.URI + +open class Wibufile : ExtractorApi() { + override val name: String = "Wibufile" + override val mainUrl: String = "https://wibufile.com" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val res = app.get(url).text + val video = Regex("src: ['\"](.*?)['\"]").find(res)?.groupValues?.get(1) + + callback.invoke( + ExtractorLink( + name, + name, + video ?: return, + "$mainUrl/", + Qualities.Unknown.value, + URI(url).path.endsWith(".m3u8") + ) + ) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index f6373dce..f0c1ea3b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -244,6 +244,7 @@ val extractorApis: MutableList = arrayListOf( XStreamCdn(), StreamSB(), + Sblona(), Vidgomunimesb(), StreamSB1(), StreamSB2(), @@ -265,6 +266,10 @@ val extractorApis: MutableList = arrayListOf( Sbflix(), Streamsss(), Sbspeed(), + Sbsonic(), + Sbface(), + Sbrapid(), + Lvturbo(), Fastream(), @@ -300,6 +305,7 @@ val extractorApis: MutableList = arrayListOf( DoodToExtractor(), DoodSoExtractor(), DoodLaExtractor(), + Dooood(), DoodWsExtractor(), DoodShExtractor(), DoodWatchExtractor(), @@ -366,9 +372,14 @@ val extractorApis: MutableList = arrayListOf( Movhide(), StreamhideCom(), + StreamhideTo(), + Pixeldrain(), + Wibufile(), FileMoonIn(), Moviesm4u(), Filesim(), + Ahvsh(), + Guccihide(), FileMoon(), FileMoonSx(), Vido(), From 525bf8d86116036b8a1e370f44aa19db8fc0cbcf Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 9 Jul 2023 01:35:42 +0200 Subject: [PATCH 005/156] Translated using Weblate (Odia) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 38.9% (242 of 621 strings) Translated using Weblate (Dutch) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Turkish) Currently translated at 99.5% (618 of 621 strings) Translated using Weblate (Turkish) Currently translated at 99.5% (618 of 621 strings) Translated using Weblate (German) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Portuguese) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Malay) Currently translated at 20.4% (127 of 621 strings) Translated using Weblate (Italian) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (German) Currently translated at 99.8% (620 of 621 strings) Translated using Weblate (Polish) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Czech) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Odia) Currently translated at 39.1% (239 of 610 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Hebrew) Currently translated at 94.9% (579 of 610 strings) Translated using Weblate (Malay) Currently translated at 18.1% (111 of 610 strings) Translated using Weblate (Urdu) Currently translated at 100.0% (610 of 610 strings) Translated using Weblate (Odia) Currently translated at 39.0% (238 of 610 strings) Translated using Weblate (Norwegian Nynorsk) Currently translated at 46.0% (281 of 610 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 96.7% (590 of 610 strings) Translated using Weblate (Odia) Currently translated at 39.0% (238 of 610 strings) Translated using Weblate (Odia) Currently translated at 38.6% (236 of 610 strings) Translated using Weblate (Bengali) Currently translated at 38.1% (233 of 610 strings) Co-authored-by: Aftabuzzaman Co-authored-by: Aitor Salaberria Co-authored-by: Alexthegib Co-authored-by: Andreas Co-authored-by: Bananenbrot Co-authored-by: BluTiger Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com> Co-authored-by: Dan Co-authored-by: Efe Devirgen Co-authored-by: Eryk Michalak Co-authored-by: Ettore Atalan Co-authored-by: Fjuro Co-authored-by: Hosted Weblate Co-authored-by: Kai Co-authored-by: Levi Klippel Co-authored-by: Nathan Khutorskoy Co-authored-by: Rex_sa Co-authored-by: Subham Jena Co-authored-by: Sufyan Zahoor Jutt Co-authored-by: enescan201 Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bn/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/he/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ms/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nn/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/or/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ur/ Translation: Cloudstream/App --- app/src/main/res/values-ar/strings.xml | 19 ++- app/src/main/res/values-bn/strings.xml | 31 ++--- app/src/main/res/values-cs/strings.xml | 19 ++- app/src/main/res/values-de/strings.xml | 19 ++- app/src/main/res/values-es/strings.xml | 21 ++- app/src/main/res/values-in/strings.xml | 19 ++- app/src/main/res/values-it/strings.xml | 19 ++- app/src/main/res/values-iw/strings.xml | 4 +- app/src/main/res/values-ms/strings.xml | 40 +++++- app/src/main/res/values-nl/strings.xml | 19 ++- app/src/main/res/values-nn/strings.xml | 15 ++- app/src/main/res/values-no/strings.xml | 34 ++--- app/src/main/res/values-or/strings.xml | 11 +- app/src/main/res/values-pl/strings.xml | 19 ++- app/src/main/res/values-pt/strings.xml | 19 ++- app/src/main/res/values-tr/strings.xml | 23 +++- app/src/main/res/values-uk/strings.xml | 19 ++- app/src/main/res/values-ur/strings.xml | 179 ++++++++++++++++++++++++- 18 files changed, 473 insertions(+), 56 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index c1f07d6c..6b722e43 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -561,4 +561,21 @@ تجاوز حظر GitHub باستخدام jsdelivr ، قد يتسبب في تأخير التحديثات لبضعة أيام. وكيل raw.githubusercontent.com جودة المشاهدة المفضلة (بيانات الجوال) - + الملف الشخصي %d + واي فاي + بيانات الهاتف + تعيين الافتراضي + استخدام + تعديل + الملفات التعريفية + مساعدة + ‎‎هنا يمكنك تغيير طريقة ترتيب المصادر. إذا كان للفيديو أولوية أعلى ، فسيظهر في الأعلى في اختيار المصدر. يمثل مجموع أولوية المصدر وأولوية الجودة أولوية الفيديو. +\n +\nالمصدر أ: 3 +\nالجودة ب: 7 +\nسيكون لها أولوية فيديو مجمعة تبلغ 10. +\n +\nملاحظة: إذا كان المجموع 10 أو أكثر ، فسيتخطى المشغل التحميل تلقائيًا عند تحميل هذا الرابط! + النوعيات + خلفية الملف الشخصي + \ No newline at end of file diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 12752938..cc272a3a 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -7,7 +7,7 @@ %dদিন %dঘন্টা %dমিনিট %dঘন্টা %dমিনিট %d মিনিট - এপিসোড পোস্টার + পর্বের পোস্টার মূল পোস্টার পরবর্তী র‍্যান্ডম ফিরে যান @@ -24,9 +24,9 @@ ডাউনলোডসমূহ সেটিংস %s এপি %d - এপিসোড %d রিলিজ হবে + পর্ব %d মুক্তির তারিখ শেয়ার - ব্রাঊজারে খুলুন + ব্রাউজারে খুলুন জনরা খুঁজুন… খুঁজুন %s… @@ -35,7 +35,7 @@ পিছনে যান কোন তথ্য নেই আরো অপশন - পরবর্তি এপিসোড + পরবর্তী পর্ব লোডিং বাদ দিন লোডিং… দেখা হচ্ছে @@ -46,10 +46,10 @@ কোন কিছুই না পুনরায় দেখা হচ্ছে টরেন্ট স্ট্রিম করুন - সোর্সসমূহ + উৎসসমূহ সাবটাইটেলসমূহ আবার সংযোগ দেওয়ার চেষ্টা করুন… - এপিসোড চালান + পর্ব চালান ডাউনলোড ডাউনলোড হয়েছে ডাউনলোড চলছে @@ -61,22 +61,22 @@ লিংক লোডিং ব্যর্থ ডাব সাব - ফাইল প্লে করুন + ফাইল চালান ডাউনলোড থামান আটো বাগ রিপোর্ট বন্ধ করুন আরো তথ্য বন্ধ করুন - প্লে + চালান তথ্য বুকমার্কসমূহ বাদ দিন বুকমার্ক করুন - এপ্লাই করুন + প্রয়োগ করুন বাদ দিন কপি করুন বন্ধ করুন মুছুন - সেভ করুন + সংরক্ষণ করুন ভিডিও এর গতি ফন্ট আউটলাইন কালার সাবটাইটেল এজের ধরণ @@ -100,11 +100,11 @@ ডাউনলোড ব্যর্থ ইন্টারনাল স্টোরেজ বুকমার্ক ফিল্টার করুন - ফাইল ডিলিট করুন + ফাইল মুছুন ডাউনলোড চালু করুন সাবটাইটেল সেটিংস সাবটাইটেল ব্যাকগ্রাউন্ড কালার - ফন্ট কালার + লেখার রং এটা একটি টরেন্ট প্রভাইডার, ভিপিএন ব্যবহার করা উচিৎ উইন্ডো কালার ফন্টের সাইজ @@ -130,8 +130,8 @@ ভিডিওপ্লেয়ার এ সময় নিয়ন্ত্রণ করতে, ডানে অথবা বামে সোয়াইপ করুন সেটিংস পরিবর্তন করতে সোয়াইপ করুন উজ্জ্বলতা অথবা স্বরমাত্রা পরিবর্তন করতে যথাক্রমে বামে অথবা ডানে সোয়াইপ করুন - স্বয়ংক্রিয়ভাবে পরবর্তী এপিসোড প্লে করুন - পরবর্তী এপিসোডটি চালু করুন যখন চলতি এপিসোডটি শেষ হয় + স্বয়ংক্রিয়ভাবে পরবর্তী পর্ব চালান + বর্তমান পর্বটি শেষ হলে পরের পর্বটি চালান থামতে দুইবার চাপুন ডাটা জমা হয়েছে স্টোরেজ এর অনুমতি অনুপস্থিত। দয়া করে আবার চেষ্টা করুন। @@ -148,4 +148,5 @@ আগাতে ডবল ট্যাপ করুন আইজেনগ্রাভি মোড আপডেট শুরু হয়েছে - + ব্রাউজার + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 16ceff2d..5cbdb7db 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -553,4 +553,21 @@ Vrátit zpět Obchází blokování GitHubu pomocí jsDelivr. Může způsobit zpoždění aktualizací o několik dní. Obcházení ISP - + Profil %d + Wi-Fi + Mobilní data + Nastavit jako výchozí + Použít + Upravit + Profily + Nápověda + Kvality + Pozadí profilu + Zde můžete změnit pořadí zdrojů. Pokud má video vyšší prioritu, objeví se ve výběru zdrojů výše. Součet priority zdroje a priority kvality je priorita videa. +\n +\nZdroj A: 3 +\nKvalita B: 7 +\nBudou mít celkovou prioritu videa 10. +\n +\nPOZNÁMKA: Pokud je součet 10 nebo vyšší, přehrávač automaticky přeskočí načítání při načtení daného odkazu! + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e0a9594c..69a850b3 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -529,4 +529,21 @@ Rückgängig Abonniert ISP-Umgehungen - + Profil %d + Wi-Fi + Mobile Daten + Standard festlegen + Verwenden + Bearbeiten + Profile + Hilfe + Qualitäten + Profil-Hintergrund + Hier kannst du verändern, wie die Quellen geordnet werden. Wenn ein Video eine höhere Priorität hat, wird es höher in der Quellenauswahl erscheinen. Die Summe der Quellenpriorität und der Qualitätspriorität ist die Videopriorität. +\n +\nQuelle A: 3 +\nQualität B: 7 +\nWerden eine kombinierte Videopriorität von 10 haben. +\n +\nHINWEIS: Wenn die Summe 10 oder mehr beträgt, überspringt der Player automatisch das Laden, wenn der Link geladen wird! + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d248044d..4326bccd 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -503,7 +503,7 @@ Seleccionar biblioteca Abrir con Tu biblioteca está vacía :( -\nRegístrate con una cuenta en la biblioteca o agrega los títulos a tu biblioteca local. +\nInicia sesión en una cuenta de biblioteca o añade series desde tu biblioteca local. ¡Se encontró un archivo en modo seguro! \nNo cargar ninguna extensión al inicio hasta que se elimine el archivo. Reproductor visible - buscar cantidad @@ -529,4 +529,21 @@ Revertir ISP Bypasses Calidad de visualización preferida (Datos móviles) - + Ayuda + Aquí puedes cambiar el orden de las fuentes. Si un vídeo tiene una prioridad más alta, aparecerá más arriba en la selección de las fuentes. La suma de la prioridad de la fuente y la prioridad de la calidad es la prioridad del vídeo. +\n +\nFuente A: 3 +\nCalidad B: 7 +\nTendrá una prioridad en el vídeo combinada de 10. +\n +\nNOTA: ¡Si la suma es 10 o más el reproductor saltará automáticamente la carga cuando se cargue ese enlace! + Perfil %d + Wifi + Editar + Perfiles + Datos móviles + Establecer por defecto + Usar + Calidades + Perfil del fondo + \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index a8c6a197..e519d062 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -552,4 +552,21 @@ Bypass ISP Pulihkan Nonton dengan kualitas yang di inginkan (Data Seluler) - + Data seluler + Bantuan + Di sini anda dapat mengubah sumber yang diurutkan. Jika video memiliki prioritas lebih tinggi, video tersebut akan muncul lebih tinggi dalam pemilihan sumber. Jumlah prioritas sumber dan prioritas kualitas adalah prioritas video. +\n +\nSumber A: 3 +\nKualitas B: 7 +\nAkan memiliki prioritas video yang digabung 10. +\n +\nCATATAN: Jika jumlahnya 10 atau lebih, pemain akan melewatkan pemuatan secara otomatis saat tautan dimuat! + Profil %d + Wifi + Pengaturan default + Gunakan + Edit + Profil + Kualitas + Latar belakang profil + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 6dca2e3a..9a90b6e9 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -551,4 +551,21 @@ Aggiornando shows a cui sei iscritto L\'episodio %d è stato rilasciato! Qualità di visualizzazione preferita (Dati mobili) - + Qui puoi cambiare l\'ordine delle fonti. Se un video ha una priorità maggiore, apparirà più in alto nella selezione delle fonti. La somma tra la priorità della fonte e la priorità della qualità è la priorità video. +\n +\nFonte A: 3 +\nQualità B: 7 +\nAvranno una priorità video combinata di 10. +\n +\nNOTA: se la somma è pari o superiore a 10, il lettore salterà automaticamente il caricamento quando viene caricato quel link! + Profilo %d + Wi-Fi + Imposta predefinito + Usa + Modifica + Profili + Aiuto + Dati Mobili + Qualità + Sfondo profilo + \ No newline at end of file diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 50e96c7c..e59cdd66 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -96,7 +96,7 @@ הורדת שפות שפת הכתוביות ייבא גופנים בהצבתם ב %s - להמשיך לצפות + המשך צפיה להסיר עוד מידע הספק הזה הוא טורנט, שימוש ב-VPN הוא מומלץ @@ -506,4 +506,4 @@ אלפביתי (ת\' עד א\') פתח עם נראה שהרשימה הזו ריקה, נסו לעבור לרשימה אחרת - + \ No newline at end of file diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 42eba3cc..7fd5504e 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -1,2 +1,40 @@ - + + Semua bahasa + Langkau %s + Pelayar web + Sejarah + Kosongkan sejarah + Pengenalan + Kredit + Pembukaan bercampur + Penamat + Pembukaan + Memasang kemaskini aplikasi… + Memuat turun kemaskini aplikasi… + Tidak + Ya + Adakah anda pasti anda ingin keluar\? + Keluarkan daripada ditonton + Tanda sebagai sudah ditonton + Tunjukkan pop timbul yang dilangkau untuk pembukaan/pengakhiran + Tidak dapat memasang versi aplikasi terkini + Terlalu banyak perkataan. Tidak dapat disimpan di papan klip. + Aplikasi tidak ditemui + Penamat bercampur + Imbas kembali + Episod %d akan disiarkan dalam + Pelakon:%s + Mod Selamat Hidup + %dd %dh %dm + %dh %dm + %dm + Poster Episod + Poster Utama + Pergi Kembali + Seterusnya Rawak + Tukar Pembekal + Kelajuan (%.2fx) + Poster + Poster + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index f56b0bfb..9054eada 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -551,4 +551,21 @@ \nWord lid van onze Discord of zoek online. Audiosporen Gesorteerd op - + Wifi + Mobiele data + Maak standaard + Aanpassen + Profielen + Help + Kwaliteiten + Profiel achtergrond + Gebruik + Hier kan je de volgorde van de bronnen veranderen. Als een video een hogere prioriteit heeft zal het hoger in de bronnenlijst staan. De som van de prioriteit van de bron en de prioriteit van de kwaliteit is de prioriteit van de video. +\n +\nBron A: 3 +\nKwaliteit B: 7 +\nHeeft een totale prioriteit van de video van 10. +\n +\nNOTITIE: Als de som 10 of hoger is zal de speler automatisch het laden overslaan wanneer die link is geladen! + Profiel %d + \ No newline at end of file diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index b3dda84f..135b7272 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -183,4 +183,17 @@ Varigheit Direktesendingar Programoppdateringar - + Trykk og hold inne for å nullstille til standardinnstillinger + Søk ved bruk av typer + Automatisk språkvalg + Last ned språk + Undertekstspråk + Metadata er ikke tilgjengelig fra nettsiden. Videoavspilling vil mislykkes hvis metadata ikke finnes på nettsiden. + Innstillinger for avspilling av undertekster + Plakat + Denne leverandøren er en torrent, og det anbefales å bruke en VPN-tjeneste. + For at denne leverandøren skal fungere korrekt, kan det være nødvendig å bruke en VPN-tjeneste + Bilde i bilde + Fortsett å sjå + Prøv tilkopling på nytt… + \ No newline at end of file diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 92882faf..65b5a462 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -2,7 +2,7 @@ Plakat - @string/result_poster_img_des + Plakat Episode Plakat Main Plakat Neste tilfeldig @@ -104,7 +104,7 @@ Sveip for å søke Sveip til venstre eller høyre for å kontrollere tiden i videospilleren Sveip for å endre innstillinger - Sveip på venstre eller høyre side for å endre lysstyrke eller volum + Sveip opp eller ned på venstre eller høyre side for å endre lysstyrke eller volum Dobbelttrykk til å søke Trykk to ganger på høyre eller venstre for å søke fremover eller bakover Bruk systemets lysstyrke @@ -117,7 +117,7 @@ Sender ingen rapportere Vis filler-episode til anime Vis appoppdateringer - Søk automatisk etter nye oppdateringer + Søk automatisk etter nye oppdateringer etter at appen har startet. Oppdatering til forhåndsutgivelser Søk etter oppdateringer før utgivelse i stedet for fullstendige utgivelser Github @@ -186,7 +186,7 @@ Hoppe OP Ikke vis igjen Oppdater - Foretrukket klokkekvalitet + Foretrukket avspillingskvalitet (WiFi) DNS rundt HTTPS Nyttig til omgå Internettleverandør hinder Vis dubbet/subbed Anime @@ -296,7 +296,7 @@ Chromecast-undertekster Autospill neste episode Historikk - Trykk i midten for å sette på pause + Trykk to ganger midt på skjermen for å pause Fant ingen episoder Fyllstoff Spill direktestrøm @@ -323,7 +323,7 @@ Hovedrolle Støtterolle Neste - Referent + Referanse Programtillegg slettet Pakkebrønnsnavn Pakkebrønnsnettadresse @@ -364,7 +364,7 @@ programtillegg Trygt modus påslått Sett visningsstatus - Auto-synkroniser din nåværende episode-framdrift + Synkroniser automatisk fremdriften din for gjeldende episode %d-%d Offentlig liste programtillegg @@ -399,7 +399,7 @@ Rulletekst Introduksjon Lagringstilgang mangler. Prøv igjen. - Vis forfilmer + Vis trailere Dubbingsetikett Undertekstetikett Videomellomlagringsstørrelse @@ -407,11 +407,11 @@ Videohurtiglager på disk Tøm video- og bildehurtiglager Legg til en klone av en eksisterende side, med en annen nettadresse - Installer alle programtillegg som ikke er installert enda fra de pakkebrønnene som er lagt til. + Installer automatisk alle plugins som ikke er installert fra de tilføyde depotene. Kvalitetsetikett Slå av/på grensesnittselementer på plakat Hopp over denne oppdateringen - Forårsaker tilfeldige krasj hvis satt for høyt. Ikke endre dette hvis du ikke har lite minne. + Kan forårsake krasj hvis satt for høyt på enheter med lite minne, for eksempel en Android TV. @string/home_play Sikkerhetskopier data Data lagret @@ -425,13 +425,13 @@ @string/anime Skjul valgt videokvalitet i søkeresultater Lastet inn sikkerhetkopifil - Oppdateringer og sikkerhetskopi + Oppdateringer og sikkerhetskopier @string/ova Avslutt\? Sensurerbart Alle %s er allerede nedlastet Kunne ikke installere den nye versjonen av programmet - Installerer ny versjon av programmet … + Installerer appoppdatering… Laster ned ny versjon av programmet … Synkroniser %s innlogget @@ -449,7 +449,7 @@ TS TC Fjern døveteksting fra undertekster - Minimale undertekster + Fjern unødvendig informasjon fra undertekster Ekstra Filtrer etter foretrukket mediaspråk Vev-videosending @@ -488,7 +488,7 @@ Undertekster Sikkerhetskopi APK-installatør - Noen enheter støtter ikke den nye pakkeinstallatøren. Prøv gammeldags alternativ hvis ting ikke installeres. + Noen telefoner støtter ikke den nye pakkeinstallatøren. Prøv det eldre alternativet hvis oppdateringer ikke blir installert. Oppdatering startet Programtillegg nedlastet Programmet vil oppgraderes når du avslutter det @@ -511,7 +511,7 @@ Vist avspiller — blafringsmengde Oppdatert (gammel til ny) Vellykket - Episode %d sluppet. + Episode %d sluppet! Foretrukket visningskvalitet (mobildata) Stopp Fjern fra sette @@ -525,7 +525,7 @@ Abonnerer på %s Blafringsmengde med skjult avspiller Velg bibliotek - Omgår blokkering av GitHub ved bruk av jsDelivr. Kan utsette oppdateringer et par dager. + Omgår blokkering av GitHub ved hjelp av jsDelivr. Dette kan forårsake forsinkelser på oppdateringer med noen få dager. ISP-omgåelser Denne listen er tom. Prøv å bytte til en annen. Sorter @@ -533,4 +533,4 @@ \nLaster ikke inn noen utvidelser ved oppstart til filen er fjernet. Biblioteket ditt er tomt :( \nLogg inn på en bibliotekkonto eller legg til programmer i ditt lokale bibliotek. - + \ No newline at end of file diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index eaa76652..19b071e4 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -27,7 +27,7 @@ ଅଧ୍ୟାୟ ଚଲାଅ କୌଣସି ଅଧ୍ୟାୟ ମିଳିଲା ନାହିଁ ଟି ଅଧ୍ୟାୟ - ଟିଏ ଅଧ୍ୟାୟ + ଅଧ୍ୟାୟ %s‌ରେ ଚଲାଅ ବ୍ରାଉଜର୍‌ରେ ଚଲାଅ ଉପଶୀର୍ଷକ ଡାଉନଲୋଡ୍ କରିବା @@ -146,4 +146,11 @@ ଉପଶୀର୍ଷକ +୩୦ ଵର୍ଷ - + ଅନ୍ତଃ-ଷ୍ଟୋରେଜ୍ + ପ୍ରଦାତା ବଦଳାଅ + ପ୍ରଦାତା ଵ୍ୟଵହାର କରି ଖୋଜିବା + ଶୀଘ୍ର ଆସୁଅଛି… + ଆପ୍ ଅଦ୍ୟତନ ଦେଖାଇବା + ଅଦ୍ୟତନ ଆରମ୍ଭ ହୋଇଛି + ସନ୍ଧାନ କରିବା… + \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 2961cb47..d08a760d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -532,4 +532,21 @@ Obchodzi blokadę GitHuba za pomocą jsDelivr. może spowodować opóźnienie aktualizacji o kilka dni. Nie udało się połączyć z GitHub, włączono serwer pośredniczący jsDelivr… Domyślna jakość (dane mobilne) - + W tym miejscu można zmienić kolejność źródeł. Jeśli wideo ma wyższy priorytet, pojawi się wyżej w wyborze źródła. Priorytet wideo jest sumą priorytetu źródła i priorytetu jakości. +\n +\nŹródło A: 3 +\nJakość B: 7 +\nŁączny priorytet wideo będzie wynosił 10. +\n +\nUWAGA: Jeśli suma wynosi 10 lub więcej, odtwarzacz automatycznie pominie ładowanie po załadowaniu tego łącza! + Profil %d + Wi-Fi + Dane mobilne + Ustaw domyślny + Użyj + Edytuj + Profile + Pomoc + Jakości + Tło profilu + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 705285eb..d91e79f8 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -529,4 +529,21 @@ Configurações padrão SD Faixas de áudio - + Perfil %d + Wi-Fi + Dados móveis + Definir predefinição + Utilização + Editar + Perfis + Ajuda + Qualidades + Perfil de fundo + Aqui pode alterar a forma como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais elevada, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte com a prioridade da qualidade é a prioridade do vídeo. +\n +\nFonte A: 3 +\nQualidade B: 7 +\nTerá uma prioridade de vídeo combinada de 10. +\n +\nNOTA: Se a soma for 10 ou mais, o leitor saltará automaticamente o carregamento quando essa ligação for carregada! + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 170c3679..372552bc 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -551,13 +551,13 @@ Alfabetik (Z - A) Kütüphane Seçin Şununla aç - Görünüşe göre bu liste boş, başka bir listeye geçmeyi deneyin + Görünüşe göre bu liste boş, başka bir listeye geçmeyi deneyin. Derecelendirme (Yüksekten Düşüğe) Derecelendirme (Düşükten Yükseğe) Yeniden başlat Oynatıcı gizlenmişken atlanacak süre İSS Kısıtlamaları - GitHub\'a ulaşılamadı, jsdelivr vekil sunucusu etkinleştiriliyor. + GitHub\'a ulaşılamadı. jsDelivr vekil sunucusu etkinleştiriliyor… Başlat Başarılı oldu raw.githubusercontent.com vekil sunucusu (proxy) @@ -577,4 +577,21 @@ %s kanalı aboneliğinden çıkıldı Günlük Oynatıcı görünür durumdayken atlanacak süre miktarı - + Wi-Fi + Profiller + Yardım + Profil %d + Kullan + Mobil veri + Varsayılanı ayarla + Düzenle + Burada kaynakların nasıl sıralandığını değiştirebilirsiniz. Bir video daha yüksek bir önceliğe sahipse, kaynak seçiminde daha yüksek görünecektir. Kaynak önceliği ve kalite önceliğinin toplamı video önceliğidir. +\n +\nKaynak A: 3 +\nKalite B: 7 +\nBirleştirilmiş video önceliği 10 olacaktır. +\n +\nNOT: Toplam 10 veya daha fazlaysa, bu bağlantı yüklendiğinde oynatıcı otomatik olarak yüklemeyi atlayacaktır! + Kaliteler + Profil arkaplanı + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 82527c95..09fd40d5 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -529,4 +529,21 @@ Обходи ISP Обхід блокування GitHub за допомогою jsDelivr. Можлива затримка оновлень на кілька днів. Бажана якість перегляду (Мобільні дані) - + Встановити за замовчуванням + Профілі + Допомога + Тут ви можете змінити порядок джерел. Якщо відео має вищий пріоритет, воно з\'явиться вище у списку джерел. Сума пріоритету джерела та пріоритету якості є пріоритетом відео. +\n +\nДжерело A: 3 +\nЯкість B: 7 +\nСумарний пріоритет відео дорівнюватиме 10. +\n +\nПРИМІТКА: Якщо сума пріоритетів дорівнює 10 або більше, плеєр автоматично пропустить завантаження при завантаженні цього посилання! + Профіль %d + Wi-Fi + Мобільні дані + Використовувати + Редагувати + Якості + Фон профілю + \ No newline at end of file diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index df2e9a8b..4c9091dd 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -165,9 +165,9 @@ ڈیفالٹ سیٹنگز پر ری سیٹ کرنے کے لیے دبائیں اور تھامیں سائٹ کی طرف سے میٹا ڈیٹا فراہم نہیں کیا گیا ہے، اور اگر ویڈیو سائٹ میں موجود نہیں ہے تو وہ لوڈ ہونے میں ناکام ہو جائے گا. پلیئر کے ترجمہ کی ترتیبات - ویڈیو پلیئر میں وقت کو کنٹرول کرنے کے لیے بائیں یا دائیں سوائپ کریں + ویڈیو میں اپنی پوزیشن کو کنٹرول کرنے کے لیے ایک طرف سے دوسری طرف سوائپ کریں سکرین کی روشنی یا والیوم تبدیل کرنے کے لیے بائیں یا دائیں سوائپ کریں - کنٹرول کریں کہ پلیئر کتنا آگے ہے + کنٹرول کریں کہ پلیئر کتنا آگے ہے(سیکنڈوں میں) توقف کرنے کے لیے درمیان میں دبائیں سٹوریج کی اجازتیں غائب ہیں، براہ کرم دوبارہ کوشش کریں۔ کٹسو سے پوسٹر نمایش کریں @@ -263,7 +263,7 @@ دوبارہ نہ دکھائیں اس اپ ڈیٹ کو چھوڑ دیں اپ ڈیٹ - ترجیحی ویڈیو کا معیار + ترجیحی ویڈیو کا معیار(وائ فائ) ویڈیو پلیئر کے عنوان کے زیادہ سے زیادہ حروف ویڈیو پلیئر ریزولوشن ویڈیو بفر سائز @@ -356,4 +356,175 @@ %d / 10 اٹھایا اگر سب ٹائٹلز %d ms بہت جلد دکھائے جائیں تو اسے استعمال کریں - + کریڈٹس + اضافی + مرکزی + ترتیب + قرارداد اور عنوان + HDR + %s سے ان سبسکرائب کیا گیا + ویب ویڈیو کاسٹ + 18+ + لاگ + سلسلہ سے لنک کریں + اینڈرائیڈ ٹی وی + پچھلا + ٹریلر + حادثے کی معلومات دیکھیں + باہر نکلنے پر ایپ کو اپ ڈیٹ کر دیا جائے گا + اس ذخیرہ سے تمام پلگ ان ڈاؤن لوڈ کریں؟ + قسط %d ریلیز ہو گئی! + ایپ کا نیا ورژن انسٹال نہیں ہو سکا + تجویز کردہ + دوبارہ شروع کریں + لوڈ %s + انٹرنیٹ سے لوڈ کریں + ڈاؤن لوڈڈ فائل + پس منظر + ذریعہ + کیم + کیم + کیم + ایچ ڈی + ٹی ایس + ٹی سی + ڈبلیو پی + 4کے + SDR + پلیئر + عنوان + قرارداد + غلط ڈیٹا + خرابی + سب ٹائٹلز سے بلوٹ کو ہٹا دیں + ترجیحی میڈیا زبان کے لحاظ سے فلٹر کریں + حوالہ دینے والا + ان زبانوں میں ویڈیوز دیکھیں + سیٹ اپ کو چھوڑ دیں + کریش رپورٹنگ + ہو گیا + ایکسٹینشنز + ذخیرہ URL + پلگ ان لوڈ ہو گیا + %s کو لوڈ نہیں کیا جا سکا + ڈاؤن لوڈ ہونا شروع ہو گیا %d %s… + ڈاؤن لوڈ کیا گیا %d %s + سبھی %s پہلے ہی ڈاؤن لوڈ ہو چکے ہیں + رابطہ بحال کرو + پلگ ان + ذخیرہ کو حذف کریں + ان سائٹس کی فہرست ڈاؤن لوڈ کریں جنہیں آپ استعمال کرنا چاہتے ہیں + ڈاؤن لوڈڈ: %d + غیر فعال: %d + اپ ڈیٹ کردہ %d پلگ ان + تمام سب ٹائٹلز کو بڑے کریں + %s (غیر فعال) + ٹریکس + آڈیو ٹریکس + ری اسٹارٹ پر اپلائی کریں + سیف موڈ آن + درجہ بندی: %s + تفصیل + ورژن + حالت + سائز + زبان + وی ایل سی + ترجیحی ویڈیو پلیئر + اندرونی پلیئر + ایم پی وی + ویب براؤزر + ایپ نہیں ملی + Recap + مخلوط اختتام + مخلوط افتتاحی + کھولنے/ختم کرنے کے لیے سکپ پاپ اپ دکھائیں + دیکھا گیا کے طور پر نشان زد کریں + نہیں + ایپ اپ ڈیٹ ڈاؤن لوڈ ہو رہا ہے… + میراث + اپ ڈیٹ شروع ہو گیا + پلگ ان ڈاؤن لوڈ ہو گیا + دیکھے گئے سے ہٹا دیں + پیکیج انسٹالر + درجہ بندی (اعلی سے کم) + درجہ بندی (کم سے زیادہ) + اپ ڈیٹ کیا گیا (پرانا سے نیا) + حروف تہجی (A سے Z) + حروف تہجی (Z سے A) + لائبریری کو منتخب کریں + پلیئر دکھایا گیا - مقدار تلاش کریں + پلیئر کے نظر آنے پر استعمال کی جانے والی مقدار + پلیئر کے چھپنے پر استعمال ہونے والی تلاش کی مقدار + سیف موڈ فائل مل گئی! +\nفائل کو ہٹانے تک اسٹارٹ اپ پر کوئی ایکسٹینشن لوڈ نہیں کرنا۔ + شروع کریں + ناکام + کامیاب ہو گیا + فراہم کنندہ ٹیسٹ + رک جاؤ + سبسکرائب شدہ شوز کو اپ ڈیٹ کرنا + دیکھنے کا ترجیحی معیار (موبائل ڈیٹا) + raw.githubusercontent.com پراکسی + آئی ایس پی بائی پاسز + %s کو سبسکرائب کیا + jsDelivr کا استعمال کرتے ہوئے GitHub کو بلاک کرنے کو نظرانداز کرتا ہے۔ اپ ڈیٹس میں کچھ دنوں کی تاخیر ہو سکتی ہے۔ + آپ کی لائبریری خالی ہے:( +\nلائبریری اکاؤنٹ میں لاگ ان کریں یا اپنی مقامی لائبریری میں شوز شامل کریں۔ + غلط URL + براؤزر + ویب + مددگار + ایچ کیو + حمایت کی + ہاں + سبسکرائب کیا ہوا + تازہ کاری (نئے سے پرانے) + ڈی وی ڈی + ماضی مٹا دو + تعارف + تاریخ + کے ساتھ کھولیں + غلط ID + مخزن کا نام + ختم ہونے والا + کھل رہا + کیا آپ کو یقین ہے کہ آپ ہیاں سے نکلنا چاہتے ہیں؟ + ایس ڈی + تمام زبانیں + پلیئر پوشیدہ - مقدار تلاش کریں + لائبریری + بلو رے + واپس لوٹنا + ذخیرہ شامل کریں + آپ کیا دیکھنا چاہتے ہیں + ‪کمیونٹی ریپوزٹریز دیکھیں + ڈاؤن لوڈ نہیں ہوا: %d + عوامی فہرست + فائل سے لوڈ کریں + ویڈیو ٹریکس + HLS پلے لسٹ + پوسٹر کی تصویر + UHD + %s کو چھوڑ دیں + بے ترتیب + ترتیب دیں + مصنفین + اس سے تمام ریپوزٹری پلگ ان بھی حذف ہو جائیں گے + پلگ ان کو حذف کر دیا گیا + بیچ ڈاؤن لوڈ + GitHub تک نہیں پہنچ سکا۔ jsDelivr پراکسی کو آن کیا جا رہا ہے… + تیز بھوری لومڑی سست کتے پر چھلانگ لگاتی ہے + جلد آرہا ہے… + سب ٹائٹلز سے بند کیپشنز کو ہٹا دیں + اپنے آلے کے مطابق ایپ کی شکل تبدیل کریں + اگلے + CloudStream میں بذریعہ ڈیفالٹ کوئی سائٹ انسٹال نہیں ہے۔ آپ کو ریپوزٹری سے سائٹس انسٹال کرنے کی ضرورت ہے۔ +\nSky UK Limited 🤮 کی طرف سے بے دماغ DMCA ہٹانے کی وجہ سے ہم ایپ میں ریپوزٹری سائٹ کو لنک نہیں کر سکتے۔ +\nہمارے ڈسکارڈ میں شامل ہوں یا آن لائن تلاش کریں۔ + تمام ایکسٹینشنز کریش کی وجہ سے آف کر دی گئیں تاکہ آپ کو پریشانی کا باعث تلاش کرنے میں مدد مل سکے۔ + پہلے ایکسٹینشن انسٹال کریں + بہت زیادہ متن۔ کلپ بورڈ میں محفوظ کرنے سے قاصر۔ + ایپ اپ ڈیٹ انسٹال ہو رہا ہے… + یہ فہرست خالی ہے۔ کسی اور پر سوئچ کرنے کی کوشش کریں۔ + \ No newline at end of file From 9237817bd3ab3c48548e6ee69152cdb8c35ac8a6 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 06:36:01 +0000 Subject: [PATCH 006/156] chore(locales): fix locale issues --- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-bn/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-iw/strings.xml | 2 +- app/src/main/res/values-ms/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-nn/strings.xml | 2 +- app/src/main/res/values-no/strings.xml | 2 +- app/src/main/res/values-or/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-ur/strings.xml | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 6b722e43..c423b239 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -578,4 +578,4 @@ \nملاحظة: إذا كان المجموع 10 أو أكثر ، فسيتخطى المشغل التحميل تلقائيًا عند تحميل هذا الرابط! النوعيات خلفية الملف الشخصي - \ No newline at end of file + diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index cc272a3a..ba754fa5 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -149,4 +149,4 @@ আইজেনগ্রাভি মোড আপডেট শুরু হয়েছে ব্রাউজার - \ No newline at end of file + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 5cbdb7db..cc9f0ad4 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -570,4 +570,4 @@ \nBudou mít celkovou prioritu videa 10. \n \nPOZNÁMKA: Pokud je součet 10 nebo vyšší, přehrávač automaticky přeskočí načítání při načtení daného odkazu! - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 69a850b3..45a6a66c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -546,4 +546,4 @@ \nWerden eine kombinierte Videopriorität von 10 haben. \n \nHINWEIS: Wenn die Summe 10 oder mehr beträgt, überspringt der Player automatisch das Laden, wenn der Link geladen wird! - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 4326bccd..bc830084 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -546,4 +546,4 @@ Usar Calidades Perfil del fondo - \ No newline at end of file + diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index e519d062..722352a6 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -569,4 +569,4 @@ Profil Kualitas Latar belakang profil - \ No newline at end of file + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 9a90b6e9..431b2a8c 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -568,4 +568,4 @@ Dati Mobili Qualità Sfondo profilo - \ No newline at end of file + diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index e59cdd66..aaa65897 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -506,4 +506,4 @@ אלפביתי (ת\' עד א\') פתח עם נראה שהרשימה הזו ריקה, נסו לעבור לרשימה אחרת - \ No newline at end of file + diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 7fd5504e..17eeb883 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -37,4 +37,4 @@ Kelajuan (%.2fx) Poster Poster - \ No newline at end of file + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 9054eada..dff95be7 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -568,4 +568,4 @@ \n \nNOTITIE: Als de som 10 of hoger is zal de speler automatisch het laden overslaan wanneer die link is geladen! Profiel %d - \ No newline at end of file + diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 135b7272..592ff22c 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -196,4 +196,4 @@ Bilde i bilde Fortsett å sjå Prøv tilkopling på nytt… - \ No newline at end of file + diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 65b5a462..dac15d61 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -533,4 +533,4 @@ \nLaster ikke inn noen utvidelser ved oppstart til filen er fjernet. Biblioteket ditt er tomt :( \nLogg inn på en bibliotekkonto eller legg til programmer i ditt lokale bibliotek. - \ No newline at end of file + diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 19b071e4..9b9385c2 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -153,4 +153,4 @@ ଆପ୍ ଅଦ୍ୟତନ ଦେଖାଇବା ଅଦ୍ୟତନ ଆରମ୍ଭ ହୋଇଛି ସନ୍ଧାନ କରିବା… - \ No newline at end of file + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d08a760d..7d4bf3e3 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -549,4 +549,4 @@ Pomoc Jakości Tło profilu - \ No newline at end of file + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index d91e79f8..773598bf 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -546,4 +546,4 @@ \nTerá uma prioridade de vídeo combinada de 10. \n \nNOTA: Se a soma for 10 ou mais, o leitor saltará automaticamente o carregamento quando essa ligação for carregada! - \ No newline at end of file + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 372552bc..eef9bc95 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -594,4 +594,4 @@ \nNOT: Toplam 10 veya daha fazlaysa, bu bağlantı yüklendiğinde oynatıcı otomatik olarak yüklemeyi atlayacaktır! Kaliteler Profil arkaplanı - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 09fd40d5..0fad744c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -546,4 +546,4 @@ Редагувати Якості Фон профілю - \ No newline at end of file + diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 4c9091dd..48b73efa 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -527,4 +527,4 @@ بہت زیادہ متن۔ کلپ بورڈ میں محفوظ کرنے سے قاصر۔ ایپ اپ ڈیٹ انسٹال ہو رہا ہے… یہ فہرست خالی ہے۔ کسی اور پر سوئچ کرنے کی کوشش کریں۔ - \ No newline at end of file + From 927453d9fe0bb370ae811ea6df10f385eb0862b6 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Wed, 12 Jul 2023 23:15:25 +0700 Subject: [PATCH 007/156] Extractor: added Moviesapi and fix some extractors (#504) * Extractor: added Pixeldrain, Wibufile and fix some extractors * Extractor: added Moviesapi and fix some extractors --------- Co-authored-by: Sofie99 --- .../cloudstream3/extractors/Chillx.kt | 9 ++++-- .../cloudstream3/extractors/Filesim.kt | 32 +++++++------------ .../cloudstream3/utils/ExtractorApi.kt | 1 + 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt index 1c548e74..b4f3d897 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -12,6 +12,11 @@ import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.PBEKeySpec import javax.crypto.spec.SecretKeySpec +class Moviesapi : Chillx() { + override val name = "Moviesapi" + override val mainUrl = "https://w1.moviesapi.club" +} + class Bestx : Chillx() { override val name = "Bestx" override val mainUrl = "https://bestx.stream" @@ -27,7 +32,7 @@ open class Chillx : ExtractorApi() { override val requiresReferer = true companion object { - private const val KEY = "4VqE3#N7zt&HEP^a" + private const val KEY = "11x&W5UBrcqn\$9Yl" } override suspend fun getUrl( @@ -45,7 +50,7 @@ open class Chillx : ExtractorApi() { val encData = AppUtils.tryParseJson(base64Decode(master ?: return)) val decrypt = cryptoAESHandler(encData ?: return, KEY, false) - val source = Regex("""sources:\s*\[\{"file":"([^"]+)""").find(decrypt)?.groupValues?.get(1) + val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1) // required diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt index be0efd0c..a1148bb8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt @@ -65,27 +65,19 @@ open class Filesim : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val response = app.get(url, referer = referer).document - response.select("script[type=text/javascript]").map { script -> - if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) { - val unpackedscript = getAndUnpack(script.data()) - val m3u8Regex = Regex("file.\"(.*?m3u8.*?)\"") - val m3u8 = m3u8Regex.find(unpackedscript)?.destructured?.component1() ?: "" - if (m3u8.isNotEmpty()) { - generateM3u8( - name, - m3u8, - mainUrl - ).forEach(callback) - } - } + val response = app.get(url, referer = referer) + val script = if (!getPacked(response.text).isNullOrEmpty()) { + getAndUnpack(response.text) + } else { + response.document.selectFirst("script:containsData(sources:)")?.data() } + val m3u8 = + Regex("file:\\s*\"(.*?m3u8.*?)\"").find(script ?: return)?.groupValues?.getOrNull(1) + generateM3u8( + name, + m3u8 ?: return, + mainUrl + ).forEach(callback) } - /* private data class ResponseSource( - @JsonProperty("file") val file: String, - @JsonProperty("type") val type: String?, - @JsonProperty("label") val label: String? - ) */ - } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index f0c1ea3b..f6f76fe7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -357,6 +357,7 @@ val extractorApis: MutableList = arrayListOf( DesuDrive(), Chillx(), + Moviesapi(), Watchx(), Bestx(), Keephealth(), From 05a0d3cd817c5484a44acd9bb6e8199e52ced387 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 13 Jul 2023 23:18:37 +0200 Subject: [PATCH 008/156] migrated some items to viewbindings + removed Some --- app/build.gradle.kts | 5 + .../cloudstream3/mvvm/ArchComponentExt.kt | 26 -- .../ui/download/DownloadChildFragment.kt | 44 ++- .../ui/download/DownloadFragment.kt | 98 +++--- .../cloudstream3/ui/home/HomeFragment.kt | 325 ++++++++++-------- .../ui/home/HomeParentItemAdapterPreview.kt | 4 +- .../ui/library/LibraryFragment.kt | 198 ++++++----- .../ui/library/LibraryViewModel.kt | 1 - .../cloudstream3/ui/library/PageAdapter.kt | 30 +- .../ui/library/ViewpagerAdapter.kt | 70 ++-- .../ui/quicksearch/QuickSearchFragment.kt | 65 ++-- .../cloudstream3/ui/result/ActorAdaptor.kt | 77 ++--- .../cloudstream3/ui/result/EpisodeAdapter.kt | 296 ++++++++++------ .../cloudstream3/ui/result/ImageAdapter.kt | 14 +- .../cloudstream3/ui/result/ResultFragment.kt | 133 +++---- .../ui/result/ResultFragmentPhone.kt | 106 +++--- .../ui/result/ResultFragmentTv.kt | 77 ++--- .../ui/result/ResultViewModel2.kt | 194 ++++++----- .../cloudstream3/ui/result/SelectAdaptor.kt | 13 +- .../cloudstream3/ui/result/UiText.kt | 9 - .../cloudstream3/ui/search/SearchAdaptor.kt | 2 +- .../cloudstream3/ui/search/SearchFragment.kt | 128 ++++--- .../ui/search/SearchHistoryAdaptor.kt | 30 +- .../ui/search/SearchResultBuilder.kt | 20 +- .../ui/settings/AccountAdapter.kt | 22 +- .../ui/settings/SettingsAccount.kt | 2 +- .../ui/settings/SettingsFragment.kt | 4 +- .../settings/extensions/ExtensionsFragment.kt | 42 ++- .../extensions/ExtensionsViewModel.kt | 7 +- .../ui/settings/extensions/PluginsFragment.kt | 74 ++-- .../ui/setup/SetupFragmentExtensions.kt | 79 +++-- .../ui/setup/SetupFragmentLanguage.kt | 53 +-- .../ui/setup/SetupFragmentLayout.kt | 101 +++--- .../ui/setup/SetupFragmentMedia.kt | 89 +++-- .../ui/setup/SetupFragmentProviderLanguage.kt | 50 ++- .../subtitles/ChromecastSubtitlesFragment.kt | 90 +++-- .../ui/subtitles/SubtitlesFragment.kt | 2 +- .../lagradost/cloudstream3/utils/UIHelper.kt | 13 +- app/src/main/res/layout/fragment_home_tv.xml | 11 + app/src/main/res/layout/fragment_plugins.xml | 2 +- app/src/main/res/layout/fragment_search.xml | 2 +- .../main/res/layout/fragment_search_tv.xml | 2 +- .../main/res/layout/home_select_mainpage.xml | 2 +- .../res/layout/library_viewpager_page.xml | 2 - .../main/res/layout/result_episode_large.xml | 213 ++++++------ .../main/res/layout/tvtypes_chips_scroll.xml | 2 +- 46 files changed, 1565 insertions(+), 1264 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ebde6187..86d91147 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,6 +28,11 @@ android { testOptions { unitTests.isReturnDefaultValues = true } + + viewBinding { + enable = true + } + signingConfigs { create("prerelease") { if (prereleaseStoreFile != null) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index bb15bc85..28b552d1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -57,32 +57,6 @@ fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> liveData.observe(this) { action(it) } } -inline fun some(value: T?): Some { - return if (value == null) { - Some.None - } else { - Some.Success(value) - } -} - -sealed class Some { - data class Success(val value: T) : Some() - object None : Some() - - override fun toString(): String { - return when (this) { - is None -> "None" - is Success -> "Some(${value.toString()})" - } - } -} - -sealed class ResourceSome { - data class Success(val value: T) : ResourceSome() - object None : ResourceSome() - data class Loading(val data: Any? = null) : ResourceSome() -} - sealed class Resource { data class Success(val value: T) : Resource() data class Failure( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index 477a18e0..0cef49b1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey @@ -15,13 +16,12 @@ import com.lagradost.cloudstream3.utils.DataStore.getKeys import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager -import kotlinx.android.synthetic.main.fragment_child_downloads.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class DownloadChildFragment : Fragment() { companion object { - fun newInstance(headerName: String, folder: String) : Bundle { + fun newInstance(headerName: String, folder: String): Bundle { return Bundle().apply { putString("folder", folder) putString("name", headerName) @@ -30,13 +30,21 @@ class DownloadChildFragment : Fragment() { } override fun onDestroyView() { - (download_child_list?.adapter as DownloadChildAdapter?)?.killAdapter() + (binding?.downloadChildList?.adapter as DownloadChildAdapter?)?.killAdapter() downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it } + binding = null super.onDestroyView() } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_child_downloads, container, false) + var binding: FragmentChildDownloadsBinding? = null + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root//inflater.inflate(R.layout.fragment_child_downloads, container, false) } private fun updateList(folder: String) = main { @@ -50,14 +58,15 @@ class DownloadChildFragment : Fragment() { ?: return@mapNotNull null VisualDownloadChildCached(info.fileLength, info.totalBytes, it) } - }.sortedBy { it.data.episode + (it.data.season?: 0)*100000 } + }.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 } if (eps.isEmpty()) { activity?.onBackPressed() return@main } - (download_child_list?.adapter as DownloadChildAdapter? ?: return@main).cardList = eps - download_child_list?.adapter?.notifyDataSetChanged() + (binding?.downloadChildList?.adapter as DownloadChildAdapter? ?: return@main).cardList = + eps + binding?.downloadChildList?.adapter?.notifyDataSetChanged() } } @@ -72,14 +81,17 @@ class DownloadChildFragment : Fragment() { activity?.onBackPressed() // TODO FIX return } - context?.fixPaddingStatusbar(download_child_root) + fixPaddingStatusbar(binding?.downloadChildRoot) - download_child_toolbar.title = name - download_child_toolbar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - download_child_toolbar.setNavigationOnClickListener { - activity?.onBackPressed() + binding?.downloadChildToolbar?.apply { + title = name + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + setNavigationOnClickListener { + activity?.onBackPressed() + } } + val adapter: RecyclerView.Adapter = DownloadChildAdapter( ArrayList(), @@ -88,7 +100,7 @@ class DownloadChildFragment : Fragment() { } downloadDeleteEventListener = { id: Int -> - val list = (download_child_list?.adapter as DownloadChildAdapter?)?.cardList + val list = (binding?.downloadChildList?.adapter as DownloadChildAdapter?)?.cardList if (list != null) { if (list.any { it.data.id == id }) { updateList(folder) @@ -98,8 +110,8 @@ class DownloadChildFragment : Fragment() { downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } - download_child_list.adapter = adapter - download_child_list.layoutManager = GridLayoutManager(context, 1) + binding?.downloadChildList?.adapter = adapter + binding?.downloadChildList?.layoutManager = GridLayoutManager(context, 1) updateList(folder) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index e80a8fa5..629ab11a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -34,10 +34,10 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager -import kotlinx.android.synthetic.main.fragment_downloads.* -import kotlinx.android.synthetic.main.stream_input.* 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.settings.SettingsFragment.Companion.isTrueTvSettings @@ -60,8 +60,8 @@ class DownloadFragment : Fragment() { private fun setList(list: List) { main { - (download_list?.adapter as DownloadHeaderAdapter?)?.cardList = list - download_list?.adapter?.notifyDataSetChanged() + (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList = list + binding?.downloadList?.adapter?.notifyDataSetChanged() } } @@ -70,10 +70,13 @@ class DownloadFragment : Fragment() { VideoDownloadManager.downloadDeleteEvent -= downloadDeleteEventListener!! downloadDeleteEventListener = null } - (download_list?.adapter as DownloadHeaderAdapter?)?.killAdapter() + (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.killAdapter() + binding = null super.onDestroyView() } + var binding : FragmentDownloadsBinding? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -82,7 +85,9 @@ class DownloadFragment : Fragment() { downloadsViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] - return inflater.inflate(R.layout.fragment_downloads, container, false) + val localBinding = FragmentDownloadsBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root//inflater.inflate(R.layout.fragment_downloads, container, false) } private var downloadDeleteEventListener: ((Int) -> Unit)? = null @@ -92,36 +97,40 @@ class DownloadFragment : Fragment() { hideKeyboard() observe(downloadsViewModel.noDownloadsText) { - text_no_downloads.text = it + binding?.textNoDownloads?.text = it } observe(downloadsViewModel.headerCards) { setList(it) - download_loading.isVisible = false + binding?.downloadLoading?.isVisible = false } observe(downloadsViewModel.availableBytes) { - download_free_txt?.text = + binding?.downloadFreeTxt?.text = getString(R.string.storage_size_format).format( getString(R.string.free_storage), formatShortFileSize(view.context, it) ) - download_free?.setLayoutWidth(it) + binding?.downloadFree?.setLayoutWidth(it) } observe(downloadsViewModel.usedBytes) { - download_used_txt?.text = - getString(R.string.storage_size_format).format( - getString(R.string.used_storage), - formatShortFileSize(view.context, it) - ) - download_used?.setLayoutWidth(it) - download_storage_appbar?.isVisible = it > 0 + binding?.apply { + downloadUsedTxt.text = + getString(R.string.storage_size_format).format( + getString(R.string.used_storage), + formatShortFileSize(view.context, it) + ) + downloadUsed.setLayoutWidth(it) + downloadStorageAppbar.isVisible = it > 0 + } } observe(downloadsViewModel.downloadBytes) { - download_app_txt?.text = - getString(R.string.storage_size_format).format( - getString(R.string.app_storage), - formatShortFileSize(view.context, it) - ) - download_app?.setLayoutWidth(it) + 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 = @@ -164,7 +173,7 @@ class DownloadFragment : Fragment() { ) downloadDeleteEventListener = { id -> - val list = (download_list?.adapter as DownloadHeaderAdapter?)?.cardList + val list = (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList if (list != null) { if (list.any { it.data.id == id }) { context?.let { ctx -> @@ -177,31 +186,36 @@ class DownloadFragment : Fragment() { downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } - download_list?.adapter = adapter - download_list?.layoutManager = GridLayoutManager(context, 1) + binding?.downloadList?.apply { + this.adapter = adapter + layoutManager = GridLayoutManager(context, 1) + } // Should be visible in emulator layout - download_stream_button?.isGone = isTrueTvSettings() - download_stream_button?.setOnClickListener { + binding?.downloadStreamButton?.isGone = isTrueTvSettings() + binding?.downloadStreamButton?.setOnClickListener { val dialog = Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom) - dialog.setContentView(R.layout.stream_input) + + val binding = StreamInputBinding.inflate(dialog.layoutInflater) + + dialog.setContentView(binding.root) dialog.show() // If user has clicked the switch do not interfere var preventAutoSwitching = false - dialog.hls_switch?.setOnClickListener { + binding.hlsSwitch.setOnClickListener { preventAutoSwitching = true } fun activateSwitchOnHls(text: String?) { - dialog.hls_switch?.isChecked = normalSafeApiCall { + binding.hlsSwitch.isChecked = normalSafeApiCall { URI(text).path?.substringAfterLast(".")?.contains("m3u") } == true } - dialog.stream_referer?.doOnTextChanged { text, _, _, _ -> + binding.streamReferer.doOnTextChanged { text, _, _, _ -> if (!preventAutoSwitching) activateSwitchOnHls(text?.toString()) } @@ -210,16 +224,16 @@ class DownloadFragment : Fragment() { 0 )?.text?.toString()?.let { copy -> val fixedText = copy.trim() - dialog.stream_url?.setText(fixedText) + binding.streamUrl.setText(fixedText) activateSwitchOnHls(fixedText) } - dialog.apply_btt?.setOnClickListener { - val url = dialog.stream_url.text?.toString() + binding.applyBtt.setOnClickListener { + val url = binding.streamUrl.text?.toString() if (url.isNullOrEmpty()) { showToast(activity, R.string.error_invalid_url, Toast.LENGTH_SHORT) } else { - val referer = dialog.stream_referer.text?.toString() + val referer = binding.streamReferer.text?.toString() activity?.navigate( R.id.global_to_navigation_player, @@ -228,7 +242,7 @@ class DownloadFragment : Fragment() { listOf(BasicLink(url)), extract = true, referer = referer, - isM3u8 = dialog.hls_switch?.isChecked + isM3u8 = binding.hlsSwitch.isChecked ) ) ) @@ -237,22 +251,22 @@ class DownloadFragment : Fragment() { } } - dialog.cancel_btt?.setOnClickListener { + binding.cancelBtt.setOnClickListener { dialog.dismissSafe(activity) } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - download_list?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> + binding?.downloadList?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> val dy = scrollY - oldScrollY if (dy > 0) { //check for scroll down - download_stream_button?.shrink() // hide + binding?.downloadStreamButton?.shrink() // hide } else if (dy < -5) { - download_stream_button?.extend() // show + binding?.downloadStreamButton?.extend() // show } } } downloadsViewModel.updateList(requireContext()) - context?.fixPaddingStatusbar(download_root) + fixPaddingStatusbar(binding?.downloadRoot) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 5cf6fc8e..99ce7c3b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -34,12 +34,15 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent import com.lagradost.cloudstream3.MainActivity.Companion.bookmarksUpdatedEvent import com.lagradost.cloudstream3.MainActivity.Companion.mainPluginsLoadedEvent +import com.lagradost.cloudstream3.databinding.FragmentHomeBinding +import com.lagradost.cloudstream3.databinding.HomeEpisodesExpandedBinding +import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding +import com.lagradost.cloudstream3.databinding.TvtypesChipsBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi -import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.search.* @@ -64,24 +67,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API -import kotlinx.android.synthetic.main.activity_main_tv.* -import kotlinx.android.synthetic.main.fragment_home.* -import kotlinx.android.synthetic.main.fragment_home.home_api_fab -import kotlinx.android.synthetic.main.fragment_home.home_change_api_loading -import kotlinx.android.synthetic.main.fragment_home.home_loading -import kotlinx.android.synthetic.main.fragment_home.home_loading_error -import kotlinx.android.synthetic.main.fragment_home.home_loading_shimmer -import kotlinx.android.synthetic.main.fragment_home.home_loading_statusbar -import kotlinx.android.synthetic.main.fragment_home.home_master_recycler -import kotlinx.android.synthetic.main.fragment_home.home_reload_connection_open_in_browser -import kotlinx.android.synthetic.main.fragment_home.home_reload_connectionerror -import kotlinx.android.synthetic.main.fragment_home.result_error_text -import kotlinx.android.synthetic.main.fragment_home_tv.* -import kotlinx.android.synthetic.main.fragment_result.* -import kotlinx.android.synthetic.main.fragment_search.* -import kotlinx.android.synthetic.main.home_episodes_expanded.* -import kotlinx.android.synthetic.main.tvtypes_chips.* -import kotlinx.android.synthetic.main.tvtypes_chips.view.* + import java.util.* @@ -125,22 +111,26 @@ class HomeFragment : Fragment() { expand: HomeViewModel.ExpandableHomepageList, deleteCallback: (() -> Unit)? = null, expandCallback: (suspend (String) -> HomeViewModel.ExpandableHomepageList?)? = null, - dismissCallback : (() -> Unit), + dismissCallback: (() -> Unit), ): BottomSheetDialog { val context = this val bottomSheetDialogBuilder = BottomSheetDialog(context) - - bottomSheetDialogBuilder.setContentView(R.layout.home_episodes_expanded) - val title = bottomSheetDialogBuilder.findViewById(R.id.home_expanded_text)!! + val binding: HomeEpisodesExpandedBinding = HomeEpisodesExpandedBinding.inflate( + bottomSheetDialogBuilder.layoutInflater, + null, + false + ) + bottomSheetDialogBuilder.setContentView(binding.root) + //val title = bottomSheetDialogBuilder.findViewById(R.id.home_expanded_text)!! //title.findViewTreeLifecycleOwner().lifecycle.addObserver() val item = expand.list - title.text = item.name - val recycle = - bottomSheetDialogBuilder.findViewById(R.id.home_expanded_recycler)!! - val titleHolder = - bottomSheetDialogBuilder.findViewById(R.id.home_expanded_drag_down)!! + binding.homeExpandedText.text = item.name + // val recycle = + // bottomSheetDialogBuilder.findViewById(R.id.home_expanded_recycler)!! + //val titleHolder = + // bottomSheetDialogBuilder.findViewById(R.id.home_expanded_drag_down)!! // main { //(bottomSheetDialogBuilder.ownerActivity as androidx.fragment.app.FragmentActivity?)?.supportFragmentManager?.fragments?.lastOrNull()?.viewLifecycleOwner?.apply { @@ -159,10 +149,10 @@ class HomeFragment : Fragment() { // }) //} // } - val delete = bottomSheetDialogBuilder.home_expanded_delete - delete.isGone = deleteCallback == null + //val delete = bottomSheetDialogBuilder.home_expanded_delete + binding.homeExpandedDelete.isGone = deleteCallback == null if (deleteCallback != null) { - delete.setOnClickListener { + binding.homeExpandedDelete.setOnClickListener { try { val builder: AlertDialog.Builder = AlertDialog.Builder(context) val dialogClickListener = @@ -172,6 +162,7 @@ class HomeFragment : Fragment() { deleteCallback.invoke() bottomSheetDialogBuilder.dismissSafe(this) } + DialogInterface.BUTTON_NEGATIVE -> {} } } @@ -191,26 +182,27 @@ class HomeFragment : Fragment() { } } } - - titleHolder.setOnClickListener { + binding.homeExpandedDragDown.setOnClickListener { bottomSheetDialogBuilder.dismissSafe(this) } // Span settings - recycle.spanCount = currentSpan + binding.homeExpandedRecycler.spanCount = currentSpan - recycle.adapter = SearchAdapter(item.list.toMutableList(), recycle) { callback -> - handleSearchClickCallback(this, callback) - if (callback.action == SEARCH_ACTION_LOAD || callback.action == SEARCH_ACTION_PLAY_FILE) { - bottomSheetDialogBuilder.ownHide() // we hide here because we want to resume it later - //bottomSheetDialogBuilder.dismissSafe(this) + binding.homeExpandedRecycler.adapter = + SearchAdapter(item.list.toMutableList(), binding.homeExpandedRecycler) { callback -> + handleSearchClickCallback(this, callback) + if (callback.action == SEARCH_ACTION_LOAD || callback.action == SEARCH_ACTION_PLAY_FILE) { + bottomSheetDialogBuilder.ownHide() // we hide here because we want to resume it later + //bottomSheetDialogBuilder.dismissSafe(this) + } + }.apply { + hasNext = expand.hasNext } - }.apply { - hasNext = expand.hasNext - } - recycle.addOnScrollListener(object : RecyclerView.OnScrollListener() { + binding.homeExpandedRecycler.addOnScrollListener(object : + RecyclerView.OnScrollListener() { var expandCount = 0 val name = expand.list.name @@ -238,7 +230,7 @@ class HomeFragment : Fragment() { }) val spanListener = { span: Int -> - recycle.spanCount = span + binding.homeExpandedRecycler.spanCount = span //(recycle.adapter as SearchAdapter).notifyDataSetChanged() } @@ -280,19 +272,19 @@ class HomeFragment : Fragment() { ) } - private fun getPairList(header: ChipGroup) = getPairList( - header.home_select_anime, - header.home_select_cartoons, - header.home_select_tv_series, - header.home_select_documentaries, - header.home_select_movies, - header.home_select_asian, - header.home_select_livestreams, - header.home_select_nsfw, - header.home_select_others + private fun getPairList(header: TvtypesChipsBinding) = getPairList( + header.homeSelectAnime, + header.homeSelectCartoons, + header.homeSelectTvSeries, + header.homeSelectDocumentaries, + header.homeSelectMovies, + header.homeSelectAsian, + header.homeSelectLivestreams, + header.homeSelectNsfw, + header.homeSelectOthers ) - fun validateChips(header: ChipGroup?, validTypes: List) { + fun validateChips(header: TvtypesChipsBinding?, validTypes: List) { if (header == null) return val pairList = getPairList(header) for ((button, types) in pairList) { @@ -301,7 +293,7 @@ class HomeFragment : Fragment() { } } - fun updateChips(header: ChipGroup?, selectedTypes: List) { + fun updateChips(header: TvtypesChipsBinding?, selectedTypes: List) { if (header == null) return val pairList = getPairList(header) for ((button, types) in pairList) { @@ -311,7 +303,7 @@ class HomeFragment : Fragment() { } fun bindChips( - header: ChipGroup?, + header: TvtypesChipsBinding?, selectedTypes: List, validTypes: List, callback: (List) -> Unit @@ -344,7 +336,13 @@ class HomeFragment : Fragment() { BottomSheetDialog(this) builder.behavior.state = BottomSheetBehavior.STATE_EXPANDED - builder.setContentView(R.layout.home_select_mainpage) + val binding: HomeSelectMainpageBinding = HomeSelectMainpageBinding.inflate( + builder.layoutInflater, + null, + false + ) + + builder.setContentView(binding.root) builder.show() builder.let { dialog -> val isMultiLang = getApiProviderLangSettings().let { set -> @@ -408,7 +406,7 @@ class HomeFragment : Fragment() { } bindChips( - dialog.home_select_group, + binding.tvtypesChipsScroll.tvtypesChips, preSelectedTypes, validAPIs.flatMap { it.supportedTypes }.distinct() ) { list -> @@ -423,6 +421,9 @@ class HomeFragment : Fragment() { private val homeViewModel: HomeViewModel by activityViewModels() + var binding: FragmentHomeBinding? = null + + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -433,11 +434,24 @@ class HomeFragment : Fragment() { bottomSheetDialog?.ownShow() val layout = if (isTvSettings()) R.layout.fragment_home_tv else R.layout.fragment_home - return inflater.inflate(layout, container, false) + + /* val binding = FragmentHomeTvBinding.inflate(layout, container, false) + binding.homeLoadingError + + val binding2 = FragmentHomeBinding.inflate(layout, container, false) + binding2.homeLoadingError*/ + val root = inflater.inflate(layout, container, false) + binding = FragmentHomeBinding.bind(root) + //val localBinding = FragmentHomeBinding.inflate(inflater) + //binding = localBinding + return root + + //return inflater.inflate(layout, container, false) } override fun onDestroyView() { bottomSheetDialog?.ownHide() + binding = null super.onDestroyView() } @@ -467,7 +481,7 @@ class HomeFragment : Fragment() { fixGrid() } - fun bookmarksUpdated(_data : Boolean) { + fun bookmarksUpdated(_data: Boolean) { reloadStored() } @@ -525,14 +539,18 @@ class HomeFragment : Fragment() { private var bottomSheetDialog: BottomSheetDialog? = null + @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) fixGrid() - home_change_api_loading?.setOnClickListener(apiChangeClickListener) - home_api_fab?.setOnClickListener(apiChangeClickListener) - home_random?.setOnClickListener { + binding?.homeChangeApiLoading?.setOnClickListener(apiChangeClickListener) + + + binding?.homeChangeApiLoading?.setOnClickListener(apiChangeClickListener) + binding?.homeApiFab?.setOnClickListener(apiChangeClickListener) + binding?.homeRandom?.setOnClickListener { if (listHomepageItems.isNotEmpty()) { activity.loadSearchResult(listHomepageItems.random()) } @@ -542,91 +560,100 @@ class HomeFragment : Fragment() { context?.let { val settingsManager = PreferenceManager.getDefaultSharedPreferences(it) toggleRandomButton = - settingsManager.getBoolean(getString(R.string.random_button_key), false) - home_random?.visibility = View.GONE + settingsManager.getBoolean( + getString(R.string.random_button_key), + false + ) && !isTvSettings() + binding?.homeRandom?.visibility = View.GONE } observe(homeViewModel.preview) { preview -> - (home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setPreviewData( + (binding?.homeMasterRecycler?.adapter as? HomeParentItemAdapterPreview?)?.setPreviewData( preview ) } observe(homeViewModel.apiName) { apiName -> currentApiName = apiName - home_api_fab?.text = apiName - (home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setApiName( + binding?.homeApiFab?.text = apiName + (binding?.homeMasterRecycler?.adapter as? HomeParentItemAdapterPreview?)?.setApiName( apiName ) } observe(homeViewModel.page) { data -> - when (data) { - is Resource.Success -> { - home_loading_shimmer?.stopShimmer() + binding?.apply { + when (data) { + is Resource.Success -> { + homeLoadingShimmer.stopShimmer() - val d = data.value - val mutableListOfResponse = mutableListOf() - listHomepageItems.clear() + val d = data.value + val mutableListOfResponse = mutableListOf() + listHomepageItems.clear() - (home_master_recycler?.adapter as? ParentItemAdapter)?.updateList( - d.values.toMutableList(), - home_master_recycler - ) + (homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList( + d.values.toMutableList(), + homeMasterRecycler + ) - home_loading?.isVisible = false - home_loading_error?.isVisible = false - home_master_recycler?.isVisible = true - //home_loaded?.isVisible = true - if (toggleRandomButton) { - //Flatten list - d.values.forEach { dlist -> - mutableListOfResponse.addAll(dlist.list.list) + homeLoading.isVisible = false + homeLoadingError.isVisible = false + homeMasterRecycler.isVisible = true + //home_loaded?.isVisible = true + if (toggleRandomButton) { + //Flatten list + d.values.forEach { dlist -> + mutableListOfResponse.addAll(dlist.list.list) + } + listHomepageItems.addAll(mutableListOfResponse.distinctBy { it.url }) + + homeRandom.isVisible = listHomepageItems.isNotEmpty() + } else { + homeRandom.isGone = true } - listHomepageItems.addAll(mutableListOfResponse.distinctBy { it.url }) - home_random?.isVisible = listHomepageItems.isNotEmpty() - } else { - home_random?.isGone = true } - } - is Resource.Failure -> { - home_loading_shimmer?.stopShimmer() - result_error_text.text = data.errorString + is Resource.Failure -> { - home_reload_connectionerror.setOnClickListener(apiChangeClickListener) + homeLoadingShimmer.stopShimmer() - home_reload_connection_open_in_browser.setOnClickListener { view -> - val validAPIs = apis//.filter { api -> api.hasMainPage } + resultErrorText.text = data.errorString - view.popupMenuNoIconsAndNoStringRes(validAPIs.mapIndexed { index, api -> - Pair( - index, - api.name - ) - }) { - try { - val i = Intent(Intent.ACTION_VIEW) - i.data = Uri.parse(validAPIs[itemId].mainUrl) - startActivity(i) - } catch (e: Exception) { - logError(e) + homeReloadConnectionerror.setOnClickListener(apiChangeClickListener) + + homeReloadConnectionOpenInBrowser.setOnClickListener { view -> + val validAPIs = apis//.filter { api -> api.hasMainPage } + + view.popupMenuNoIconsAndNoStringRes(validAPIs.mapIndexed { index, api -> + Pair( + index, + api.name + ) + }) { + try { + val i = Intent(Intent.ACTION_VIEW) + i.data = Uri.parse(validAPIs[itemId].mainUrl) + startActivity(i) + } catch (e: Exception) { + logError(e) + } } } + + homeLoading.isVisible = false + homeLoadingError.isVisible = true + homeMasterRecycler.isVisible = false + //home_loaded?.isVisible = false } - home_loading?.isVisible = false - home_loading_error?.isVisible = true - home_master_recycler?.isVisible = false - //home_loaded?.isVisible = false - } - is Resource.Loading -> { - (home_master_recycler?.adapter as? ParentItemAdapter)?.updateList(listOf()) - home_loading_shimmer?.startShimmer() - home_loading?.isVisible = true - home_loading_error?.isVisible = false - home_master_recycler?.isVisible = false - //home_loaded?.isVisible = false + is Resource.Loading -> { + (homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(listOf()) + homeLoadingShimmer.startShimmer() + homeLoading.isVisible = true + homeLoadingError.isVisible = false + homeMasterRecycler.isVisible = false + //home_loaded?.isVisible = false + } } } } @@ -638,19 +665,19 @@ class HomeFragment : Fragment() { HOME_BOOKMARK_VALUE_LIST, availableWatchStatusTypes.first.map { it.internalId }.toIntArray() ) - (home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setAvailableWatchStatusTypes( + (binding?.homeMasterRecycler?.adapter as? HomeParentItemAdapterPreview?)?.setAvailableWatchStatusTypes( availableWatchStatusTypes ) } observe(homeViewModel.bookmarks) { data -> - (home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setBookmarkData( + (binding?.homeMasterRecycler?.adapter as? HomeParentItemAdapterPreview?)?.setBookmarkData( data ) } observe(homeViewModel.resumeWatching) { resumeWatching -> - (home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setResumeWatchingData( + (binding?.homeMasterRecycler?.adapter as? HomeParentItemAdapterPreview?)?.setResumeWatchingData( resumeWatching ) if (isTrueTvSettings()) { @@ -665,9 +692,9 @@ class HomeFragment : Fragment() { //context?.fixPaddingStatusbarView(home_statusbar) //context?.fixPaddingStatusbar(home_padding) - context?.fixPaddingStatusbar(home_loading_statusbar) + fixPaddingStatusbar(binding?.homeLoadingStatusbar) - home_master_recycler?.adapter = + binding?.homeMasterRecycler?.adapter = HomeParentItemAdapterPreview(mutableListOf(), { callback -> homeHandleSearch(callback) }, { item -> @@ -699,18 +726,22 @@ class HomeFragment : Fragment() { reloadStored() loadHomePage(false) - home_master_recycler?.addOnScrollListener(object : RecyclerView.OnScrollListener() { + binding?.homeMasterRecycler?.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - if (dy > 0) { //check for scroll down - home_api_fab?.shrink() // hide - home_random?.shrink() - } else if (dy < -5) { - if (!isTvSettings()) { - home_api_fab?.extend() // show - home_random?.extend() + + binding?.apply { + if (dy > 0) { //check for scroll down + homeApiFab.shrink() // hide + homeRandom.shrink() + } else if (dy < -5) { + if (!isTvSettings()) { + homeApiFab.extend() // show + homeRandom.extend() + } } } + super.onScrolled(recyclerView, dx, dy) } }) @@ -718,18 +749,20 @@ class HomeFragment : Fragment() { // nice profile pic on homepage //home_profile_picture_holder?.isVisible = false // just in case - if (isTvSettings()) { - home_api_fab?.isVisible = false - if (isTrueTvSettings()) { - home_change_api_loading?.isVisible = true - home_change_api_loading?.isFocusable = true - home_change_api_loading?.isFocusableInTouchMode = true + binding?.apply { + if (isTvSettings()) { + homeApiFab.isVisible = false + if (isTrueTvSettings()) { + homeChangeApiLoading.isVisible = true + homeChangeApiLoading.isFocusable = true + homeChangeApiLoading.isFocusableInTouchMode = true + } + // home_bookmark_select?.isFocusable = true + // home_bookmark_select?.isFocusableInTouchMode = true + } else { + homeApiFab.isVisible = true + homeChangeApiLoading.isVisible = false } - // home_bookmark_select?.isFocusable = true - // home_bookmark_select?.isFocusableInTouchMode = true - } else { - home_api_fab?.isVisible = true - home_change_api_loading?.isVisible = false } //TODO READD THIS /*for (syncApi in OAuth2Apis) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 94a1a526..715f1867 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -544,7 +544,7 @@ class HomeParentItemAdapterPreview( } } - itemView.home_search?.context?.fixPaddingStatusbar(itemView.home_search) + fixPaddingStatusbar(itemView.home_search) itemView.home_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { @@ -575,7 +575,7 @@ class HomeParentItemAdapterPreview( layoutParams = params } } else { - itemView.home_none_padding?.context?.fixPaddingStatusbarView(itemView.home_none_padding) + fixPaddingStatusbarView(itemView.home_none_padding) } when (preview) { is Resource.Success -> { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index d7c06c4e..a20cd5c6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -6,7 +6,6 @@ import android.content.res.Configuration import android.os.Bundle import android.os.Handler import android.os.Looper -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -14,6 +13,7 @@ import android.view.animation.AlphaAnimation import androidx.annotation.StringRes import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible +import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import com.google.android.material.tabs.TabLayoutMediator import com.lagradost.cloudstream3.APIHolder @@ -22,6 +22,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.observe @@ -37,7 +38,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount -import kotlinx.android.synthetic.main.fragment_library.* import kotlin.math.abs const val LIBRARY_FOLDER = "library_folder" @@ -73,14 +73,25 @@ class LibraryFragment : Fragment() { private val libraryViewModel: LibraryViewModel by activityViewModels() + var binding: FragmentLibraryBinding? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_library, container, false) + ): View { + val localBinding = FragmentLibraryBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + + //return inflater.inflate(R.layout.fragment_library, container, false) + } + + override fun onDestroyView() { + binding = null + super.onDestroyView() } override fun onSaveInstanceState(outState: Bundle) { - viewpager?.currentItem?.let { currentItem -> + binding?.viewpager?.currentItem?.let { currentItem -> outState.putInt(VIEWPAGER_ITEM_KEY, currentItem) } super.onSaveInstanceState(outState) @@ -88,9 +99,9 @@ class LibraryFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - context?.fixPaddingStatusbar(search_status_bar_padding) + fixPaddingStatusbar(binding?.searchStatusBarPadding) - sort_fab?.setOnClickListener { + binding?.sortFab?.setOnClickListener { val methods = libraryViewModel.sortingMethods.map { txt(it.stringRes).asString(view.context) } @@ -106,7 +117,7 @@ class LibraryFragment : Fragment() { }) } - main_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { libraryViewModel.sort(ListSorting.Query, query) return true @@ -129,7 +140,7 @@ class LibraryFragment : Fragment() { libraryViewModel.reloadPages(false) - list_selector?.setOnClickListener { + binding?.listSelector?.setOnClickListener { val items = libraryViewModel.availableApiNames val currentItem = libraryViewModel.currentApiName.value @@ -209,20 +220,22 @@ class LibraryFragment : Fragment() { } } - provider_selector?.setOnClickListener { + binding?.providerSelector?.setOnClickListener { val syncName = libraryViewModel.currentSyncApi?.syncIdName ?: return@setOnClickListener activity?.showPluginSelectionDialog(syncName.name, syncName) } - viewpager?.setPageTransformer(LibraryScrollTransformer()) - viewpager?.adapter = - viewpager.adapter ?: ViewpagerAdapter(mutableListOf(), { isScrollingDown: Boolean -> - if (isScrollingDown) { - sort_fab?.shrink() - } else { - sort_fab?.extend() - } - }) callback@{ searchClickCallback -> + binding?.viewpager?.setPageTransformer(LibraryScrollTransformer()) + binding?.viewpager?.adapter = + binding?.viewpager?.adapter ?: ViewpagerAdapter( + mutableListOf(), + { isScrollingDown: Boolean -> + if (isScrollingDown) { + binding?.sortFab?.shrink() + } else { + binding?.sortFab?.extend() + } + }) callback@{ searchClickCallback -> // To prevent future accidents debugAssert({ searchClickCallback.card !is SyncAPI.LibraryItem @@ -267,6 +280,7 @@ class LibraryFragment : Fragment() { ) } } + LibraryOpenerType.None -> {} LibraryOpenerType.Provider -> savedSelection.providerData?.apiName?.let { apiName -> @@ -275,8 +289,10 @@ class LibraryFragment : Fragment() { apiName, ) } + LibraryOpenerType.Browser -> openBrowser(searchClickCallback.card.url) + LibraryOpenerType.Search -> { QuickSearchFragment.pushSearch( activity, @@ -288,22 +304,28 @@ class LibraryFragment : Fragment() { } } - viewpager?.offscreenPageLimit = 2 - viewpager?.reduceDragSensitivity() + binding?.apply { + viewpager.offscreenPageLimit = 2 + viewpager.reduceDragSensitivity() + } val startLoading = Runnable { - gridview?.numColumns = context?.getSpanCount() ?: 3 - gridview?.adapter = - context?.let { LoadingPosterAdapter(it, 6 * 3) } - library_loading_overlay?.isVisible = true - library_loading_shimmer?.startShimmer() - empty_list_textview?.isVisible = false + binding?.apply { + gridview.numColumns = context?.getSpanCount() ?: 3 + gridview.adapter = + context?.let { LoadingPosterAdapter(it, 6 * 3) } + libraryLoadingOverlay.isVisible = true + libraryLoadingShimmer.startShimmer() + emptyListTextview.isVisible = false + } } val stopLoading = Runnable { - gridview?.adapter = null - library_loading_overlay?.isVisible = false - library_loading_shimmer?.stopShimmer() + binding?.apply { + gridview.adapter = null + libraryLoadingOverlay.isVisible = false + libraryLoadingShimmer.stopShimmer() + } } val handler = Handler(Looper.getMainLooper()) @@ -314,65 +336,75 @@ class LibraryFragment : Fragment() { handler.removeCallbacks(startLoading) val pages = resource.value val showNotice = pages.all { it.items.isEmpty() } - empty_list_textview?.isVisible = showNotice - if (showNotice) { - if (libraryViewModel.availableApiNames.size > 1) { - empty_list_textview?.setText(R.string.empty_library_logged_in_message) - } else { - empty_list_textview?.setText(R.string.empty_library_no_accounts_message) + + + binding?.apply { + emptyListTextview.isVisible = showNotice + if (showNotice) { + if (libraryViewModel.availableApiNames.size > 1) { + emptyListTextview.setText(R.string.empty_library_logged_in_message) + } else { + emptyListTextview.setText(R.string.empty_library_no_accounts_message) + } } + + (viewpager.adapter as? ViewpagerAdapter)?.pages = pages + // Using notifyItemRangeChanged keeps the animations when sorting + viewpager.adapter?.notifyItemRangeChanged( + 0, + viewpager.adapter?.itemCount ?: 0 + ) + + // Only stop loading after 300ms to hide the fade effect the viewpager produces when updating + // Without this there would be a flashing effect: + // loading -> show old viewpager -> black screen -> show new viewpager + handler.postDelayed(stopLoading, 300) + + savedInstanceState?.getInt(VIEWPAGER_ITEM_KEY)?.let { currentPos -> + if (currentPos < 0) return@let + viewpager.setCurrentItem(currentPos, false) + // Using remove() sets the key to 0 instead of removing it + savedInstanceState.putInt(VIEWPAGER_ITEM_KEY, -1) + } + + // Since the animation to scroll multiple items is so much its better to just hide + // the viewpager a bit while the fastest animation is running + fun hideViewpager(distance: Int) { + if (distance < 3) return + + val hideAnimation = AlphaAnimation(1f, 0f).apply { + duration = distance * 50L + fillAfter = true + } + val showAnimation = AlphaAnimation(0f, 1f).apply { + duration = distance * 50L + startOffset = distance * 100L + fillAfter = true + } + viewpager.startAnimation(hideAnimation) + viewpager.startAnimation(showAnimation) + } + + TabLayoutMediator( + libraryTabLayout, + viewpager, + ) { tab, position -> + tab.text = pages.getOrNull(position)?.title?.asStringNull(context) + tab.view.setOnClickListener { + val currentItem = + binding?.viewpager?.currentItem ?: return@setOnClickListener + val distance = abs(position - currentItem) + hideViewpager(distance) + } + }.attach() } - - (viewpager.adapter as? ViewpagerAdapter)?.pages = pages - // Using notifyItemRangeChanged keeps the animations when sorting - viewpager.adapter?.notifyItemRangeChanged(0, viewpager.adapter?.itemCount ?: 0) - - // Only stop loading after 300ms to hide the fade effect the viewpager produces when updating - // Without this there would be a flashing effect: - // loading -> show old viewpager -> black screen -> show new viewpager - handler.postDelayed(stopLoading, 300) - - savedInstanceState?.getInt(VIEWPAGER_ITEM_KEY)?.let { currentPos -> - if (currentPos < 0) return@let - viewpager?.setCurrentItem(currentPos, false) - // Using remove() sets the key to 0 instead of removing it - savedInstanceState.putInt(VIEWPAGER_ITEM_KEY, -1) - } - - // Since the animation to scroll multiple items is so much its better to just hide - // the viewpager a bit while the fastest animation is running - fun hideViewpager(distance: Int) { - if (distance < 3) return - - val hideAnimation = AlphaAnimation(1f, 0f).apply { - duration = distance * 50L - fillAfter = true - } - val showAnimation = AlphaAnimation(0f, 1f).apply { - duration = distance * 50L - startOffset = distance * 100L - fillAfter = true - } - viewpager?.startAnimation(hideAnimation) - viewpager?.startAnimation(showAnimation) - } - - TabLayoutMediator( - library_tab_layout, - viewpager, - ) { tab, position -> - tab.text = pages.getOrNull(position)?.title?.asStringNull(context) - tab.view.setOnClickListener { - val currentItem = viewpager?.currentItem ?: return@setOnClickListener - val distance = abs(position - currentItem) - hideViewpager(distance) - } - }.attach() } + is Resource.Loading -> { // Only start loading after 200ms to prevent loading cached lists handler.postDelayed(startLoading, 200) } + is Resource.Failure -> { stopLoading.run() // No user indication it failed :( @@ -383,7 +415,7 @@ class LibraryFragment : Fragment() { } override fun onConfigurationChanged(newConfig: Configuration) { - (viewpager.adapter as? ViewpagerAdapter)?.rebind() + (binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind() super.onConfigurationChanged(newConfig) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt index 5f64880c..14d31356 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -11,7 +11,6 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import kotlinx.coroutines.delay enum class ListSorting(@StringRes val stringRes: Int) { Query(R.string.none), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt index 2435f8be..05b05f44 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt @@ -3,23 +3,21 @@ package com.lagradost.cloudstream3.ui.library import android.content.res.ColorStateList import android.graphics.Color import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import android.widget.ImageView import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.AcraApplication import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.android.synthetic.main.search_result_grid_expanded.view.* import kotlin.math.roundToInt @@ -32,8 +30,7 @@ class PageAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return LibraryItemViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.search_result_grid_expanded, parent, false) + SearchResultGridExpandedBinding.inflate(LayoutInflater.from(parent.context), parent, false) ) } @@ -57,8 +54,7 @@ class PageAdapter( } } - inner class LibraryItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val cardView: ImageView = itemView.imageView + inner class LibraryItemViewHolder(val binding : SearchResultGridExpandedBinding) : RecyclerView.ViewHolder(binding.root) { private val compactView = false//itemView.context.getGridIsCompact() private val coverHeight: Int = @@ -85,11 +81,11 @@ class PageAdapter( val fg = getDifferentColor(bg)//palette.getVibrantColor(ContextCompat.getColor(ctx,R.color.ratingColor)) - itemView.text_rating.apply { + binding.textRating.apply { setTextColor(ColorStateList.valueOf(fg)) } - itemView.text_rating_holder?.backgroundTintList = ColorStateList.valueOf(bg) - itemView.watchProgress?.apply { + binding.textRatingHolder.backgroundTintList = ColorStateList.valueOf(bg) + binding.watchProgress.apply { progressTintList = ColorStateList.valueOf(fg) progressBackgroundTintList = ColorStateList.valueOf(bg) } @@ -99,7 +95,7 @@ class PageAdapter( // See searchAdaptor for this, it basically fixes the height if (!compactView) { - cardView.apply { + binding.imageView.apply { layoutParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, coverHeight @@ -108,22 +104,22 @@ class PageAdapter( } val showProgress = item.episodesCompleted != null && item.episodesTotal != null - itemView.watchProgress.isVisible = showProgress + binding.watchProgress.isVisible = showProgress if (showProgress) { - itemView.watchProgress.max = item.episodesTotal!! - itemView.watchProgress.progress = item.episodesCompleted!! + binding.watchProgress.max = item.episodesTotal!! + binding.watchProgress.progress = item.episodesCompleted!! } - itemView.imageText.text = item.name + binding.imageText.text = item.name val showRating = (item.personalRating ?: 0) != 0 - itemView.text_rating_holder.isVisible = showRating + binding.textRatingHolder.isVisible = showRating if (showRating) { // We want to show 8.5 but not 8.0 hence the replace val rating = ((item.personalRating ?: 0).toDouble() / 10).toString() .replace(".0", "") - itemView.text_rating.text = "★ $rating" + binding.textRating.text = "★ $rating" } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt index 33a40386..441d6adc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt @@ -2,16 +2,14 @@ package com.lagradost.cloudstream3.ui.library import android.os.Build import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.core.view.doOnAttach import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.OnFlingListener -import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.LibraryViewpagerPageBinding import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount -import kotlinx.android.synthetic.main.library_viewpager_page.view.* class ViewpagerAdapter( var pages: List, @@ -20,8 +18,7 @@ class ViewpagerAdapter( ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return PageViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.library_viewpager_page, parent, false) + LibraryViewpagerPageBinding.inflate(LayoutInflater.from(parent.context), parent, false) ) } @@ -34,6 +31,7 @@ class ViewpagerAdapter( } private val unbound = mutableSetOf() + /** * Used to mark all pages for re-binding and forces all items to be refreshed * Without this the pages will still use the same adapters @@ -43,44 +41,46 @@ class ViewpagerAdapter( this.notifyItemRangeChanged(0, pages.size) } - inner class PageViewHolder(private val itemViewTest: View) : - RecyclerView.ViewHolder(itemViewTest) { + inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) : + RecyclerView.ViewHolder(binding.root) { fun bind(page: SyncAPI.Page, rebind: Boolean) { - itemView.page_recyclerview?.spanCount = - this@PageViewHolder.itemView.context.getSpanCount() ?: 3 - - if (itemViewTest.page_recyclerview?.adapter == null || rebind) { - // Only add the items after it has been attached since the items rely on ItemWidth - // Which is only determined after the recyclerview is attached. - // If this fails then item height becomes 0 when there is only one item - itemViewTest.page_recyclerview?.doOnAttach { - itemViewTest.page_recyclerview?.adapter = PageAdapter( - page.items.toMutableList(), - itemViewTest.page_recyclerview, - clickCallback - ) + 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 { - (itemViewTest.page_recyclerview?.adapter as? PageAdapter)?.updateList(page.items) - itemViewTest.page_recyclerview?.scrollToPosition(0) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - itemViewTest.page_recyclerview.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY -> - val diff = scrollY - oldScrollY - if (diff == 0) return@setOnScrollChangeListener + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY -> + val diff = scrollY - oldScrollY + if (diff == 0) return@setOnScrollChangeListener - scrollCallback.invoke(diff > 0) - } - } else { - itemViewTest.page_recyclerview.onFlingListener = object : OnFlingListener() { - override fun onFling(velocityX: Int, velocityY: Int): Boolean { - scrollCallback.invoke(velocityY > 0) - return false + scrollCallback.invoke(diff > 0) + } + } else { + onFlingListener = object : OnFlingListener() { + override fun onFling(velocityX: Int, velocityY: Int): Boolean { + scrollCallback.invoke(velocityY > 0) + return false + } } } } + } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt index ba57d2de..9e5ca6ba 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt @@ -21,6 +21,7 @@ import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.QuickSearchBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe @@ -37,7 +38,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage -import kotlinx.android.synthetic.main.quick_search.* import java.util.concurrent.locks.ReentrantLock class QuickSearchFragment : Fragment() { @@ -72,6 +72,8 @@ class QuickSearchFragment : Fragment() { private var providers: Set? = null private lateinit var searchViewModel: SearchViewModel + var binding: QuickSearchBinding? = null + private var bottomSheetDialog: BottomSheetDialog? = null @@ -79,13 +81,21 @@ class QuickSearchFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { activity?.window?.setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE ) searchViewModel = ViewModelProvider(this)[SearchViewModel::class.java] bottomSheetDialog?.ownShow() - return inflater.inflate(R.layout.quick_search, container, false) + val localBinding = QuickSearchBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + //return inflater.inflate(R.layout.quick_search, container, false) + } + + override fun onDestroyView() { + binding = null + super.onDestroyView() } override fun onDestroy() { @@ -111,7 +121,7 @@ class QuickSearchFragment : Fragment() { activity?.getSpanCount()?.let { HomeFragment.currentSpan = it } - quick_search_autofit_results.spanCount = HomeFragment.currentSpan + binding?.quickSearchAutofitResults?.spanCount = HomeFragment.currentSpan HomeFragment.currentSpan = HomeFragment.currentSpan HomeFragment.configEvent.invoke(HomeFragment.currentSpan) } @@ -123,7 +133,7 @@ class QuickSearchFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - context?.fixPaddingStatusbar(quick_search_root) + fixPaddingStatusbar(binding?.quickSearchRoot) fixGrid() arguments?.getStringArray(PROVIDER_KEY)?.let { @@ -136,21 +146,22 @@ class QuickSearchFragment : Fragment() { } else false if (isSingleProvider) { - quick_search_autofit_results.adapter = activity?.let { - SearchAdapter( + binding?.quickSearchAutofitResults?.apply { + adapter = SearchAdapter( ArrayList(), - quick_search_autofit_results, + this, ) { callback -> SearchHelper.handleSearchClickCallback(activity, callback) } } + try { - quick_search?.queryHint = getString(R.string.search_hint_site).format(providers?.first()) + binding?.quickSearch?.queryHint = getString(R.string.search_hint_site).format(providers?.first()) } catch (e: Exception) { logError(e) } } else { - quick_search_master_recycler?.adapter = + binding?.quickSearchMasterRecycler?.adapter = ParentItemAdapter(mutableListOf(), { callback -> SearchHelper.handleSearchClickCallback(activity, callback) //when (callback.action) { @@ -164,18 +175,17 @@ class QuickSearchFragment : Fragment() { bottomSheetDialog = null }) }) - quick_search_master_recycler?.layoutManager = GridLayoutManager(context, 1) + binding?.quickSearchMasterRecycler?.layoutManager = GridLayoutManager(context, 1) } - - quick_search_autofit_results?.isVisible = isSingleProvider - quick_search_master_recycler?.isGone = isSingleProvider + binding?.quickSearchAutofitResults?.isVisible = isSingleProvider + binding?.quickSearchMasterRecycler?.isGone = isSingleProvider val listLock = ReentrantLock() observe(searchViewModel.currentSearch) { list -> try { // https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist listLock.lock() - (quick_search_master_recycler?.adapter as ParentItemAdapter?)?.apply { + (binding?.quickSearchMasterRecycler?.adapter as ParentItemAdapter?)?.apply { updateList(list.map { ongoing -> val ongoingList = HomePageList( ongoing.apiName, @@ -192,19 +202,18 @@ class QuickSearchFragment : Fragment() { } val searchExitIcon = - quick_search?.findViewById(androidx.appcompat.R.id.search_close_btn) + binding?.quickSearch?.findViewById(androidx.appcompat.R.id.search_close_btn) //val searchMagIcon = - // quick_search?.findViewById(androidx.appcompat.R.id.search_mag_icon) + // binding.quickSearch.findViewById(androidx.appcompat.R.id.search_mag_icon) //searchMagIcon?.scaleX = 0.65f //searchMagIcon?.scaleY = 0.65f - - quick_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + binding?.quickSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { if (search(context, query, false)) - UIHelper.hideKeyboard(quick_search) + UIHelper.hideKeyboard(binding?.quickSearch) return true } @@ -214,27 +223,26 @@ class QuickSearchFragment : Fragment() { return true } }) - - quick_search_loading_bar.alpha = 0f + binding?.quickSearchLoadingBar?.alpha = 0f observe(searchViewModel.searchResponse) { when (it) { is Resource.Success -> { it.value.let { data -> - (quick_search_autofit_results?.adapter as? SearchAdapter)?.updateList( + (binding?.quickSearchAutofitResults?.adapter as? SearchAdapter)?.updateList( context?.filterSearchResultByFilmQuality(data) ?: data ) } searchExitIcon?.alpha = 1f - quick_search_loading_bar?.alpha = 0f + binding?.quickSearchLoadingBar?.alpha = 0f } is Resource.Failure -> { // Toast.makeText(activity, "Server error", Toast.LENGTH_LONG).show() searchExitIcon?.alpha = 1f - quick_search_loading_bar?.alpha = 0f + binding?.quickSearchLoadingBar?.alpha = 0f } is Resource.Loading -> { searchExitIcon?.alpha = 0f - quick_search_loading_bar?.alpha = 1f + binding?.quickSearchLoadingBar?.alpha = 1f } } } @@ -246,13 +254,12 @@ class QuickSearchFragment : Fragment() { // UIHelper.showInputMethod(view.findFocus()) // } //} - - quick_search_back.setOnClickListener { + binding?.quickSearchBack?.setOnClickListener { activity?.popCurrentPage() } arguments?.getString(AUTOSEARCH_KEY)?.let { - quick_search?.setQuery(it, true) + binding?.quickSearch?.setQuery(it, true) arguments?.remove(AUTOSEARCH_KEY) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index 92cecc37..7b415d78 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -1,20 +1,17 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.ActorData import com.lagradost.cloudstream3.ActorRole import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.CastItemBinding import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.cast_item.view.* -class ActorAdaptor() : RecyclerView.Adapter() { +class ActorAdaptor : RecyclerView.Adapter() { data class ActorMetaData( var isInverted: Boolean, val actor: ActorData, @@ -24,7 +21,7 @@ class ActorAdaptor() : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return CardViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.cast_item, parent, false), + CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) ) } @@ -68,15 +65,9 @@ class ActorAdaptor() : RecyclerView.Adapter() { private class CardViewHolder constructor( - itemView: View, + val binding: CastItemBinding, ) : - RecyclerView.ViewHolder(itemView) { - private val actorImage: ImageView = itemView.actor_image - private val actorName: TextView = itemView.actor_name - private val actorExtra: TextView = itemView.actor_extra - private val voiceActorImage: ImageView = itemView.voice_actor_image - private val voiceActorImageHolder: View = itemView.voice_actor_image_holder - private val voiceActorName: TextView = itemView.voice_actor_name + RecyclerView.ViewHolder(binding.root) { fun bind(actor: ActorData, isInverted: Boolean, position: Int, callback: (Int) -> Unit) { val (mainImg, vaImage) = if (!isInverted || actor.voiceActor?.image.isNullOrBlank()) { @@ -89,39 +80,43 @@ class ActorAdaptor() : RecyclerView.Adapter() { callback(position) } - actorImage.setImage(mainImg) + binding.apply { + actorImage.setImage(mainImg) - actorName.text = actor.actor.name - actor.role?.let { - actorExtra.context?.getString( - when (it) { - ActorRole.Main -> { - R.string.actor_main - } - ActorRole.Supporting -> { - R.string.actor_supporting - } - ActorRole.Background -> { - R.string.actor_background + actorName.text = actor.actor.name + actor.role?.let { + actorExtra.context?.getString( + when (it) { + ActorRole.Main -> { + R.string.actor_main + } + + ActorRole.Supporting -> { + R.string.actor_supporting + } + + ActorRole.Background -> { + R.string.actor_background + } } + )?.let { text -> + actorExtra.isVisible = true + actorExtra.text = text } - )?.let { text -> + } ?: actor.roleString?.let { actorExtra.isVisible = true - actorExtra.text = text + actorExtra.text = it + } ?: run { + actorExtra.isVisible = false } - } ?: actor.roleString?.let { - actorExtra.isVisible = true - actorExtra.text = it - } ?: run { - actorExtra.isVisible = false - } - if (actor.voiceActor == null) { - voiceActorImageHolder.isVisible = false - voiceActorName.isVisible = false - } else { - voiceActorName.text = actor.voiceActor.name - voiceActorImageHolder.isVisible = voiceActorImage.setImage(vaImage) + if (actor.voiceActor == null) { + voiceActorImageHolder.isVisible = false + voiceActorName.isVisible = false + } else { + voiceActorName.text = actor.voiceActor.name + voiceActorImageHolder.isVisible = voiceActorImage.setImage(vaImage) + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 0932b001..216d95a4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -3,34 +3,25 @@ package com.lagradost.cloudstream3.ui.result import android.annotation.SuppressLint import android.content.Context import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView import androidx.core.view.isGone import androidx.core.view.isVisible -import androidx.core.widget.ContentLoadingProgressBar import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding +import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DownloadButtonViewHolder import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.ui.download.EasyDownloadButton import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager -import kotlinx.android.synthetic.main.result_episode.view.* -import kotlinx.android.synthetic.main.result_episode.view.episode_text -import kotlinx.android.synthetic.main.result_episode_large.view.* -import kotlinx.android.synthetic.main.result_episode_large.view.episode_filler -import kotlinx.android.synthetic.main.result_episode_large.view.episode_progress -import kotlinx.android.synthetic.main.result_episode_large.view.result_episode_download -import kotlinx.android.synthetic.main.result_episode_large.view.result_episode_progress_downloaded import java.util.* const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 @@ -144,26 +135,52 @@ class EpisodeAdapter( diffResult.dispatchUpdatesTo(this) } - var layout = R.layout.result_episode_both + fun getItem(position: Int) : ResultEpisode { + return cardList[position] + } + + override fun getItemViewType(position: Int): Int { + val item = getItem(position) + return if(item.poster.isNullOrBlank()) 0 else 1 + } + + + // private val layout = R.layout.result_episode_both override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { /*val layout = if (cardList.filter { it.poster != null }.size >= cardList.size / 2) R.layout.result_episode_large else R.layout.result_episode*/ - return EpisodeCardViewHolder( - LayoutInflater.from(parent.context) - .inflate(layout, parent, false), - hasDownloadSupport, - clickCallback, - downloadClickCallback - ) + return when(viewType) { + 0 -> { + EpisodeCardViewHolderSmall( + ResultEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false), + hasDownloadSupport, + clickCallback, + downloadClickCallback + ) + } + 1 -> { + EpisodeCardViewHolderLarge( + ResultEpisodeLargeBinding.inflate(LayoutInflater.from(parent.context), parent, false), + hasDownloadSupport, + clickCallback, + downloadClickCallback + ) + } + else -> throw NotImplementedError() + } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { - is EpisodeCardViewHolder -> { - holder.bind(cardList[position]) + is EpisodeCardViewHolderLarge -> { + holder.bind(getItem(position)) + mBoundViewHolders.add(holder) + } + is EpisodeCardViewHolderSmall -> { + holder.bind(getItem(position)) mBoundViewHolders.add(holder) } } @@ -173,94 +190,81 @@ class EpisodeAdapter( return cardList.size } - class EpisodeCardViewHolder + class EpisodeCardViewHolderLarge constructor( - itemView: View, + val binding : ResultEpisodeLargeBinding, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, private val downloadClickCallback: (DownloadClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder { + ) : RecyclerView.ViewHolder(binding.root), DownloadButtonViewHolder { override var downloadButton = EasyDownloadButton() - var episodeDownloadBar: ContentLoadingProgressBar? = null - var episodeDownloadImage: ImageView? = null + // TODO TV + var localCard: ResultEpisode? = null @SuppressLint("SetTextI18n") fun bind(card: ResultEpisode) { localCard = card + binding.episodeLinHolder.layoutParams.apply { + width = if(isTvSettings()) ViewGroup.LayoutParams.WRAP_CONTENT else ViewGroup.LayoutParams.MATCH_PARENT + } + val isTrueTv = isTrueTvSettings() - val (parentView, otherView) = if (card.poster == null) { - itemView.episode_holder to itemView.episode_holder_large - } else { - itemView.episode_holder_large to itemView.episode_holder - } - parentView.isVisible = true - otherView.isVisible = false + binding.apply { - val episodeText: TextView = parentView.episode_text - val episodeFiller: MaterialButton? = parentView.episode_filler - val episodeRating: TextView? = parentView.episode_rating - val episodeDescript: TextView? = parentView.episode_descript - val episodeProgress: ContentLoadingProgressBar? = parentView.episode_progress - val episodePoster: ImageView? = parentView.episode_poster + val name = + if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}" + episodeFiller.isVisible = card.isFiller == true + episodeText.text = + name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name + episodeText.isSelected = true // is needed for text repeating - episodeDownloadBar = - parentView.result_episode_progress_downloaded - episodeDownloadImage = parentView.result_episode_download - - val name = - if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}" - episodeFiller?.isVisible = card.isFiller == true - episodeText.text = - name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name - episodeText.isSelected = true // is needed for text repeating - - if (card.videoWatchState == VideoWatchState.Watched) { - // This cannot be done in getDisplayPosition() as when you have not watched something - // the duration and position is 0 - episodeProgress?.max = 1 - episodeProgress?.progress = 1 - episodeProgress?.isVisible = true - } else { - val displayPos = card.getDisplayPosition() - episodeProgress?.max = (card.duration / 1000).toInt() - episodeProgress?.progress = (displayPos / 1000).toInt() - episodeProgress?.isVisible = displayPos > 0L - } - - episodePoster?.isVisible = episodePoster?.setImage(card.poster) == true - - if (card.rating != null) { - episodeRating?.text = episodeRating?.context?.getString(R.string.rated_format) - ?.format(card.rating.toFloat() / 10f) - } else { - episodeRating?.text = "" - } - - episodeRating?.isGone = episodeRating?.text.isNullOrBlank() - - episodeDescript?.apply { - text = card.description.html() - isGone = text.isNullOrBlank() - setOnClickListener { - clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card)) - } - } - - if (!isTrueTv) { - episodePoster?.setOnClickListener { - clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) + if (card.videoWatchState == VideoWatchState.Watched) { + // This cannot be done in getDisplayPosition() as when you have not watched something + // the duration and position is 0 + episodeProgress.max = 1 + episodeProgress.progress = 1 + episodeProgress.isVisible = true + } else { + val displayPos = card.getDisplayPosition() + episodeProgress.max = (card.duration / 1000).toInt() + episodeProgress.progress = (displayPos / 1000).toInt() + episodeProgress.isVisible = displayPos > 0L } - episodePoster?.setOnLongClickListener { - clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_TOAST, card)) - return@setOnLongClickListener true + episodePoster.isVisible = episodePoster.setImage(card.poster) == true + + if (card.rating != null) { + episodeRating.text = episodeRating.context?.getString(R.string.rated_format) + ?.format(card.rating.toFloat() / 10f) + } else { + episodeRating.text = "" + } + + episodeRating.isGone = episodeRating.text.isNullOrBlank() + + episodeDescript.apply { + text = card.description.html() + isGone = text.isNullOrBlank() + setOnClickListener { + clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card)) + } + } + + if (!isTrueTv) { + episodePoster.setOnClickListener { + clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) + } + + episodePoster.setOnLongClickListener { + clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_TOAST, card)) + return@setOnLongClickListener true + } } } - itemView.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } @@ -276,8 +280,8 @@ class EpisodeAdapter( return@setOnLongClickListener true } - episodeDownloadImage?.isVisible = hasDownloadSupport - episodeDownloadBar?.isVisible = hasDownloadSupport + binding.resultEpisodeDownload.isVisible = hasDownloadSupport + binding.resultEpisodeProgressDownloaded.isVisible = hasDownloadSupport reattachDownloadButton() } @@ -285,9 +289,6 @@ class EpisodeAdapter( downloadButton.dispose() val card = localCard if (hasDownloadSupport && card != null) { - if (episodeDownloadBar == null || - episodeDownloadImage == null - ) return val downloadInfo = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings( itemView.context, card.id @@ -296,8 +297,109 @@ class EpisodeAdapter( downloadButton.setUpButton( downloadInfo?.fileLength, downloadInfo?.totalBytes, - episodeDownloadBar ?: return, - episodeDownloadImage ?: return, + binding.resultEpisodeProgressDownloaded, + binding.resultEpisodeDownload, + null, + VideoDownloadHelper.DownloadEpisodeCached( + card.name, + card.poster, + card.episode, + card.season, + card.id, + card.parentId, + card.rating, + card.description, + System.currentTimeMillis(), + ) + ) { + if (it.action == DOWNLOAD_ACTION_DOWNLOAD) { + clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card)) + } else { + downloadClickCallback.invoke(it) + } + } + } + } + } + + class EpisodeCardViewHolderSmall + constructor( + val binding : ResultEpisodeBinding, + private val hasDownloadSupport: Boolean, + private val clickCallback: (EpisodeClickEvent) -> Unit, + private val downloadClickCallback: (DownloadClickEvent) -> Unit, + ) : RecyclerView.ViewHolder(binding.root), DownloadButtonViewHolder { + override var downloadButton = EasyDownloadButton() + + var localCard: ResultEpisode? = null + + @SuppressLint("SetTextI18n") + fun bind(card: ResultEpisode) { + localCard = card + + val isTrueTv = isTrueTvSettings() + + binding.episodeHolder.layoutParams.apply { + width = if(isTvSettings()) ViewGroup.LayoutParams.WRAP_CONTENT else ViewGroup.LayoutParams.MATCH_PARENT + } + + binding.apply { + val name = + if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}" + episodeFiller.isVisible = card.isFiller == true + episodeText.text = + name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name + episodeText.isSelected = true // is needed for text repeating + + if (card.videoWatchState == VideoWatchState.Watched) { + // This cannot be done in getDisplayPosition() as when you have not watched something + // the duration and position is 0 + episodeProgress.max = 1 + episodeProgress.progress = 1 + episodeProgress.isVisible = true + } else { + val displayPos = card.getDisplayPosition() + episodeProgress.max = (card.duration / 1000).toInt() + episodeProgress.progress = (displayPos / 1000).toInt() + episodeProgress.isVisible = displayPos > 0L + } + + itemView.setOnClickListener { + clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) + } + + if (isTrueTv) { + itemView.isFocusable = true + itemView.isFocusableInTouchMode = true + //itemView.touchscreenBlocksFocus = false + } + + itemView.setOnLongClickListener { + clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card)) + return@setOnLongClickListener true + } + + binding.resultEpisodeDownload.isVisible = hasDownloadSupport + binding.resultEpisodeProgressDownloaded.isVisible = hasDownloadSupport + reattachDownloadButton() + } + } + + override fun reattachDownloadButton() { + downloadButton.dispose() + val card = localCard + if (hasDownloadSupport && card != null) { + + val downloadInfo = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings( + itemView.context, + card.id + ) + + downloadButton.setUpButton( + downloadInfo?.fileLength, + downloadInfo?.totalBytes, + binding.resultEpisodeProgressDownloaded, + binding.resultEpisodeDownload, null, VideoDownloadHelper.DownloadEpisodeCached( card.name, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt index ebd6a658..ca2934ef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt @@ -1,11 +1,10 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.ImageView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings /* @@ -24,7 +23,6 @@ const val IMAGE_CLICK = 0 const val IMAGE_LONG_CLICK = 1 class ImageAdapter( - val layout: Int, val clickCallback: ((Int) -> Unit)? = null, val nextFocusUp: Int? = null, val nextFocusDown: Int? = null, @@ -34,7 +32,9 @@ class ImageAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ImageViewHolder( - LayoutInflater.from(parent.context).inflate(layout, parent, false) + //result_mini_image + ResultMiniImageBinding.inflate(LayoutInflater.from(parent.context), parent, false) + // LayoutInflater.from(parent.context).inflate(layout, parent, false) ) } @@ -66,15 +66,15 @@ class ImageAdapter( } class ImageViewHolder - constructor(itemView: View) : - RecyclerView.ViewHolder(itemView) { + constructor(val binding: ResultMiniImageBinding) : + RecyclerView.ViewHolder(binding.root) { fun bind( img: Int, clickCallback: ((Int) -> Unit)?, nextFocusUp: Int?, nextFocusDown: Int?, ) { - (itemView as? ImageView?)?.apply { + binding.root.apply { setImageResource(img) if (nextFocusDown != null) { this.nextFocusDownId = nextFocusDown diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 5a3e28b4..68fc87e6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -310,6 +310,7 @@ open class ResultFragment : ResultTrailerPlayer() { result_finish_loading?.isVisible = false result_loading_error?.isVisible = false } + 1 -> { result_bookmark_fab?.isGone = true result_loading?.isVisible = false @@ -317,6 +318,7 @@ open class ResultFragment : ResultTrailerPlayer() { result_loading_error?.isVisible = true result_reload_connection_open_in_browser?.isVisible = true } + 2 -> { result_bookmark_fab?.isGone = isTrueTvSettings() result_bookmark_fab?.extend() @@ -350,9 +352,9 @@ open class ResultFragment : ResultTrailerPlayer() { viewModel.reloadEpisodes() } - open fun updateMovie(data: ResourceSome>) { + open fun updateMovie(data: Resource>?) { when (data) { - is ResourceSome.Success -> { + is Resource.Success -> { data.value.let { (text, ep) -> result_play_movie.setText(text) result_play_movie?.setOnClickListener { @@ -410,6 +412,7 @@ open class ResultFragment : ResultTrailerPlayer() { EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, ep) ) } + else -> handleDownloadClick(activity, click) } } @@ -417,6 +420,7 @@ open class ResultFragment : ResultTrailerPlayer() { } } } + else -> { result_movie_progress_downloaded_holder?.isVisible = false result_play_movie?.isVisible = false @@ -424,17 +428,14 @@ open class ResultFragment : ResultTrailerPlayer() { } } - open fun updateEpisodes(episodes: ResourceSome>) { + open fun updateEpisodes(episodes: Resource>?) { when (episodes) { - is ResourceSome.None -> { - result_episode_loading?.isVisible = false - result_episodes?.isVisible = false - } - is ResourceSome.Loading -> { + is Resource.Loading -> { result_episode_loading?.isVisible = true result_episodes?.isVisible = false } - is ResourceSome.Success -> { + + is Resource.Success -> { result_episodes?.isVisible = true result_episode_loading?.isVisible = false @@ -471,6 +472,11 @@ open class ResultFragment : ResultTrailerPlayer() { result_episodes?.requestFocus() } } + + else -> { + result_episode_loading?.isVisible = false + result_episodes?.isVisible = false + } } } @@ -565,7 +571,7 @@ open class ResultFragment : ResultTrailerPlayer() { context?.updateHasTrailers() activity?.loadCache() - activity?.fixPaddingStatusbar(result_top_bar) + fixPaddingStatusbar(result_top_bar) //activity?.fixPaddingStatusbar(result_barstatus) /* val backParameter = result_back.layoutParams as FrameLayout.LayoutParams @@ -588,7 +594,7 @@ open class ResultFragment : ResultTrailerPlayer() { result_episodes?.adapter = EpisodeAdapter( - api?.hasDownloadSupport == true, + api?.hasDownloadSupport == true && !isTvSettings(), { episodeClick -> viewModel.handleAction(activity, episodeClick) }, @@ -738,10 +744,12 @@ open class ResultFragment : ResultTrailerPlayer() { viewModel.setMeta(d, syncModel.getSyncs()) } + is Resource.Loading -> { result_sync_max_episodes?.text = result_sync_max_episodes?.context?.getString(R.string.sync_total_episodes_none) } + else -> {} } } @@ -755,11 +763,13 @@ open class ResultFragment : ResultTrailerPlayer() { result_sync_holder?.isVisible = false closed = true } + is Resource.Loading -> { result_sync_loading_shimmer?.startShimmer() result_sync_loading_shimmer?.isVisible = true result_sync_holder?.isVisible = false } + is Resource.Success -> { result_sync_loading_shimmer?.stopShimmer() result_sync_loading_shimmer?.isVisible = false @@ -789,6 +799,7 @@ open class ResultFragment : ResultTrailerPlayer() { } } } + null -> { closed = false } @@ -796,58 +807,56 @@ open class ResultFragment : ResultTrailerPlayer() { result_overlapping_panels?.setStartPanelLockState(if (closed) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED) } - observe(viewModel.resumeWatching) { resume -> - when (resume) { - is Some.Success -> { - result_resume_parent?.isVisible = true - val value = resume.value - value.progress?.let { progress -> - result_resume_series_title?.apply { - isVisible = !value.isMovie - text = - if (value.isMovie) null else activity?.getNameFull( - value.result.name, - value.result.episode, - value.result.season - ) - } - result_resume_series_progress_text.setText(progress.progressLeft) - result_resume_series_progress?.apply { - isVisible = true - this.max = progress.maxProgress - this.progress = progress.progress - } - result_resume_progress_holder?.isVisible = true - } ?: run { - result_resume_progress_holder?.isVisible = false - result_resume_series_progress?.isVisible = false - result_resume_series_title?.isVisible = false - result_resume_series_progress_text?.isVisible = false - } - - result_resume_series_button?.isVisible = !value.isMovie - result_resume_series_button_play?.isVisible = !value.isMovie - - val click = View.OnClickListener { - viewModel.handleAction( - activity, - EpisodeClickEvent( - storedData?.playerAction ?: ACTION_PLAY_EPISODE_IN_PLAYER, - value.result - ) - ) - } - - result_resume_series_button?.setOnClickListener(click) - result_resume_series_button_play?.setOnClickListener(click) - } - is Some.None -> { - result_resume_parent?.isVisible = false - } + observeNullable(viewModel.resumeWatching) { resume -> + if (resume == null) { + result_resume_parent?.isVisible = false + return@observeNullable } + result_resume_parent?.isVisible = true + resume.progress?.let { progress -> + result_resume_series_title?.apply { + isVisible = !resume.isMovie + text = + if (resume.isMovie) null else activity?.getNameFull( + resume.result.name, + resume.result.episode, + resume.result.season + ) + } + result_resume_series_progress_text?.setText(progress.progressLeft) + result_resume_series_progress?.apply { + isVisible = true + this.max = progress.maxProgress + this.progress = progress.progress + } + result_resume_progress_holder?.isVisible = true + } ?: run { + result_resume_progress_holder?.isVisible = false + result_resume_series_progress?.isVisible = false + result_resume_series_title?.isVisible = false + result_resume_series_progress_text?.isVisible = false + } + + result_resume_series_button?.isVisible = !resume.isMovie + result_resume_series_button_play?.isVisible = !resume.isMovie + + val click = View.OnClickListener { + viewModel.handleAction( + activity, + EpisodeClickEvent( + storedData?.playerAction ?: ACTION_PLAY_EPISODE_IN_PLAYER, + resume.result + ) + ) + } + + result_resume_series_button?.setOnClickListener(click) + result_resume_series_button_play?.setOnClickListener(click) } - observe(viewModel.episodes) { episodes -> + + + observeNullable(viewModel.episodes) { episodes -> updateEpisodes(episodes) } @@ -868,7 +877,7 @@ open class ResultFragment : ResultTrailerPlayer() { setRecommendations(recommendations, null) } - observe(viewModel.movie) { data -> + observeNullable(viewModel.movie) { data -> updateMovie(data) } @@ -1046,10 +1055,12 @@ open class ResultFragment : ResultTrailerPlayer() { }*/ //} } + is Resource.Failure -> { result_error_text.text = storedData?.url?.plus("\n") + data.errorString updateVisStatus(1) } + is Resource.Loading -> { updateVisStatus(0) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index 2f232995..571d4b0b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -20,9 +20,9 @@ import com.google.android.gms.cast.framework.CastState import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.updateHasTrailers -import com.lagradost.cloudstream3.mvvm.Some import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper @@ -212,7 +212,6 @@ class ResultFragmentPhone : ResultFragment() { }*/ result_mini_sync?.adapter = ImageAdapter( - R.layout.result_mini_image, nextFocusDown = R.id.result_sync_set_score, clickCallback = { action -> if (action == IMAGE_CLICK || action == IMAGE_LONG_CLICK) { @@ -271,73 +270,68 @@ class ResultFragmentPhone : ResultFragment() { } } - observe(viewModel.episodesCountText) { count -> + observeNullable(viewModel.episodesCountText) { count -> result_episodes_text.setText(count) } - observe(viewModel.selectPopup) { popup -> - when (popup) { - is Some.Success -> { - popupDialog?.dismissSafe(activity) - - popupDialog = activity?.let { act -> - val pop = popup.value - val options = pop.getOptions(act) - val title = pop.getTitle(act) - - act.showBottomDialogInstant( - options, title, { - popupDialog = null - pop.callback(null) - }, { - popupDialog = null - pop.callback(it) - } - ) - } - } - is Some.None -> { - popupDialog?.dismissSafe(activity) - popupDialog = null - } + observeNullable(viewModel.selectPopup) { popup -> + if (popup == null) { + popupDialog?.dismissSafe(activity) + popupDialog = null + return@observeNullable } + popupDialog?.dismissSafe(activity) + + popupDialog = activity?.let { act -> + val options = popup.getOptions(act) + val title = popup.getTitle(act) + + act.showBottomDialogInstant( + options, title, { + popupDialog = null + popup.callback(null) + }, { + popupDialog = null + popup.callback(it) + } + ) + } + + } observe(viewModel.loadedLinks) { load -> - when (load) { - is Some.Success -> { - if (loadingDialog?.isShowing != true) { - loadingDialog?.dismissSafe(activity) - loadingDialog = null - } - loadingDialog = loadingDialog ?: context?.let { ctx -> - val builder = - BottomSheetDialog(ctx) - builder.setContentView(R.layout.bottom_loading) - builder.setOnDismissListener { - loadingDialog = null - viewModel.cancelLinks() - } - //builder.setOnCancelListener { - // it?.dismiss() - //} - builder.setCanceledOnTouchOutside(true) - builder.show() - builder - } - } - is Some.None -> { - loadingDialog?.dismissSafe(activity) + if (load == null) { + loadingDialog?.dismissSafe(activity) + loadingDialog = null + return@observe + } + if (loadingDialog?.isShowing != true) { + loadingDialog?.dismissSafe(activity) + loadingDialog = null + } + loadingDialog = loadingDialog ?: context?.let { ctx -> + val builder = + BottomSheetDialog(ctx) + builder.setContentView(R.layout.bottom_loading) + builder.setOnDismissListener { loadingDialog = null + viewModel.cancelLinks() } + //builder.setOnCancelListener { + // it?.dismiss() + //} + builder.setCanceledOnTouchOutside(true) + builder.show() + builder } } - observe(viewModel.selectedSeason) { text -> + observeNullable(viewModel.selectedSeason) { text -> result_season_button.setText(text) selectSeason = - (if (text is Some.Success) text.value else null)?.asStringNull(result_season_button?.context) + text?.asStringNull(result_season_button?.context) // If the season button is visible the result season button will be next focus down if (result_season_button?.isVisible == true) if (result_resume_parent?.isVisible == true) @@ -346,7 +340,7 @@ class ResultFragmentPhone : ResultFragment() { // setFocusUpAndDown(result_bookmark_button, result_season_button) } - observe(viewModel.selectedDubStatus) { status -> + observeNullable(viewModel.selectedDubStatus) { status -> result_dub_select?.setText(status) if (result_dub_select?.isVisible == true) @@ -357,7 +351,7 @@ class ResultFragmentPhone : ResultFragment() { // setFocusUpAndDown(result_bookmark_button, result_dub_select) } } - observe(viewModel.selectedRange) { range -> + observeNullable(viewModel.selectedRange) { range -> result_episode_select.setText(range) // If Season button is invisible then the bookmark button next focus is episode select diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index 71ecb0e9..91354117 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -12,9 +12,9 @@ import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse -import com.lagradost.cloudstream3.mvvm.ResourceSome -import com.lagradost.cloudstream3.mvvm.Some +import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.search.SearchAdapter @@ -69,16 +69,16 @@ class ResultFragmentTv : ResultFragment() { return focus == this.result_root } - override fun updateEpisodes(episodes: ResourceSome>) { + override fun updateEpisodes(episodes: Resource>?) { super.updateEpisodes(episodes) - if (episodes is ResourceSome.Success && hasNoFocus()) { + if (episodes is Resource.Success && hasNoFocus()) { result_episodes?.requestFocus() } } - override fun updateMovie(data: ResourceSome>) { + override fun updateMovie(data: Resource>?) { super.updateMovie(data) - if (data is ResourceSome.Success && hasNoFocus()) { + if (data is Resource.Success && hasNoFocus()) { result_play_movie?.requestFocus() } } @@ -130,9 +130,9 @@ class ResultFragmentTv : ResultFragment() { LinearListLayout(result_episodes?.context).apply { setHorizontal() } - (result_episodes?.adapter as EpisodeAdapter?)?.apply { - layout = R.layout.result_episode_both_tv - } + // (result_episodes?.adapter as EpisodeAdapter?)?.apply { + // layout = R.layout.result_episode_both_tv + // } //result_episodes?.setMaxViewPoolSize(0, Int.MAX_VALUE) result_season_selection.setAdapter() @@ -140,37 +140,37 @@ class ResultFragmentTv : ResultFragment() { result_dub_selection.setAdapter() result_recommendations_filter_selection.setAdapter() - observe(viewModel.selectPopup) { popup -> - when (popup) { - is Some.Success -> { - popupDialog?.dismissSafe(activity) + observeNullable(viewModel.selectPopup) { popup -> + if(popup == null) { + popupDialog?.dismissSafe(activity) + popupDialog = null + return@observeNullable + } - popupDialog = activity?.let { act -> - val pop = popup.value - val options = pop.getOptions(act) - val title = pop.getTitle(act) + popupDialog?.dismissSafe(activity) - act.showBottomDialogInstant( - options, title, { - popupDialog = null - pop.callback(null) - }, { - popupDialog = null - pop.callback(it) - } - ) + popupDialog = activity?.let { act -> + val options = popup.getOptions(act) + val title = popup.getTitle(act) + + act.showBottomDialogInstant( + options, title, { + popupDialog = null + popup.callback(null) + }, { + popupDialog = null + popup.callback(it) } - } - is Some.None -> { - popupDialog?.dismissSafe(activity) - popupDialog = null - } + ) } } - observe(viewModel.loadedLinks) { load -> - when (load) { - is Some.Success -> { + observeNullable(viewModel.loadedLinks) { load -> + if(load == null) { + loadingDialog?.dismissSafe(activity) + loadingDialog = null + return@observeNullable + } if (loadingDialog?.isShowing != true) { loadingDialog?.dismissSafe(activity) loadingDialog = null @@ -189,16 +189,11 @@ class ResultFragmentTv : ResultFragment() { builder.show() builder } - } - is Some.None -> { - loadingDialog?.dismissSafe(activity) - loadingDialog = null - } - } + } - observe(viewModel.episodesCountText) { count -> + observeNullable(viewModel.episodesCountText) { count -> result_episodes_text.setText(count) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 46a8c9f6..dbd1dd40 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -145,15 +145,18 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { minute ) } + hours > 0 -> txt( R.string.next_episode_time_hour_format, hours, minute ) + minute > 0 -> txt( R.string.next_episode_time_min_format, minute ) + else -> null }?.also { nextAiringEpisode = txt(R.string.next_episode_format, airing.episode) @@ -305,6 +308,7 @@ fun SelectPopup.getOptions(context: Context): List { is SelectPopup.SelectArray -> { this.options.map { it.first.asString(context) } } + is SelectPopup.SelectText -> options.map { it.asString(context) } } } @@ -352,17 +356,17 @@ class ResultViewModel2 : ViewModel() { MutableLiveData(null) val page: LiveData?> = _page - private val _episodes: MutableLiveData>> = - MutableLiveData(ResourceSome.Loading()) - val episodes: LiveData>> = _episodes + private val _episodes: MutableLiveData>?> = + MutableLiveData(Resource.Loading()) + val episodes: LiveData>?> = _episodes - private val _movie: MutableLiveData>> = - MutableLiveData(ResourceSome.None) - val movie: LiveData>> = _movie + private val _movie: MutableLiveData>?> = + MutableLiveData(null) + val movie: LiveData>?> = _movie - private val _episodesCountText: MutableLiveData> = - MutableLiveData(Some.None) - val episodesCountText: LiveData> = _episodesCountText + private val _episodesCountText: MutableLiveData = + MutableLiveData(null) + val episodesCountText: LiveData = _episodesCountText private val _trailers: MutableLiveData> = MutableLiveData(mutableListOf()) @@ -384,16 +388,16 @@ class ResultViewModel2 : ViewModel() { MutableLiveData(emptyList()) val recommendations: LiveData> = _recommendations - private val _selectedRange: MutableLiveData> = - MutableLiveData(Some.None) - val selectedRange: LiveData> = _selectedRange + private val _selectedRange: MutableLiveData = + MutableLiveData(null) + val selectedRange: LiveData = _selectedRange - private val _selectedSeason: MutableLiveData> = - MutableLiveData(Some.None) - val selectedSeason: LiveData> = _selectedSeason + private val _selectedSeason: MutableLiveData = + MutableLiveData(null) + val selectedSeason: LiveData = _selectedSeason - private val _selectedDubStatus: MutableLiveData> = MutableLiveData(Some.None) - val selectedDubStatus: LiveData> = _selectedDubStatus + private val _selectedDubStatus: MutableLiveData = MutableLiveData(null) + val selectedDubStatus: LiveData = _selectedDubStatus private val _selectedRangeIndex: MutableLiveData = MutableLiveData(-1) @@ -406,12 +410,12 @@ class ResultViewModel2 : ViewModel() { private val _selectedDubStatusIndex: MutableLiveData = MutableLiveData(-1) val selectedDubStatusIndex: LiveData = _selectedDubStatusIndex - private val _loadedLinks: MutableLiveData> = MutableLiveData(Some.None) - val loadedLinks: LiveData> = _loadedLinks + private val _loadedLinks: MutableLiveData = MutableLiveData(null) + val loadedLinks: LiveData = _loadedLinks - private val _resumeWatching: MutableLiveData> = - MutableLiveData(Some.None) - val resumeWatching: LiveData> = _resumeWatching + private val _resumeWatching: MutableLiveData = + MutableLiveData(null) + val resumeWatching: LiveData = _resumeWatching private val _episodeSynopsis: MutableLiveData = MutableLiveData(null) val episodeSynopsis: LiveData = _episodeSynopsis @@ -800,8 +804,8 @@ class ResultViewModel2 : ViewModel() { private val _watchStatus: MutableLiveData = MutableLiveData(WatchType.NONE) val watchStatus: LiveData get() = _watchStatus - private val _selectPopup: MutableLiveData> = MutableLiveData(Some.None) - val selectPopup: LiveData> get() = _selectPopup + private val _selectPopup: MutableLiveData = MutableLiveData(null) + val selectPopup: LiveData = _selectPopup fun updateWatchStatus(status: WatchType) { @@ -885,23 +889,22 @@ class ResultViewModel2 : ViewModel() { } fun cancelLinks() { - println("called::cancelLinks") currentLoadLinkJob?.cancel() currentLoadLinkJob = null - _loadedLinks.postValue(Some.None) + _loadedLinks.postValue(null) } private fun postPopup(text: UiText, options: List, callback: suspend (Int?) -> Unit) { _selectPopup.postValue( - some(SelectPopup.SelectText( + SelectPopup.SelectText( text, options ) { value -> viewModelScope.launchSafe { - _selectPopup.postValue(Some.None) + _selectPopup.postValue(null) callback.invoke(value) } - }) + } ) } @@ -912,15 +915,15 @@ class ResultViewModel2 : ViewModel() { callback: suspend (Int?) -> Unit ) { _selectPopup.postValue( - some(SelectPopup.SelectArray( + SelectPopup.SelectArray( text, options, ) { value -> viewModelScope.launchSafe { - _selectPopup.value = Some.None + _selectPopup.postValue(null) callback.invoke(value) } - }) + } ) } @@ -988,7 +991,7 @@ class ResultViewModel2 : ViewModel() { val subs: MutableSet = mutableSetOf() fun updatePage() { if (isVisible && isActive) { - _loadedLinks.postValue(some(LinkProgress(links.size, subs.size))) + _loadedLinks.postValue(LinkProgress(links.size, subs.size)) } } try { @@ -1005,7 +1008,7 @@ class ResultViewModel2 : ViewModel() { } catch (e: Exception) { logError(e) } finally { - _loadedLinks.postValue(Some.None) + _loadedLinks.postValue(null) } return LinkLoadingResult(sortUrls(links), sortSubs(subs)) @@ -1233,6 +1236,7 @@ class ResultViewModel2 : ViewModel() { ) } } + ACTION_CLICK_DEFAULT -> { activity?.let { ctx -> if (ctx.isConnectedToChromecast()) { @@ -1249,6 +1253,7 @@ class ResultViewModel2 : ViewModel() { } } } + ACTION_SHOW_DESCRIPTION -> { _episodeSynopsis.postValue(click.data.description) } @@ -1286,9 +1291,11 @@ class ResultViewModel2 : ViewModel() { ) } } + ACTION_SHOW_TOAST -> { showToast(activity, R.string.play_episode_toast, Toast.LENGTH_SHORT) } + ACTION_DOWNLOAD_EPISODE -> { val response = currentResponse ?: return downloadEpisode( @@ -1303,6 +1310,7 @@ class ResultViewModel2 : ViewModel() { response.url ) } + ACTION_DOWNLOAD_MIRROR -> { val response = currentResponse ?: return acquireSingleLink( @@ -1332,6 +1340,7 @@ class ResultViewModel2 : ViewModel() { ) } } + ACTION_RELOAD_EPISODE -> { ioSafe { loadLinks( @@ -1342,6 +1351,7 @@ class ResultViewModel2 : ViewModel() { ) } } + ACTION_CHROME_CAST_MIRROR -> { acquireSingleLink( click.data, @@ -1351,6 +1361,7 @@ class ResultViewModel2 : ViewModel() { startChromecast(activity, click.data, result.links, result.subs, index) } } + ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink( click.data, isCasting = true, @@ -1364,6 +1375,7 @@ class ResultViewModel2 : ViewModel() { logError(e) } } + ACTION_COPY_LINK -> { acquireSingleLink( click.data, @@ -1380,9 +1392,11 @@ class ResultViewModel2 : ViewModel() { showToast(act, R.string.copy_link_toast, Toast.LENGTH_SHORT) } } + ACTION_CHROME_CAST_EPISODE -> { startChromecast(activity, click.data) } + ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> { loadLinks(click.data, isVisible = true, isCasting = true) { links -> if (links.links.isEmpty()) { @@ -1397,6 +1411,7 @@ class ResultViewModel2 : ViewModel() { ) } } + ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink( click.data, isCasting = true, @@ -1413,6 +1428,7 @@ class ResultViewModel2 : ViewModel() { result.subs ) } + ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink( click.data, isCasting = true, @@ -1428,6 +1444,7 @@ class ResultViewModel2 : ViewModel() { result.subs ) } + ACTION_PLAY_EPISODE_IN_PLAYER -> { val data = currentResponse?.syncData?.toList() ?: emptyList() val list = @@ -1448,6 +1465,7 @@ class ResultViewModel2 : ViewModel() { ) ) } + ACTION_MARK_AS_WATCHED -> { val isWatched = DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched @@ -1672,10 +1690,10 @@ class ResultViewModel2 : ViewModel() { private fun postMovie() { val response = currentResponse - _episodes.postValue(ResourceSome.None) + _episodes.postValue(null) if (response == null) { - _movie.postValue(ResourceSome.None) + _movie.postValue(null) return } @@ -1692,11 +1710,11 @@ class ResultViewModel2 : ViewModel() { } ) val data = getMovie() - _episodes.postValue(ResourceSome.None) + _episodes.postValue(null) if (text == null || data == null) { - _movie.postValue(ResourceSome.None) + _movie.postValue(null) } else { - _movie.postValue(ResourceSome.Success(text to data)) + _movie.postValue(Resource.Success(text to data)) } } @@ -1705,14 +1723,14 @@ class ResultViewModel2 : ViewModel() { postMovie() } else { _episodes.postValue( - ResourceSome.Success( + Resource.Success( getEpisodes( currentIndex ?: return, currentRange ?: return ) ) ) - _movie.postValue(ResourceSome.None) + _movie.postValue(null) } postResume() } @@ -1755,14 +1773,14 @@ class ResultViewModel2 : ViewModel() { val size = currentEpisodes[indexer]?.size _episodesCountText.postValue( - some( - if (isMovie) null else - txt( - R.string.episode_format, - size, - txt(if (size == 1) R.string.episode else R.string.episodes), - ) - ) + + if (isMovie) null else + txt( + R.string.episode_format, + size, + txt(if (size == 1) R.string.episode else R.string.episodes), + ) + ) _selectedSeasonIndex.postValue( @@ -1770,29 +1788,29 @@ class ResultViewModel2 : ViewModel() { ) _selectedSeason.postValue( - some( - if (isMovie || currentSeasons.size <= 1) null else - when (indexer.season) { - 0 -> txt(R.string.no_season) - else -> { - val seasonNames = (currentResponse as? EpisodeResponse)?.seasonNames - val seasonData = seasonNames.getSeason(indexer.season) - // If displaySeason is null then only show the name! - if (seasonData?.name != null && seasonData.displaySeason == null) { - txt(seasonData.name) - } else { - val suffix = seasonData?.name?.let { " $it" } ?: "" - txt( - R.string.season_format, - txt(R.string.season), - seasonData?.displaySeason ?: indexer.season, - suffix - ) - } + if (isMovie || currentSeasons.size <= 1) null else + when (indexer.season) { + 0 -> txt(R.string.no_season) + else -> { + val seasonNames = (currentResponse as? EpisodeResponse)?.seasonNames + val seasonData = seasonNames.getSeason(indexer.season) + + // If displaySeason is null then only show the name! + if (seasonData?.name != null && seasonData.displaySeason == null) { + txt(seasonData.name) + } else { + val suffix = seasonData?.name?.let { " $it" } ?: "" + txt( + R.string.season_format, + txt(R.string.season), + seasonData?.displaySeason ?: indexer.season, + suffix + ) } } - ) + } + ) _selectedRangeIndex.postValue( @@ -1800,13 +1818,13 @@ class ResultViewModel2 : ViewModel() { ) _selectedRange.postValue( - some( - if (isMovie) null else if ((currentRanges[indexer]?.size ?: 0) > 1) { - txt(R.string.episodes_range, range.startEpisode, range.endEpisode) - } else { - null - } - ) + + if (isMovie) null else if ((currentRanges[indexer]?.size ?: 0) > 1) { + txt(R.string.episodes_range, range.startEpisode, range.endEpisode) + } else { + null + } + ) _selectedDubStatusIndex.postValue( @@ -1814,10 +1832,10 @@ class ResultViewModel2 : ViewModel() { ) _selectedDubStatus.postValue( - some( - if (isMovie || currentDubStatus.size <= 1) null else - txt(indexer.dubStatus) - ) + + if (isMovie || currentDubStatus.size <= 1) null else + txt(indexer.dubStatus) + ) currentId?.let { id -> @@ -1851,7 +1869,7 @@ class ResultViewModel2 : ViewModel() { } }*/ - _episodes.postValue(ResourceSome.Success(ret)) + _episodes.postValue(Resource.Success(ret)) } } @@ -1869,7 +1887,7 @@ class ResultViewModel2 : ViewModel() { } private suspend fun postEpisodes(loadResponse: LoadResponse, updateFillers: Boolean) { - _episodes.postValue(ResourceSome.Loading()) + _episodes.postValue(Resource.Loading()) val mainId = loadResponse.getId() currentId = mainId @@ -1924,6 +1942,7 @@ class ResultViewModel2 : ViewModel() { } episodes } + is TvSeriesLoadResponse -> { val episodes: MutableMap> = mutableMapOf() @@ -1968,6 +1987,7 @@ class ResultViewModel2 : ViewModel() { } episodes } + is MovieLoadResponse -> { singleMap( buildResultEpisode( @@ -1989,6 +2009,7 @@ class ResultViewModel2 : ViewModel() { ) ) } + is LiveStreamLoadResponse -> { singleMap( buildResultEpisode( @@ -2010,6 +2031,7 @@ class ResultViewModel2 : ViewModel() { ) ) } + is TorrentLoadResponse -> { singleMap( buildResultEpisode( @@ -2031,6 +2053,7 @@ class ResultViewModel2 : ViewModel() { ) ) } + else -> { mapOf() } @@ -2088,7 +2111,7 @@ class ResultViewModel2 : ViewModel() { } fun postResume() { - _resumeWatching.postValue(some(resume())) + _resumeWatching.postValue(resume()) } private fun resume(): ResumeWatchingStatus? { @@ -2196,6 +2219,7 @@ class ResultViewModel2 : ViewModel() { } } } + START_ACTION_LOAD_EP -> { val all = currentEpisodes.values.flatten() val episode = @@ -2227,7 +2251,7 @@ class ResultViewModel2 : ViewModel() { ) = ioSafe { _page.postValue(Resource.Loading(url)) - _episodes.postValue(ResourceSome.Loading()) + _episodes.postValue(Resource.Loading()) preferDubStatus = dubStatus currentShowFillers = showFillers @@ -2271,6 +2295,7 @@ class ResultViewModel2 : ViewModel() { is Resource.Failure -> { _page.postValue(data) } + is Resource.Success -> { if (!isActive) return@ioSafe val loadResponse = ioWork { @@ -2307,6 +2332,7 @@ class ResultViewModel2 : ViewModel() { if (!isActive) return@ioSafe handleAutoStart(activity, autostart) } + is Resource.Loading -> { debugException { "Invalid load result" } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt index 2e7ec529..bcf401ea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt @@ -1,12 +1,11 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton -import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.ResultSelectionBinding import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings typealias SelectData = Pair @@ -17,7 +16,9 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter Unit) : RecyclerView.Adapter?) { - setTextHtml(if (text is Some.Success) text.value else null) -} - -fun TextView?.setText(text: Some?) { - setText(if (text is Some.Success) text.value else null) -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt index 649641c8..7fdd6e1d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt @@ -78,7 +78,7 @@ class SearchAdapter( resView: AutofitRecyclerView ) : RecyclerView.ViewHolder(itemView) { - val cardView: ImageView = itemView.imageView + private val cardView: ImageView = itemView.imageView private val compactView = false//itemView.context.getGridIsCompact() private val coverHeight: Int = diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index b4a38216..e0f67d4a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -33,6 +33,8 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent +import com.lagradost.cloudstream3.databinding.FragmentSearchBinding +import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe @@ -56,8 +58,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard -import kotlinx.android.synthetic.main.fragment_search.* -import kotlinx.android.synthetic.main.tvtypes_chips.* import java.util.concurrent.locks.ReentrantLock const val SEARCH_PREF_TAGS = "search_pref_tags" @@ -89,6 +89,7 @@ class SearchFragment : Fragment() { private val searchViewModel: SearchViewModel by activityViewModels() private var bottomSheetDialog: BottomSheetDialog? = null + var binding: FragmentSearchBinding? = null override fun onCreateView( inflater: LayoutInflater, @@ -99,18 +100,20 @@ class SearchFragment : Fragment() { WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE ) bottomSheetDialog?.ownShow() - return inflater.inflate( - if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search, - container, - false - ) + + val layout = if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search + + val root = inflater.inflate(layout, container, false) + binding = FragmentSearchBinding.bind(root) + + return root } private fun fixGrid() { activity?.getSpanCount()?.let { currentSpan = it } - search_autofit_results.spanCount = currentSpan + binding?.searchAutofitResults?.spanCount = currentSpan currentSpan = currentSpan HomeFragment.configEvent.invoke(currentSpan) } @@ -123,6 +126,7 @@ class SearchFragment : Fragment() { override fun onDestroyView() { hideKeyboard() bottomSheetDialog?.ownHide() + binding = null super.onDestroyView() } @@ -181,7 +185,7 @@ class SearchFragment : Fragment() { searchViewModel.reloadRepos() context?.filterProviderByPreferredMedia()?.let { validAPIs -> bindChips( - home_select_group, + binding?.tvtypesChipsScroll?.tvtypesChips, selectedSearchTypes, validAPIs.flatMap { api -> api.supportedTypes }.distinct() ) { list -> @@ -189,7 +193,7 @@ class SearchFragment : Fragment() { setKey(SEARCH_PREF_TAGS, selectedSearchTypes) selectedSearchTypes.clear() selectedSearchTypes.addAll(list) - search(main_search?.query?.toString()) + search(binding?.mainSearch?.query?.toString()) } } } @@ -199,24 +203,27 @@ class SearchFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - context?.fixPaddingStatusbar(searchRoot) + fixPaddingStatusbar(binding?.searchRoot) fixGrid() reloadRepos() - val adapter: RecyclerView.Adapter? = activity?.let { - SearchAdapter( - ArrayList(), - search_autofit_results, - ) { callback -> - SearchHelper.handleSearchClickCallback(activity, callback) - } + binding?.apply { + val adapter: RecyclerView.Adapter? = + SearchAdapter( + ArrayList(), + searchAutofitResults, + ) { callback -> + SearchHelper.handleSearchClickCallback(activity, callback) + } + + + searchAutofitResults.adapter = adapter + searchLoadingBar.alpha = 0f } - search_autofit_results.adapter = adapter - search_loading_bar.alpha = 0f val searchExitIcon = - main_search.findViewById(androidx.appcompat.R.id.search_close_btn) + binding?.mainSearch?.findViewById(androidx.appcompat.R.id.search_close_btn) // val searchMagIcon = // main_search.findViewById(androidx.appcompat.R.id.search_mag_icon) //searchMagIcon.scaleX = 0.65f @@ -230,7 +237,7 @@ class SearchFragment : Fragment() { )!!.toMutableSet() } - search_filter.setOnClickListener { searchView -> + binding?.searchFilter?.setOnClickListener { searchView -> searchView?.context?.let { ctx -> val validAPIs = ctx.filterProviderByPreferredMedia(hasHomePageIsRequired = false) var currentValidApis = listOf() @@ -241,7 +248,13 @@ class SearchFragment : Fragment() { BottomSheetDialog(ctx) builder.behavior.state = BottomSheetBehavior.STATE_EXPANDED - builder.setContentView(R.layout.home_select_mainpage) + + val binding: HomeSelectMainpageBinding = HomeSelectMainpageBinding.inflate( + builder.layoutInflater, + null, + false + ) + builder.setContentView(binding.root) builder.show() builder.let { dialog -> val isMultiLang = ctx.getApiProviderLangSettings().let { set -> @@ -303,7 +316,7 @@ class SearchFragment : Fragment() { ?: mutableListOf(TvType.Movie, TvType.TvSeries) bindChips( - dialog.home_select_group, + binding.tvtypesChipsScroll.tvtypesChips, selectedSearchTypes, TvType.values().toList() ) { list -> @@ -343,15 +356,15 @@ class SearchFragment : Fragment() { ?: mutableListOf(TvType.Movie, TvType.TvSeries) if (isTrueTvSettings()) { - search_filter.isFocusable = true - search_filter.isFocusableInTouchMode = true + binding?.searchFilter?.isFocusable = true + binding?.searchFilter?.isFocusableInTouchMode = true } - main_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { search(query) - main_search?.let { + binding?.mainSearch?.let { hideKeyboard(it) } @@ -365,17 +378,17 @@ class SearchFragment : Fragment() { searchViewModel.clearSearch() searchViewModel.updateHistory() } - - search_history_holder?.isVisible = showHistory - - search_master_recycler?.isVisible = !showHistory && isAdvancedSearch - search_autofit_results?.isVisible = !showHistory && !isAdvancedSearch + binding?.apply { + searchHistoryHolder.isVisible = showHistory + searchMasterRecycler.isVisible = !showHistory && isAdvancedSearch + searchAutofitResults.isVisible = !showHistory && !isAdvancedSearch + } return true } }) - search_clear_call_history?.setOnClickListener { + binding?.searchClearCallHistory?.setOnClickListener { activity?.let { ctx -> val builder: AlertDialog.Builder = AlertDialog.Builder(ctx) val dialogClickListener = @@ -409,8 +422,8 @@ class SearchFragment : Fragment() { } observe(searchViewModel.currentHistory) { list -> - search_clear_call_history?.isVisible = list.isNotEmpty() - (search_history_recycler.adapter as? SearchHistoryAdaptor?)?.updateList(list) + binding?.searchClearCallHistory?.isVisible = list.isNotEmpty() + (binding?.searchHistoryRecycler?.adapter as? SearchHistoryAdaptor?)?.updateList(list) } searchViewModel.updateHistory() @@ -420,20 +433,20 @@ class SearchFragment : Fragment() { is Resource.Success -> { it.value.let { data -> if (data.isNotEmpty()) { - (search_autofit_results?.adapter as? SearchAdapter)?.updateList(data) + (binding?.searchAutofitResults?.adapter as? SearchAdapter)?.updateList(data) } } - searchExitIcon.alpha = 1f - search_loading_bar.alpha = 0f + searchExitIcon?.alpha = 1f + binding?.searchLoadingBar?.alpha = 0f } is Resource.Failure -> { // Toast.makeText(activity, "Server error", Toast.LENGTH_LONG).show() - searchExitIcon.alpha = 1f - search_loading_bar.alpha = 0f + searchExitIcon?.alpha = 1f + binding?.searchLoadingBar?.alpha = 0f } is Resource.Loading -> { - searchExitIcon.alpha = 0f - search_loading_bar.alpha = 1f + searchExitIcon?.alpha = 0f + binding?.searchLoadingBar?.alpha = 1f } } } @@ -443,7 +456,7 @@ class SearchFragment : Fragment() { try { // https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist listLock.lock() - (search_master_recycler?.adapter as ParentItemAdapter?)?.apply { + (binding?.searchMasterRecycler?.adapter as ParentItemAdapter?)?.apply { val newItems = list.map { ongoing -> val dataList = if (ongoing.data is Resource.Success) ongoing.data.value else ArrayList() @@ -490,8 +503,8 @@ class SearchFragment : Fragment() { SEARCH_HISTORY_OPEN -> { searchViewModel.clearSearch() if (searchItem.type.isNotEmpty()) - updateChips(home_select_group, searchItem.type.toMutableList()) - main_search?.setQuery(searchItem.searchText, true) + updateChips(binding?.tvtypesChipsScroll?.tvtypesChips, searchItem.type.toMutableList()) + binding?.mainSearch?.setQuery(searchItem.searchText, true) } SEARCH_HISTORY_REMOVE -> { removeKey(SEARCH_HISTORY_KEY, searchItem.key) @@ -503,20 +516,23 @@ class SearchFragment : Fragment() { } } - search_history_recycler?.adapter = historyAdapter - search_history_recycler?.layoutManager = GridLayoutManager(context, 1) + binding?.apply { + searchHistoryRecycler.adapter = historyAdapter + searchHistoryRecycler.layoutManager = GridLayoutManager(context, 1) - search_master_recycler?.adapter = masterAdapter - search_master_recycler?.layoutManager = GridLayoutManager(context, 1) + searchMasterRecycler.adapter = masterAdapter + searchMasterRecycler.layoutManager = GridLayoutManager(context, 1) - // Automatically search the specified query, this allows the app search to launch from intent - arguments?.getString(SEARCH_QUERY)?.let { query -> - if (query.isBlank()) return@let - main_search?.setQuery(query, true) - // Clear the query as to not make it request the same query every time the page is opened - arguments?.putString(SEARCH_QUERY, null) + // Automatically search the specified query, this allows the app search to launch from intent + arguments?.getString(SEARCH_QUERY)?.let { query -> + if (query.isBlank()) return@let + mainSearch.setQuery(query, true) + // Clear the query as to not make it request the same query every time the page is opened + arguments?.putString(SEARCH_QUERY, null) + } } + // SubtitlesFragment.push(activity) //searchViewModel.search("iron man") //(activity as AppCompatActivity).loadResult("https://shiro.is/overlord-dubbed", "overlord-dubbed", "Shiro") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt index 8132301b..0a2ecb81 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt @@ -10,7 +10,8 @@ import androidx.recyclerview.widget.RecyclerView import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType -import kotlinx.android.synthetic.main.search_history_item.view.* +import com.lagradost.cloudstream3.databinding.AccountSingleBinding +import com.lagradost.cloudstream3.databinding.SearchHistoryItemBinding data class SearchHistoryItem( @JsonProperty("searchedAt") val searchedAt: Long, @@ -34,8 +35,7 @@ class SearchHistoryAdaptor( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return CardViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.search_history_item, parent, false), + SearchHistoryItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), clickCallback, ) } @@ -65,22 +65,24 @@ class SearchHistoryAdaptor( class CardViewHolder constructor( - itemView: View, + val binding: SearchHistoryItemBinding, private val clickCallback: (SearchHistoryCallback) -> Unit, ) : - RecyclerView.ViewHolder(itemView) { - private val removeButton: ImageView = itemView.home_history_remove - private val openButton: View = itemView.home_history_tab - private val title: TextView = itemView.home_history_title + RecyclerView.ViewHolder(binding.root) { + // private val removeButton: ImageView = itemView.home_history_remove + // private val openButton: View = itemView.home_history_tab + // private val title: TextView = itemView.home_history_title fun bind(card: SearchHistoryItem) { - title.text = card.searchText + binding.apply { + homeHistoryTitle.text = card.searchText - removeButton.setOnClickListener { - clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_REMOVE)) - } - openButton.setOnClickListener { - clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_OPEN)) + homeHistoryRemove.setOnClickListener { + clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_REMOVE)) + } + homeHistoryTab.setOnClickListener { + clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_OPEN)) + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt index 3447ee32..69812f22 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt @@ -1,7 +1,6 @@ package com.lagradost.cloudstream3.ui.search import android.content.Context -import android.graphics.drawable.Drawable import android.view.View import android.widget.ImageView import android.widget.ProgressBar @@ -10,14 +9,29 @@ import androidx.cardview.widget.CardView import androidx.core.view.isVisible import androidx.palette.graphics.Palette import androidx.preference.PreferenceManager -import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.AnimeSearchResponse +import com.lagradost.cloudstream3.DubStatus +import com.lagradost.cloudstream3.LiveSearchResponse +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SearchQuality +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.isMovieType import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.home_result_grid.view.* +import kotlinx.android.synthetic.main.home_result_grid.view.background_card +import kotlinx.android.synthetic.main.home_result_grid.view.imageText +import kotlinx.android.synthetic.main.home_result_grid.view.imageView +import kotlinx.android.synthetic.main.home_result_grid.view.search_item_download_play +import kotlinx.android.synthetic.main.home_result_grid.view.text_flag +import kotlinx.android.synthetic.main.home_result_grid.view.text_is_dub +import kotlinx.android.synthetic.main.home_result_grid.view.text_is_sub +import kotlinx.android.synthetic.main.home_result_grid.view.text_quality +import kotlinx.android.synthetic.main.home_result_grid.view.title_shadow +import kotlinx.android.synthetic.main.home_result_grid.view.watchProgress object SearchResultBuilder { private val showCache: MutableMap = mutableMapOf() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt index e879f0df..1dc79dc0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt @@ -3,11 +3,10 @@ package com.lagradost.cloudstream3.ui.settings import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.AccountSingleBinding import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.utils.UIHelper.setImage @@ -15,14 +14,15 @@ class AccountClickCallback(val action: Int, val view: View, val card: AuthAPI.Lo class AccountAdapter( val cardList: List, - val layout: Int = R.layout.account_single, private val clickCallback: (AccountClickCallback) -> Unit ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return CardViewHolder( - LayoutInflater.from(parent.context).inflate(layout, parent, false), clickCallback + AccountSingleBinding.inflate(LayoutInflater.from(parent.context), parent, false), //LayoutInflater.from(parent.context).inflate(layout, parent, false), + + clickCallback ) } @@ -43,18 +43,18 @@ class AccountAdapter( } class CardViewHolder - constructor(itemView: View, private val clickCallback: (AccountClickCallback) -> Unit) : - RecyclerView.ViewHolder(itemView) { - private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!! - private val accountName: TextView = itemView.findViewById(R.id.account_name)!! + constructor(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) : + RecyclerView.ViewHolder(binding.root) { + // private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!! + // private val accountName: TextView = itemView.findViewById(R.id.account_name)!! fun bind(card: AuthAPI.LoginInfo) { // just in case name is null account index will show, should never happened - accountName.text = card.name ?: "%s %d".format( - accountName.context.getString(R.string.account), + binding.accountName.text = card.name ?: "%s %d".format( + binding.accountName.context.getString(R.string.account), card.accountIndex ) - pfp.isVisible = pfp.setImage(card.profilePicture) + binding.accountProfilePicture.isVisible = binding.accountProfilePicture.setImage(card.profilePicture) itemView.setOnClickListener { clickCallback.invoke(AccountClickCallback(0, itemView, card)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 1ef3cb55..acf715b3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -96,7 +96,7 @@ class SettingsAccount : PreferenceFragmentCompat() { } } api.accountIndex = ogIndex - val adapter = AccountAdapter(items, R.layout.account_single) { + val adapter = AccountAdapter(items) { dialog?.dismissSafe(activity) api.changeAccount(it.card.accountIndex) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 40c996cc..453f93be 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -62,7 +62,7 @@ class SettingsFragment : Fragment() { activity?.onBackPressed() } } - context.fixPaddingStatusbar(settings_toolbar) + fixPaddingStatusbar(settings_toolbar) } fun Fragment?.setUpToolbar(@StringRes title: Int) { @@ -74,7 +74,7 @@ class SettingsFragment : Fragment() { activity?.onBackPressed() } } - context.fixPaddingStatusbar(settings_toolbar) + fixPaddingStatusbar(settings_toolbar) } fun getFolderSize(dir: File): Long { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 045ed92d..75ff8305 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -18,8 +18,8 @@ import androidx.navigation.fragment.findNavController import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.mvvm.Some import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings @@ -97,6 +97,7 @@ class ExtensionsFragment : Fragment() { extensionViewModel.loadRepositories() } } + DialogInterface.BUTTON_NEGATIVE -> {} } } @@ -138,29 +139,26 @@ class ExtensionsFragment : Fragment() { // } // } - observe(extensionViewModel.pluginStats) { - when (it) { - is Some.Success -> { - val value = it.value + observeNullable(extensionViewModel.pluginStats) { value -> + if (value == null) { + plugin_storage_appbar?.isVisible = false - plugin_storage_appbar?.isVisible = true - if (value.total == 0) { - plugin_download?.setLayoutWidth(1) - plugin_disabled?.setLayoutWidth(0) - plugin_not_downloaded?.setLayoutWidth(0) - } else { - plugin_download?.setLayoutWidth(value.downloaded) - plugin_disabled?.setLayoutWidth(value.disabled) - plugin_not_downloaded?.setLayoutWidth(value.notDownloaded) - } - plugin_not_downloaded_txt.setText(value.notDownloadedText) - plugin_disabled_txt.setText(value.disabledText) - plugin_download_txt.setText(value.downloadedText) - } - is Some.None -> { - plugin_storage_appbar?.isVisible = false - } + return@observeNullable } + + plugin_storage_appbar?.isVisible = true + if (value.total == 0) { + plugin_download?.setLayoutWidth(1) + plugin_disabled?.setLayoutWidth(0) + plugin_not_downloaded?.setLayoutWidth(0) + } else { + plugin_download?.setLayoutWidth(value.downloaded) + plugin_disabled?.setLayoutWidth(value.disabled) + plugin_not_downloaded?.setLayoutWidth(value.notDownloaded) + } + plugin_not_downloaded_txt.setText(value.notDownloadedText) + plugin_disabled_txt.setText(value.disabledText) + plugin_download_txt.setText(value.downloadedText) } plugin_storage_appbar?.setOnClickListener { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt index 63ed5357..866d167c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt @@ -7,7 +7,6 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.amap -import com.lagradost.cloudstream3.mvvm.Some import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager.getPluginsOnline @@ -40,8 +39,8 @@ class ExtensionsViewModel : ViewModel() { private val _repositories = MutableLiveData>() val repositories: LiveData> = _repositories - private val _pluginStats: MutableLiveData> = MutableLiveData(Some.None) - val pluginStats: LiveData> = _pluginStats + private val _pluginStats: MutableLiveData = MutableLiveData(null) + val pluginStats: LiveData = _pluginStats //TODO CACHE GET REQUESTS // DO not use viewModelScope.launchSafe, it will ANR on slow internet @@ -78,7 +77,7 @@ class ExtensionsViewModel : ViewModel() { debugAssert({ stats.downloaded + stats.notDownloaded + stats.disabled != stats.total }) { "downloaded(${stats.downloaded}) + notDownloaded(${stats.notDownloaded}) + disabled(${stats.disabled}) != total(${stats.total})" } - _pluginStats.postValue(Some.Success(stats)) + _pluginStats.postValue(stats) } private fun repos() = (getKey>(REPOSITORIES_KEY) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt index d328d226..1a6215db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt @@ -12,6 +12,7 @@ import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.databinding.FragmentPluginsBinding import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings @@ -20,9 +21,6 @@ import com.lagradost.cloudstream3.ui.settings.appLanguages import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.android.synthetic.main.fragment_plugins.* -import kotlinx.android.synthetic.main.tvtypes_chips.* -import kotlinx.android.synthetic.main.tvtypes_chips_scroll.* const val PLUGINS_BUNDLE_NAME = "name" const val PLUGINS_BUNDLE_URL = "url" @@ -33,11 +31,19 @@ class PluginsFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? { - return inflater.inflate(R.layout.fragment_plugins, container, false) + ): View { + val localBinding = FragmentPluginsBinding.inflate(inflater,container,false) + binding = localBinding + return localBinding.root//inflater.inflate(R.layout.fragment_plugins, container, false) + } + + override fun onDestroyView() { + binding = null + super.onDestroyView() } private val pluginViewModel: PluginsViewModel by activityViewModels() + var binding: FragmentPluginsBinding? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -66,8 +72,8 @@ class PluginsFragment : Fragment() { } setUpToolbar(name) - - settings_toolbar?.setOnMenuItemClickListener { menuItem -> + binding?.settingsToolbar?.apply { + setOnMenuItemClickListener { menuItem -> when (menuItem?.itemId) { R.id.download_all -> { PluginsViewModel.downloadAll(activity, url, pluginViewModel) @@ -99,67 +105,69 @@ class PluginsFragment : Fragment() { } val searchView = - settings_toolbar?.menu?.findItem(R.id.search_button)?.actionView as? SearchView + menu?.findItem(R.id.search_button)?.actionView as? SearchView // Don't go back if active query - settings_toolbar?.setNavigationOnClickListener { + setNavigationOnClickListener { if (searchView?.isIconified == false) { searchView.isIconified = true } else { activity?.onBackPressed() } } + searchView?.setOnQueryTextFocusChangeListener { _, hasFocus -> + if (!hasFocus) pluginViewModel.search(null) + } + searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + pluginViewModel.search(query) + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + pluginViewModel.search(newText) + return true + } + }) + } // searchView?.onActionViewCollapsed = { // pluginViewModel.search(null) // } // Because onActionViewCollapsed doesn't wanna work we need this workaround :( - searchView?.setOnQueryTextFocusChangeListener { _, hasFocus -> - if (!hasFocus) pluginViewModel.search(null) - } - - searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String?): Boolean { - pluginViewModel.search(query) - return true - } - - override fun onQueryTextChange(newText: String?): Boolean { - pluginViewModel.search(newText) - return true - } - }) - plugin_recycler_view?.adapter = + + binding?.pluginRecyclerView?.adapter = PluginAdapter { pluginViewModel.handlePluginAction(activity, url, it, isLocal) } if (isTvSettings()) { // Scrolling down does not reveal the whole RecyclerView on TV, add to bypass that. - plugin_recycler_view?.setPadding(0, 0, 0, 200.toPx) + binding?.pluginRecyclerView?.setPadding(0, 0, 0, 200.toPx) } observe(pluginViewModel.filteredPlugins) { (scrollToTop, list) -> - (plugin_recycler_view?.adapter as? PluginAdapter)?.updateList(list) + (binding?.pluginRecyclerView?.adapter as? PluginAdapter)?.updateList(list) if (scrollToTop) - plugin_recycler_view?.scrollToPosition(0) + binding?.pluginRecyclerView?.scrollToPosition(0) } if (isLocal) { // No download button and no categories on local - settings_toolbar?.menu?.findItem(R.id.download_all)?.isVisible = false - settings_toolbar?.menu?.findItem(R.id.lang_filter)?.isVisible = false + binding?.settingsToolbar?.menu?.findItem(R.id.download_all)?.isVisible = false + binding?.settingsToolbar?.menu?.findItem(R.id.lang_filter)?.isVisible = false pluginViewModel.updatePluginListLocal() - tv_types_scroll_view?.isVisible = false + + binding?.tvtypesChipsScroll?.root?.isVisible = false } else { pluginViewModel.updatePluginList(context, url) - tv_types_scroll_view?.isVisible = true + binding?.tvtypesChipsScroll?.root?.isVisible = true - bindChips(home_select_group, emptyList(), TvType.values().toList()) { list -> + bindChips(binding?.tvtypesChipsScroll?.tvtypesChips, emptyList(), TvType.values().toList()) { list -> pluginViewModel.tvTypes.clear() pluginViewModel.tvTypes.addAll(list.map { it.name }) pluginViewModel.updateFilteredPlugins() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt index b7d2fff6..138a31a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt @@ -8,21 +8,16 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import com.lagradost.cloudstream3.APIHolder.apis -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings -import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentSetupExtensionsBinding +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel import com.lagradost.cloudstream3.ui.settings.extensions.RepoAdapter import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import kotlinx.android.synthetic.main.fragment_extensions.blank_repo_screen -import kotlinx.android.synthetic.main.fragment_extensions.repo_recycler_view -import kotlinx.android.synthetic.main.fragment_setup_media.next_btt -import kotlinx.android.synthetic.main.fragment_setup_media.prev_btt -import kotlinx.android.synthetic.main.fragment_setup_media.setup_root class SetupFragmentExtensions : Fragment() { @@ -39,13 +34,24 @@ class SetupFragmentExtensions : Fragment() { } } + var binding: FragmentSetupExtensionsBinding? = null + + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_setup_extensions, container, false) + ): View { + val localBinding = FragmentSetupExtensionsBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + //return inflater.inflate(R.layout.fragment_setup_extensions, container, false) } + override fun onResume() { super.onResume() afterRepositoryLoadedEvent += ::setRepositories @@ -60,12 +66,12 @@ class SetupFragmentExtensions : Fragment() { main { val repositories = RepositoryManager.getRepositories() + PREBUILT_REPOSITORIES val hasRepos = repositories.isNotEmpty() - repo_recycler_view?.isVisible = hasRepos - blank_repo_screen?.isVisible = !hasRepos + binding?.repoRecyclerView?.isVisible = hasRepos + binding?.blankRepoScreen?.isVisible = !hasRepos // view_public_repositories_button?.isVisible = hasRepos if (hasRepos) { - repo_recycler_view?.adapter = RepoAdapter(true, {}, { + binding?.repoRecyclerView?.adapter = RepoAdapter(true, {}, { PluginsViewModel.downloadAll(activity, it.url, null) }).apply { updateList(repositories) } } @@ -80,39 +86,40 @@ class SetupFragmentExtensions : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - context?.fixPaddingStatusbar(setup_root) + fixPaddingStatusbar(binding?.setupRoot) val isSetup = arguments?.getBoolean(SETUP_EXTENSION_BUNDLE_IS_SETUP) ?: false // view_public_repositories_button?.setOnClickListener { // openBrowser(PUBLIC_REPOSITORIES_LIST, isTvSettings(), this) // } - with(context) { - if (this == null) return + normalSafeApiCall { + // val ctx = context ?: return@normalSafeApiCall setRepositories() + binding?.apply { + if (!isSetup) { + nextBtt.setText(R.string.setup_done) + } + prevBtt.isVisible = isSetup - if (!isSetup) { - next_btt.setText(R.string.setup_done) - } - prev_btt?.isVisible = isSetup + nextBtt.setOnClickListener { + // Continue setup + if (isSetup) + if ( + // If any available languages + apis.distinctBy { it.lang }.size > 1 + ) { + findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages) + } else { + findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_media) + } + else + findNavController().navigate(R.id.navigation_home) + } - next_btt?.setOnClickListener { - // Continue setup - if (isSetup) - if ( - // If any available languages - apis.distinctBy { it.lang }.size > 1 - ) { - findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages) - } else { - findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_media) - } - else - findNavController().navigate(R.id.navigation_home) - } - - prev_btt?.setOnClickListener { - findNavController().navigate(R.id.navigation_setup_language) + prevBtt.setOnClickListener { + findNavController().navigate(R.id.navigation_setup_language) + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt index 80db59ee..5c473b73 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt @@ -13,40 +13,49 @@ import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentSetupLanguageBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.ui.settings.appLanguages import com.lagradost.cloudstream3.ui.settings.getCurrentLocale import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import kotlinx.android.synthetic.main.fragment_setup_language.* -import kotlinx.android.synthetic.main.fragment_setup_media.listview1 -import kotlinx.android.synthetic.main.fragment_setup_media.next_btt const val HAS_DONE_SETUP_KEY = "HAS_DONE_SETUP" class SetupFragmentLanguage : Fragment() { + var binding: FragmentSetupLanguageBinding? = null + + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_setup_language, container, false) + ): View { + val localBinding = FragmentSetupLanguageBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + //return inflater.inflate(R.layout.fragment_setup_language, container, false) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - context?.fixPaddingStatusbar(setup_root) // We don't want a crash for all users normalSafeApiCall { - with(context) { - if (this == null) return@normalSafeApiCall - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + fixPaddingStatusbar(binding?.setupRoot) - val arrayAdapter = - ArrayAdapter(this, R.layout.sort_bottom_single_choice) + val ctx = context ?: return@normalSafeApiCall + val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) + val arrayAdapter = + ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) + + binding?.apply { // Icons may crash on some weird android versions? normalSafeApiCall { val drawable = when { @@ -54,10 +63,10 @@ class SetupFragmentLanguage : Fragment() { BuildConfig.BUILD_TYPE == "prerelease" -> R.drawable.cloud_2_gradient_beta else -> R.drawable.cloud_2_gradient } - app_icon_image?.setImageDrawable(ContextCompat.getDrawable(this, drawable)) + appIconImage.setImageDrawable(ContextCompat.getDrawable(ctx, drawable)) } - val current = getCurrentLocale(this) + val current = getCurrentLocale(ctx) val languageCodes = appLanguages.map { it.third } val languageNames = appLanguages.map { (emoji, name, iso) -> val flag = emoji.ifBlank { SubtitleHelper.getFlagFromIso(iso) ?: "ERROR" } @@ -66,18 +75,19 @@ class SetupFragmentLanguage : Fragment() { val index = languageCodes.indexOf(current) arrayAdapter.addAll(languageNames) - listview1?.adapter = arrayAdapter - listview1?.choiceMode = AbsListView.CHOICE_MODE_SINGLE - listview1?.setItemChecked(index, true) + listview1.adapter = arrayAdapter + listview1.choiceMode = AbsListView.CHOICE_MODE_SINGLE + listview1.setItemChecked(index, true) - listview1?.setOnItemClickListener { _, _, position, _ -> + listview1.setOnItemClickListener { _, _, position, _ -> val code = languageCodes[position] CommonActivity.setLocale(activity, code) - settingsManager.edit().putString(getString(R.string.locale_key), code).apply() + settingsManager.edit().putString(getString(R.string.locale_key), code) + .apply() activity?.recreate() } - next_btt?.setOnClickListener { + nextBtt.setOnClickListener { // If no plugins go to plugins page val nextDestination = if ( PluginManager.getPluginsOnline().isEmpty() @@ -92,10 +102,11 @@ class SetupFragmentLanguage : Fragment() { ) } - skip_btt?.setOnClickListener { + skipBtt.setOnClickListener { findNavController().navigate(R.id.navigation_home) } } + } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt index 50fb37d6..98803818 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt @@ -10,30 +10,39 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentSetupLayoutBinding +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import kotlinx.android.synthetic.main.fragment_setup_layout.* -import kotlinx.android.synthetic.main.fragment_setup_media.listview1 -import kotlinx.android.synthetic.main.fragment_setup_media.next_btt -import kotlinx.android.synthetic.main.fragment_setup_media.prev_btt -import kotlinx.android.synthetic.main.fragment_setup_media.setup_root import org.acra.ACRA class SetupFragmentLayout : Fragment() { + + var binding: FragmentSetupLayoutBinding? = null + + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_setup_layout, container, false) + ): View { + val localBinding = FragmentSetupLayoutBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + //return inflater.inflate(R.layout.fragment_setup_layout, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - context?.fixPaddingStatusbar(setup_root) + fixPaddingStatusbar(binding?.setupRoot) - with(context) { - if (this == null) return - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + normalSafeApiCall { + val ctx = context ?: return@normalSafeApiCall + + val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) val prefNames = resources.getStringArray(R.array.app_layout) val prefValues = resources.getIntArray(R.array.app_layout_values) @@ -42,48 +51,48 @@ class SetupFragmentLayout : Fragment() { settingsManager.getInt(getString(R.string.app_layout_key), -1) val arrayAdapter = - ArrayAdapter(this, R.layout.sort_bottom_single_choice) + ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) arrayAdapter.addAll(prefNames.toList()) - listview1?.adapter = arrayAdapter - listview1?.choiceMode = AbsListView.CHOICE_MODE_SINGLE - listview1?.setItemChecked( - prefValues.indexOf(currentLayout), true - ) - - listview1?.setOnItemClickListener { _, _, position, _ -> - settingsManager.edit() - .putInt(getString(R.string.app_layout_key), prefValues[position]) - .apply() - activity?.recreate() - } - - acra_switch?.setOnCheckedChangeListener { _, enableCrashReporting -> - // Use same pref as in settings - settingsManager.edit().putBoolean(ACRA.PREF_DISABLE_ACRA, !enableCrashReporting) - .apply() - val text = - if (enableCrashReporting) R.string.bug_report_settings_off else R.string.bug_report_settings_on - crash_reporting_text?.text = getText(text) - } - - val enableCrashReporting = !settingsManager.getBoolean(ACRA.PREF_DISABLE_ACRA, true) - acra_switch.isChecked = enableCrashReporting - crash_reporting_text.text = - getText( - if (enableCrashReporting) R.string.bug_report_settings_off else R.string.bug_report_settings_on + binding?.apply { + listview1.adapter = arrayAdapter + listview1.choiceMode = AbsListView.CHOICE_MODE_SINGLE + listview1.setItemChecked( + prefValues.indexOf(currentLayout), true ) + listview1.setOnItemClickListener { _, _, position, _ -> + settingsManager.edit() + .putInt(getString(R.string.app_layout_key), prefValues[position]) + .apply() + activity?.recreate() + } + acraSwitch.setOnCheckedChangeListener { _, enableCrashReporting -> + // Use same pref as in settings + settingsManager.edit().putBoolean(ACRA.PREF_DISABLE_ACRA, !enableCrashReporting) + .apply() + val text = + if (enableCrashReporting) R.string.bug_report_settings_off else R.string.bug_report_settings_on + crashReportingText.text = getText(text) + } - next_btt?.setOnClickListener { - findNavController().navigate(R.id.navigation_home) - } + val enableCrashReporting = !settingsManager.getBoolean(ACRA.PREF_DISABLE_ACRA, true) - prev_btt?.setOnClickListener { - findNavController().popBackStack() + acraSwitch.isChecked = enableCrashReporting + crashReportingText.text = + getText( + if (enableCrashReporting) R.string.bug_report_settings_off else R.string.bug_report_settings_on + ) + + + nextBtt.setOnClickListener { + findNavController().navigate(R.id.navigation_home) + } + + prevBtt.setOnClickListener { + findNavController().popBackStack() + } } } } - - } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt index 257ce5c1..6916cafe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt @@ -10,72 +10,85 @@ import androidx.core.util.forEach import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.utils.DataStore.removeKey -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API +import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import kotlinx.android.synthetic.main.fragment_setup_media.* +import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API class SetupFragmentMedia : Fragment() { + var binding: FragmentSetupMediaBinding? = null + + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_setup_media, container, false) + ): View { + val localBinding = FragmentSetupMediaBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + //return inflater.inflate(R.layout.fragment_setup_media, container, false) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - context?.fixPaddingStatusbar(setup_root) + normalSafeApiCall { + fixPaddingStatusbar(binding?.setupRoot) - with(context) { - if (this == null) return - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val ctx = context ?: return@normalSafeApiCall + val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) val arrayAdapter = - ArrayAdapter(this, R.layout.sort_bottom_single_choice) + ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) val names = enumValues().sorted().map { it.name } val selected = mutableListOf() arrayAdapter.addAll(names) - listview1?.let { - it.adapter = arrayAdapter - it.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE + binding?.apply { + listview1.let { + it.adapter = arrayAdapter + it.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE - it.setOnItemClickListener { _, _, _, _ -> - it.checkedItemPositions?.forEach { key, value -> - if (value) { - selected.add(key) - } else { - selected.remove(key) + it.setOnItemClickListener { _, _, _, _ -> + it.checkedItemPositions?.forEach { key, value -> + if (value) { + selected.add(key) + } else { + selected.remove(key) + } } + val prefValues = selected.mapNotNull { pos -> + val item = + it.getItemAtPosition(pos)?.toString() ?: return@mapNotNull null + val itemVal = TvType.valueOf(item) + itemVal.ordinal.toString() + }.toSet() + settingsManager.edit() + .putStringSet(getString(R.string.prefer_media_type_key), prefValues) + .apply() + + // Regenerate set homepage + removeKey(USER_SELECTED_HOMEPAGE_API) } - val prefValues = selected.mapNotNull { pos -> - val item = it.getItemAtPosition(pos)?.toString() ?: return@mapNotNull null - val itemVal = TvType.valueOf(item) - itemVal.ordinal.toString() - }.toSet() - settingsManager.edit() - .putStringSet(getString(R.string.prefer_media_type_key), prefValues) - .apply() - - // Regenerate set homepage - removeKey(USER_SELECTED_HOMEPAGE_API) } - } - next_btt?.setOnClickListener { - findNavController().navigate(R.id.navigation_setup_media_to_navigation_setup_layout) - } + nextBtt.setOnClickListener { + findNavController().navigate(R.id.navigation_setup_media_to_navigation_setup_layout) + } - prev_btt?.setOnClickListener { - findNavController().popBackStack() + prevBtt.setOnClickListener { + findNavController().popBackStack() + } } } } - - } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt index 51abee90..8637fc99 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt @@ -14,31 +14,43 @@ import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentSetupProviderLanguagesBinding +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import kotlinx.android.synthetic.main.fragment_setup_media.* class SetupFragmentProviderLanguage : Fragment() { + var binding: FragmentSetupProviderLanguagesBinding? = null + + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_setup_provider_languages, container, false) + ): View { + val localBinding = FragmentSetupProviderLanguagesBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + //return inflater.inflate(R.layout.fragment_setup_provider_languages, container, false) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - context?.fixPaddingStatusbar(setup_root) + fixPaddingStatusbar(binding?.setupRoot) - with(context) { - if (this == null) return - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + normalSafeApiCall { + val ctx = context ?: return@normalSafeApiCall + + val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) val arrayAdapter = - ArrayAdapter(this, R.layout.sort_bottom_single_choice) + ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) - val current = this.getApiProviderLangSettings() + val current = ctx.getApiProviderLangSettings() val langs = APIHolder.apis.map { it.lang }.toSet() .sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName @@ -56,31 +68,31 @@ class SetupFragmentProviderLanguage : Fragment() { } arrayAdapter.addAll(languageNames) - - listview1?.adapter = arrayAdapter - listview1?.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE + binding?.apply { + listview1.adapter = arrayAdapter + listview1.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE currentList.forEach { listview1.setItemChecked(it, true) } - listview1?.setOnItemClickListener { _, _, _, _ -> + listview1.setOnItemClickListener { _, _, _, _ -> val currentLanguages = mutableListOf() - listview1?.checkedItemPositions?.forEach { key, value -> + listview1.checkedItemPositions?.forEach { key, value -> if (value) currentLanguages.add(langs[key]) } settingsManager.edit().putStringSet( - this.getString(R.string.provider_lang_key), + ctx.getString(R.string.provider_lang_key), currentLanguages.toSet() ).apply() } - next_btt?.setOnClickListener { + nextBtt.setOnClickListener { findNavController().navigate(R.id.navigation_setup_provider_languages_to_navigation_setup_media) } - prev_btt?.setOnClickListener { + prevBtt.setOnClickListener { findNavController().popBackStack() - } + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt index 83d134cb..40bf8417 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt @@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.ChromecastSubtitleSettingsBinding import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.Event @@ -31,7 +32,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage -import kotlinx.android.synthetic.main.subtitle_settings.* const val CHROME_SUBTITLE_KEY = "chome_subtitle_settings" @@ -137,12 +137,21 @@ class ChromecastSubtitlesFragment : Fragment() { //subtitle_text?.setStyle(fromSaveToStyle(state)) } + var binding : ChromecastSubtitleSettingsBinding? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? { - return inflater.inflate(R.layout.chromecast_subtitle_settings, container, false) + ): View { + val localBinding = ChromecastSubtitleSettingsBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root//inflater.inflate(R.layout.chromecast_subtitle_settings, container, false) + } + + override fun onDestroyView() { + binding = null + super.onDestroyView() } private lateinit var state: SaveChromeCaptionStyle @@ -159,7 +168,7 @@ class ChromecastSubtitlesFragment : Fragment() { onColorSelectedEvent += ::onColorSelected onDialogDismissedEvent += ::onDialogDismissed - context?.fixPaddingStatusbar(subs_root) + fixPaddingStatusbar(binding?.subsRoot) state = getCurrentSavedStyle() context?.updateState() @@ -190,17 +199,20 @@ class ChromecastSubtitlesFragment : Fragment() { } } - subs_text_color.setup(0) - subs_outline_color.setup(1) - subs_background_color.setup(2) + binding?.apply { + subsTextColor.setup(0) + subsOutlineColor.setup(1) + subsBackgroundColor.setup(2) + } + val dismissCallback = { if (hide) activity?.hideSystemUI() } - subs_edge_type.setFocusableInTv() - subs_edge_type.setOnClickListener { textView -> + binding?.subsEdgeType?.setFocusableInTv() + binding?.subsEdgeType?.setOnClickListener { textView -> val edgeTypes = listOf( Pair( EDGE_TYPE_NONE, @@ -237,15 +249,15 @@ class ChromecastSubtitlesFragment : Fragment() { } } - subs_edge_type.setOnLongClickListener { + binding?.subsEdgeType?.setOnLongClickListener { state.edgeType = defaultState.edgeType it.context.updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } - subs_font_size.setFocusableInTv() - subs_font_size.setOnClickListener { textView -> + binding?.subsFontSize?.setFocusableInTv() + binding?.subsFontSize?.setOnClickListener { textView -> val fontSizes = listOf( Pair(0.75f, "75%"), Pair(0.80f, "80%"), @@ -278,24 +290,26 @@ class ChromecastSubtitlesFragment : Fragment() { } } - subs_font_size.setOnLongClickListener { _ -> + binding?.subsFontSize?.setOnLongClickListener { _ -> state.fontScale = defaultState.fontScale //textView.context.updateState() // font size not changed showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } - subs_font.setFocusableInTv() - subs_font.setOnClickListener { textView -> + + + binding?.subsFont?.setFocusableInTv() + binding?.subsFont?.setOnClickListener { textView -> val fontTypes = listOf( - Pair(null, textView.context.getString(R.string.normal)), - Pair("Droid Sans", "Droid Sans"), - Pair("Droid Sans Mono", "Droid Sans Mono"), - Pair("Droid Serif Regular", "Droid Serif Regular"), - Pair("Cutive Mono", "Cutive Mono"), - Pair("Short Stack", "Short Stack"), - Pair("Quintessential", "Quintessential"), - Pair("Alegreya Sans SC", "Alegreya Sans SC"), + null to textView.context.getString(R.string.normal), + "Droid Sans" to "Droid Sans", + "Droid Sans Mono" to "Droid Sans Mono", + "Droid Serif Regular" to "Droid Serif Regular", + "Cutive Mono" to "Cutive Mono", + "Short Stack" to "Short Stack", + "Quintessential" to "Quintessential", + "Alegreya Sans SC" to "Alegreya Sans SC", ) //showBottomDialog @@ -310,35 +324,35 @@ class ChromecastSubtitlesFragment : Fragment() { textView.context.updateState() } } - - subs_font.setOnLongClickListener { textView -> + binding?.subsFont?.setOnLongClickListener { textView -> state.fontFamily = defaultState.fontFamily textView.context.updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } - cancel_btt.setOnClickListener { + binding?.cancelBtt?.setOnClickListener { activity?.popCurrentPage() } - apply_btt.setOnClickListener { + binding?.applyBtt?.setOnClickListener { it.context.saveStyle(state) applyStyleEvent.invoke(state) //it.context.fromSaveToStyle(state) activity?.popCurrentPage() } - - subtitle_text.setCues( - listOf( - Cue.Builder() - .setTextSize( - getPixels(TypedValue.COMPLEX_UNIT_SP, 25.0f).toFloat(), - Cue.TEXT_SIZE_TYPE_ABSOLUTE - ) - .setText(subtitle_text.context.getString(R.string.subtitles_example_text)) - .build() + binding?.subtitleText?.apply { + setCues( + listOf( + Cue.Builder() + .setTextSize( + getPixels(TypedValue.COMPLEX_UNIT_SP, 25.0f).toFloat(), + Cue.TEXT_SIZE_TYPE_ABSOLUTE + ) + .setText(context.getString(R.string.subtitles_example_text)) + .build() + ) ) - ) + } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index ff0e0e82..8db205ef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -238,7 +238,7 @@ class SubtitlesFragment : Fragment() { context?.getExternalFilesDir(null)?.absolutePath.toString() + "/Fonts" ) - context?.fixPaddingStatusbar(subs_root) + fixPaddingStatusbar(subs_root) state = getCurrentSavedStyle() context?.updateState() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index 7d798204..4ed1aee6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -397,21 +397,22 @@ object UIHelper { return result } - fun Context?.fixPaddingStatusbar(v: View?) { - if (v == null || this == null) return + fun fixPaddingStatusbar(v: View?) { + if (v == null) return + val ctx = v.context ?: return v.setPadding( v.paddingLeft, - v.paddingTop + getStatusBarHeight(), + v.paddingTop + ctx.getStatusBarHeight(), v.paddingRight, v.paddingBottom ) } - fun Context.fixPaddingStatusbarView(v: View?) { + fun fixPaddingStatusbarView(v: View?) { if (v == null) return - + val ctx = v.context ?: return val params = v.layoutParams - params.height = getStatusBarHeight() + params.height = ctx.getStatusBarHeight() v.layoutParams = params } diff --git a/app/src/main/res/layout/fragment_home_tv.xml b/app/src/main/res/layout/fragment_home_tv.xml index ebcd3e9f..ac7c4abd 100644 --- a/app/src/main/res/layout/fragment_home_tv.xml +++ b/app/src/main/res/layout/fragment_home_tv.xml @@ -172,4 +172,15 @@ app:icon="@drawable/ic_baseline_filter_list_24" tools:ignore="ContentDescription" tools:visibility="visible" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_plugins.xml b/app/src/main/res/layout/fragment_plugins.xml index 40a0299c..c207b2c3 100644 --- a/app/src/main/res/layout/fragment_plugins.xml +++ b/app/src/main/res/layout/fragment_plugins.xml @@ -25,7 +25,7 @@ app:titleTextColor="?attr/textColor" tools:title="Overlord" /> - + - + diff --git a/app/src/main/res/layout/fragment_search_tv.xml b/app/src/main/res/layout/fragment_search_tv.xml index 0a85a471..bb59d503 100644 --- a/app/src/main/res/layout/fragment_search_tv.xml +++ b/app/src/main/res/layout/fragment_search_tv.xml @@ -89,7 +89,7 @@ app:tint="?attr/textColor" /> - + diff --git a/app/src/main/res/layout/home_select_mainpage.xml b/app/src/main/res/layout/home_select_mainpage.xml index a4bb686c..1d2d1780 100644 --- a/app/src/main/res/layout/home_select_mainpage.xml +++ b/app/src/main/res/layout/home_select_mainpage.xml @@ -26,7 +26,7 @@ android:layout_gravity="bottom" android:layout_width="match_parent" android:layout_height="60dp"> - + - - + app:cardCornerRadius="@dimen/rounded_image_radius"> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:foreground="?android:attr/selectableItemBackgroundBorderless" + android:orientation="vertical" + android:padding="10dp"> + android:id="@+id/episode_lin_holder" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + android:layout_width="126dp" + android:layout_height="72dp" + android:foreground="@drawable/outline_drawable"> + android:foreground="?android:attr/selectableItemBackgroundBorderless" + android:nextFocusRight="@id/result_episode_download" + android:scaleType="centerCrop" + tools:src="@drawable/example_poster" /> + android:layout_width="36dp" + android:layout_height="36dp" + android:layout_gravity="center" + android:contentDescription="@string/play_episode" + android:src="@drawable/play_button" /> + android:id="@+id/episode_progress" + style="@android:style/Widget.Material.ProgressBar.Horizontal" + android:layout_width="match_parent" + android:layout_height="5dp" + android:layout_gravity="bottom" + android:layout_marginBottom="-1.5dp" + android:progressBackgroundTint="?attr/colorPrimary" + android:progressTint="?attr/colorPrimary" + tools:progress="50" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginStart="15dp" + android:layout_marginEnd="50dp" + android:orientation="vertical"> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal"> + android:id="@+id/episode_filler" + style="@style/SmallBlackButton" + android:layout_gravity="start" + android:layout_marginEnd="10dp" + android:text="@string/filler" /> + android:id="@+id/episode_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:textColor="?attr/textColor" + android:textStyle="bold" + tools:text="1. Jobless" /> + android:id="@+id/episode_rating" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?attr/grayTextColor" + tools:text="Rated: 8.8" /> + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="end" + android:layout_marginStart="-50dp"> + android:id="@+id/result_episode_progress_downloaded" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_gravity="end|center_vertical" + android:layout_margin="5dp" + android:layout_marginStart="10dp" + android:layout_marginEnd="10dp" + android:background="@drawable/circle_shape" + android:indeterminate="false" + android:max="100" + android:progress="0" + android:progressDrawable="@drawable/circular_progress_bar" + android:visibility="visible" /> + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:background="?selectableItemBackgroundBorderless" + android:contentDescription="@string/download" + android:nextFocusLeft="@id/episode_poster" + android:padding="10dp" + android:src="@drawable/ic_baseline_play_arrow_24" + android:visibility="visible" + app:tint="?attr/white" /> + android:id="@+id/episode_descript" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="4" + android:paddingTop="10dp" + android:paddingBottom="10dp" + android:textColor="?attr/grayTextColor" + tools:text="Jon and Daenerys arrive in Winterfell and are met with skepticism. Sam learns about the fate of his family. Cersei gives Euron the reward he aims for. Theon follows his heart. Jon and Daenerys arrive in Winterfell and are met with skepticism. Sam learns about the fate of his family. Cersei gives Euron the reward he aims for. Theon follows his heart." /> \ No newline at end of file diff --git a/app/src/main/res/layout/tvtypes_chips_scroll.xml b/app/src/main/res/layout/tvtypes_chips_scroll.xml index 45b27dbc..66c7efda 100644 --- a/app/src/main/res/layout/tvtypes_chips_scroll.xml +++ b/app/src/main/res/layout/tvtypes_chips_scroll.xml @@ -6,5 +6,5 @@ android:requiresFadingEdge="horizontal" xmlns:android="http://schemas.android.com/apk/res/android"> - + \ No newline at end of file From 166a21f74eacd239be86aa1f4e60380edc571b12 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 14 Jul 2023 02:28:49 +0200 Subject: [PATCH 009/156] more views -> viewbinding --- .../lagradost/cloudstream3/MainActivity.kt | 1 + .../cloudstream3/ui/EasterEggMonke.kt | 21 +-- .../cloudstream3/ui/WebviewFragment.kt | 33 ++-- .../ui/download/DownloadChildAdapter.kt | 57 +++---- .../ui/download/DownloadHeaderAdapter.kt | 60 +++---- .../ui/home/HomeParentItemAdapter.kt | 25 --- .../ui/library/LoadingPosterAdapter.kt | 8 - .../ui/player/FullScreenPlayer.kt | 6 - .../cloudstream3/ui/player/GeneratorPlayer.kt | 2 +- .../ui/player/PlayerEpisodeAdapter.kt | 158 ------------------ .../cloudstream3/ui/search/SearchAdaptor.kt | 31 ++-- .../cloudstream3/ui/search/SearchFragment.kt | 1 + .../ui/settings/SettingsAccount.kt | 104 ++++++------ .../ui/settings/SettingsFragment.kt | 77 +++++---- .../ui/settings/SettingsGeneral.kt | 27 +-- .../ui/settings/SettingsUpdates.kt | 16 +- .../settings/extensions/ExtensionsFragment.kt | 95 ++++++----- .../ui/settings/extensions/RepoAdapter.kt | 72 ++++++-- .../ui/settings/testing/TestFragment.kt | 120 ++++++------- 19 files changed, 412 insertions(+), 502 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerEpisodeAdapter.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index d054f504..f409c10f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -743,6 +743,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) updateTv() + if (isTvSettings()) { setContentView(R.layout.activity_main_tv) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt index 556ebd34..c7041776 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt @@ -16,14 +16,16 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.AppCompatImageView import androidx.core.view.isVisible import com.lagradost.cloudstream3.R -import kotlinx.android.synthetic.main.activity_easter_egg_monke.* -import java.util.* +import com.lagradost.cloudstream3.databinding.ActivityEasterEggMonkeBinding class EasterEggMonke : AppCompatActivity() { + lateinit var binding : ActivityEasterEggMonkeBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_easter_egg_monke) + + binding = ActivityEasterEggMonkeBinding.inflate(layoutInflater) + setContentView(binding.root) val handler = Handler(mainLooper) lateinit var runnable: Runnable @@ -32,15 +34,14 @@ class EasterEggMonke : AppCompatActivity() { handler.postDelayed(runnable, 300) } handler.postDelayed(runnable, 1000) - } private fun shower() { - val containerW = frame.width - val containerH = frame.height - var starW: Float = monke.width.toFloat() - var starH: Float = monke.height.toFloat() + val containerW = binding.frame.width + val containerH = binding.frame.height + var starW: Float = binding.monke.width.toFloat() + var starH: Float = binding.monke.height.toFloat() val newStar = AppCompatImageView(this) val idx = (monkeys.size * Math.random()).toInt() @@ -48,7 +49,7 @@ class EasterEggMonke : AppCompatActivity() { newStar.isVisible = true newStar.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT) - frame.addView(newStar) + binding.frame.addView(newStar) newStar.scaleX = Math.random().toFloat() * 1.5f + newStar.scaleX newStar.scaleY = newStar.scaleX @@ -70,7 +71,7 @@ class EasterEggMonke : AppCompatActivity() { set.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - frame.removeView(newStar) + binding.frame.removeView(newStar) } }) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt index 19e24f74..9ed58e2c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt @@ -12,20 +12,23 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.findNavController import com.lagradost.cloudstream3.MainActivity -import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.USER_AGENT +import com.lagradost.cloudstream3.databinding.FragmentWebviewBinding import com.lagradost.cloudstream3.network.WebViewResolver import com.lagradost.cloudstream3.utils.AppUtils.loadRepository -import kotlinx.android.synthetic.main.fragment_webview.* + class WebviewFragment : Fragment() { + + var binding: FragmentWebviewBinding? = null + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val url = arguments?.getString(WEBVIEW_URL) ?: "".also { findNavController().popBackStack() } - web_view.webViewClient = object : WebViewClient() { + binding?.webView?.webViewClient = object : WebViewClient() { override fun shouldOverrideUrlLoading( view: WebView?, request: WebResourceRequest? @@ -40,24 +43,28 @@ class WebviewFragment : Fragment() { return super.shouldOverrideUrlLoading(view, request) } } + binding?.webView?.apply { + WebViewResolver.webViewUserAgent = settings.userAgentString - WebViewResolver.webViewUserAgent = web_view.settings.userAgentString - - web_view.addJavascriptInterface(RepoApi(activity), "RepoApi") - web_view.settings.javaScriptEnabled = true - web_view.settings.userAgentString = USER_AGENT - web_view.settings.domStorageEnabled = true + addJavascriptInterface(RepoApi(activity), "RepoApi") + settings.javaScriptEnabled = true + settings.userAgentString = USER_AGENT + settings.domStorageEnabled = true // WebView.setWebContentsDebuggingEnabled(true) - web_view.loadUrl(url) + loadUrl(url) + } + } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { + val localBinding = FragmentWebviewBinding.inflate(inflater, container, false) + binding = localBinding // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_webview, container, false) + return localBinding.root//inflater.inflate(R.layout.fragment_webview, container, false) } companion object { @@ -70,7 +77,7 @@ class WebviewFragment : Fragment() { private class RepoApi(val activity: FragmentActivity?) { @JavascriptInterface - fun installRepo(repoUrl: String) { + fun installRepo(repoUrl: String) { activity?.loadRepository(repoUrl) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt index a541171b..1a3e2db3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt @@ -3,18 +3,13 @@ package com.lagradost.cloudstream3.ui.download import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.cardview.widget.CardView -import androidx.core.widget.ContentLoadingProgressBar import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.R +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 -import kotlinx.android.synthetic.main.download_child_episode.view.* -import java.util.* +import java.util.Collections const val DOWNLOAD_ACTION_PLAY_FILE = 0 const val DOWNLOAD_ACTION_DELETE_FILE = 1 @@ -68,7 +63,7 @@ class DownloadChildAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return DownloadChildViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.download_child_episode, parent, false), + DownloadChildEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false), clickCallback ) } @@ -88,17 +83,17 @@ class DownloadChildAdapter( class DownloadChildViewHolder constructor( - itemView: View, + val binding: DownloadChildEpisodeBinding, private val clickCallback: (DownloadClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder { + ) : RecyclerView.ViewHolder(binding.root), DownloadButtonViewHolder { override var downloadButton = EasyDownloadButton() - private val title: TextView = itemView.download_child_episode_text + /*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 + private val downloadImage: ImageView = itemView.download_child_episode_download*/ private var localCard: VisualDownloadChildCached? = null @@ -107,29 +102,35 @@ class DownloadChildAdapter( val d = card.data val posDur = getViewPos(d.id) - if (posDur != null) { - val visualPos = posDur.fixVisual() - progressBar.max = (visualPos.duration / 1000).toInt() - progressBar.progress = (visualPos.position / 1000).toInt() - progressBar.visibility = View.VISIBLE - } else { - progressBar.visibility = View.GONE + 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.downloadChildEpisodeText.apply { + text = context.getNameFull(d.name, d.episode, d.season) + isSelected = true // is needed for text repeating } - title.text = title.context.getNameFull(d.name, d.episode, d.season) - title.isSelected = true // is needed for text repeating downloadButton.setUpButton( card.currentBytes, card.totalBytes, - progressBarDownload, - downloadImage, - extraInfo, + binding.downloadChildEpisodeProgressDownloaded, + binding.downloadChildEpisodeDownload, + binding.downloadChildEpisodeTextExtra, card.data, clickCallback ) - holder.setOnClickListener { + binding.downloadChildEpisodeHolder.setOnClickListener { clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) } } @@ -141,9 +142,9 @@ class DownloadChildAdapter( downloadButton.setUpButton( card.currentBytes, card.totalBytes, - progressBarDownload, - downloadImage, - extraInfo, + binding.downloadChildEpisodeProgressDownloaded, + binding.downloadChildEpisodeDownload, + binding.downloadChildEpisodeTextExtra, card.data, clickCallback ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt index 29bb303a..1634009b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt @@ -5,16 +5,12 @@ import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.cardview.widget.CardView -import androidx.core.widget.ContentLoadingProgressBar 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 kotlinx.android.synthetic.main.download_header_episode.view.* import java.util.* data class VisualDownloadHeaderCached( @@ -66,7 +62,7 @@ class DownloadHeaderAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return DownloadHeaderViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.download_header_episode, parent, false), + DownloadHeaderEpisodeBinding.inflate(LayoutInflater.from(parent.context),parent,false), clickCallback, movieClickCallback ) @@ -87,20 +83,20 @@ class DownloadHeaderAdapter( class DownloadHeaderViewHolder constructor( - itemView: View, + val binding: DownloadHeaderEpisodeBinding, private val clickCallback: (DownloadHeaderClickEvent) -> Unit, private val movieClickCallback: (DownloadClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder { + ) : RecyclerView.ViewHolder(binding.root), DownloadButtonViewHolder { override var downloadButton = EasyDownloadButton() - private val poster: ImageView? = itemView.download_header_poster + /*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 + private val normalImage: ImageView = itemView.download_header_goto_child*/ private var localCard: VisualDownloadHeaderCached? = null @SuppressLint("SetTextI18n") @@ -108,19 +104,24 @@ class DownloadHeaderAdapter( localCard = card val d = card.data - poster?.setImage(d.poster) - poster?.setOnClickListener { - clickCallback.invoke(DownloadHeaderClickEvent(1, d)) + binding.downloadHeaderPoster.apply { + setImage(d.poster) + setOnClickListener { + clickCallback.invoke(DownloadHeaderClickEvent(1, d)) + } } - title.text = d.name + binding.apply { + + binding.downloadHeaderTitle.text = d.name val mbString = formatShortFileSize(itemView.context, card.totalBytes) //val isMovie = d.type.isMovieType() if (card.child != null) { - downloadBar.visibility = View.VISIBLE - downloadImage.visibility = View.VISIBLE - normalImage.visibility = View.GONE + downloadHeaderProgressDownloaded.visibility = View.VISIBLE + + downloadHeaderEpisodeDownload.visibility = View.VISIBLE + binding.downloadHeaderGotoChild.visibility = View.GONE /*setUpButton( card.currentBytes, card.totalBytes, @@ -131,34 +132,35 @@ class DownloadHeaderAdapter( movieClickCallback )*/ - holder.setOnClickListener { + episodeHolder.setOnClickListener { movieClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child)) } } else { - downloadBar.visibility = View.GONE - downloadImage.visibility = View.GONE - normalImage.visibility = View.VISIBLE + downloadHeaderProgressDownloaded.visibility = View.GONE + downloadHeaderEpisodeDownload.visibility = View.GONE + binding.downloadHeaderGotoChild.visibility = View.VISIBLE try { - extraInfo.text = - extraInfo.context.getString(R.string.extra_info_format).format( + downloadHeaderInfo.text = + downloadHeaderInfo.context.getString(R.string.extra_info_format).format( card.totalDownloads, - if (card.totalDownloads == 1) extraInfo.context.getString(R.string.episode) else extraInfo.context.getString( + 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 - extraInfo.text = "Error" + downloadHeaderInfo.text = "Error" logError(t) } - holder.setOnClickListener { + episodeHolder.setOnClickListener { clickCallback.invoke(DownloadHeaderClickEvent(0, d)) } } + } } override fun reattachDownloadButton() { @@ -168,9 +170,9 @@ class DownloadHeaderAdapter( downloadButton.setUpButton( card.currentBytes, card.totalBytes, - downloadBar, - downloadImage, - extraInfo, + binding.downloadHeaderProgressDownloaded, + binding.downloadHeaderEpisodeDownload, + binding.downloadHeaderInfo, card.child, movieClickCallback ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt index 58c6dbe0..7ce9e67d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt @@ -3,49 +3,24 @@ package com.lagradost.cloudstream3.ui.home import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.LinearLayout import android.widget.TextView -import androidx.core.content.ContextCompat -import androidx.core.view.isGone -import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView -import androidx.transition.ChangeBounds -import androidx.transition.TransitionManager -import androidx.viewpager2.widget.ViewPager2 -import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipDrawable -import com.lagradost.cloudstream3.APIHolder.getId -import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.SearchResponse -import com.lagradost.cloudstream3.mvvm.Resource -import com.lagradost.cloudstream3.ui.WatchType -import com.lagradost.cloudstream3.ui.result.LinearListLayout -import com.lagradost.cloudstream3.ui.result.ResultViewModel2 -import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST import com.lagradost.cloudstream3.ui.result.setLinearListLayout -import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable -import com.lagradost.cloudstream3.utils.AppUtils.loadResult -import com.lagradost.cloudstream3.utils.DataStoreHelper -import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog -import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView import kotlinx.android.synthetic.main.activity_main_tv.* import kotlinx.android.synthetic.main.activity_main_tv.view.* import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.view.* import kotlinx.android.synthetic.main.fragment_home_head_tv.* import kotlinx.android.synthetic.main.fragment_home_head_tv.view.* -import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview -import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_viewpager import kotlinx.android.synthetic.main.homepage_parent.view.* class LoadClickCallback( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LoadingPosterAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LoadingPosterAdapter.kt index a637133b..160fbe2b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LoadingPosterAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LoadingPosterAdapter.kt @@ -5,15 +5,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.BaseAdapter -import android.widget.FrameLayout -import android.widget.LinearLayout -import android.widget.ListPopupWindow.MATCH_PARENT -import android.widget.RelativeLayout import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.android.synthetic.main.loading_poster_dynamic.view.* -import kotlin.math.roundToInt -import kotlin.math.sqrt class LoadingPosterAdapter(context: Context, private val itemCount: Int) : BaseAdapter() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index 9ff1c52d..37fb0373 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -97,12 +97,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { protected var isShowing = false protected var isLocked = false - //private var episodes: List = listOf() - protected fun setEpisodes(ep: List) { - //hasEpisodes = ep.size > 1 // if has 2 episodes or more because you dont want to switch to your current episode - //(player_episode_list?.adapter as? PlayerEpisodeAdapter?)?.updateList(ep) - } - protected var hasEpisodes = false private set //protected val hasEpisodes diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index fd29d998..4f29468c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -163,7 +163,7 @@ class GeneratorPlayer : FullScreenPlayer() { currentSelectedLink = link currentMeta = viewModel.getMeta() nextMeta = viewModel.getNextMeta() - setEpisodes(viewModel.getAllMeta() ?: emptyList()) + // setEpisodes(viewModel.getAllMeta() ?: emptyList()) isActive = true setPlayerDimen(null) setTitle() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerEpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerEpisodeAdapter.kt deleted file mode 100644 index cfe27a30..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerEpisodeAdapter.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.lagradost.cloudstream3.ui.player - -import android.annotation.SuppressLint -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.core.view.isGone -import androidx.core.view.isVisible -import androidx.core.widget.ContentLoadingProgressBar -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.button.MaterialButton -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.ui.result.ResultEpisode -import com.lagradost.cloudstream3.ui.result.getDisplayPosition -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings -import com.lagradost.cloudstream3.utils.AppUtils.html -import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.player_episodes_large.view.episode_holder_large -import kotlinx.android.synthetic.main.player_episodes_large.view.episode_progress -import kotlinx.android.synthetic.main.player_episodes_small.view.episode_holder -import kotlinx.android.synthetic.main.result_episode_large.view.* - - -data class PlayerEpisodeClickEvent(val action: Int, val data: Any) - -class PlayerEpisodeAdapter( - private val items: MutableList = mutableListOf(), - private val clickCallback: (PlayerEpisodeClickEvent) -> Unit, -) : RecyclerView.Adapter() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return PlayerEpisodeCardViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.player_episodes, parent, false), - clickCallback, - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - println("HOLDER $holder $position") - - when (holder) { - is PlayerEpisodeCardViewHolder -> { - holder.bind(items[position]) - } - } - } - - override fun getItemCount(): Int { - return items.size - } - - fun updateList(newList: List) { - println("Updated list $newList") - val diffResult = DiffUtil.calculateDiff(EpisodeDiffCallback(this.items, newList)) - items.clear() - items.addAll(newList) - - diffResult.dispatchUpdatesTo(this) - } - - class PlayerEpisodeCardViewHolder - constructor( - itemView: View, - private val clickCallback: (PlayerEpisodeClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(itemView) { - @SuppressLint("SetTextI18n") - fun bind(card: Any) { - if (card is ResultEpisode) { - val (parentView, otherView) = if (card.poster == null) { - itemView.episode_holder to itemView.episode_holder_large - } else { - itemView.episode_holder_large to itemView.episode_holder - } - - val episodeText: TextView? = parentView.episode_text - val episodeFiller: MaterialButton? = parentView.episode_filler - val episodeRating: TextView? = parentView.episode_rating - val episodeDescript: TextView? = parentView.episode_descript - val episodeProgress: ContentLoadingProgressBar? = parentView.episode_progress - val episodePoster: ImageView? = parentView.episode_poster - - parentView.isVisible = true - otherView.isVisible = false - - - episodeText?.apply { - val name = - if (card.name == null) "${context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}" - - text = name - isSelected = true - } - - episodeFiller?.isVisible = card.isFiller == true - - val displayPos = card.getDisplayPosition() - episodeProgress?.max = (card.duration / 1000).toInt() - episodeProgress?.progress = (displayPos / 1000).toInt() - episodeProgress?.isVisible = displayPos > 0L - episodePoster?.isVisible = episodePoster?.setImage(card.poster) == true - - if (card.rating != null) { - episodeRating?.text = episodeRating?.context?.getString(R.string.rated_format) - ?.format(card.rating.toFloat() / 10f) - } else { - episodeRating?.text = "" - } - - episodeRating?.isGone = episodeRating?.text.isNullOrBlank() - - episodeDescript?.apply { - text = card.description.html() - isGone = text.isNullOrBlank() - //setOnClickListener { - // clickCallback.invoke(PlayerEpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card)) - //} - } - - parentView.setOnClickListener { - clickCallback.invoke(PlayerEpisodeClickEvent(0, card)) - } - - if (isTrueTvSettings()) { - parentView.isFocusable = true - parentView.isFocusableInTouchMode = true - parentView.touchscreenBlocksFocus = false - } - } - } - } -} - -class EpisodeDiffCallback( - private val oldList: List, - private val newList: List -) : - DiffUtil.Callback() { - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val a = oldList[oldItemPosition] - val b = newList[newItemPosition] - return if (a is ResultEpisode && b is ResultEpisode) { - a.id == b.id - } else { - a == b - } - } - - override fun getOldListSize() = oldList.size - - override fun getNewListSize() = newList.size - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition] == newList[newItemPosition] -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt index 7fdd6e1d..233614dd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt @@ -4,16 +4,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import android.widget.ImageView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.R +import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.databinding.SearchResultGridBinding +import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding import com.lagradost.cloudstream3.ui.AutofitRecyclerView -import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.android.synthetic.main.search_result_compact.view.* import kotlin.math.roundToInt /** Click */ @@ -39,10 +38,23 @@ class SearchAdapter( var hasNext: Boolean = false override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + + val layout = - if (parent.context.IsBottomLayout()) R.layout.search_result_grid_expanded else R.layout.search_result_grid + if (parent.context.IsBottomLayout()) SearchResultGridExpandedBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) else SearchResultGridBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) //R.layout.search_result_grid_expanded else R.layout.search_result_grid + + + return CardViewHolder( - LayoutInflater.from(parent.context).inflate(layout, parent, false), + layout, clickCallback, resView ) @@ -73,12 +85,11 @@ class SearchAdapter( class CardViewHolder constructor( - itemView: View, + val binding: ViewBinding, private val clickCallback: (SearchClickCallback) -> Unit, resView: AutofitRecyclerView ) : - RecyclerView.ViewHolder(itemView) { - private val cardView: ImageView = itemView.imageView + RecyclerView.ViewHolder(binding.root) { private val compactView = false//itemView.context.getGridIsCompact() private val coverHeight: Int = @@ -86,7 +97,7 @@ class SearchAdapter( fun bind(card: SearchResponse, position: Int) { if (!compactView) { - cardView.apply { + binding.root.apply { layoutParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, coverHeight diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index e0f67d4a..a11dab25 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -104,6 +104,7 @@ class SearchFragment : Fragment() { val layout = if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search val root = inflater.inflate(layout, container, false) + // TODO TRYCATCH binding = FragmentSearchBinding.bind(root) return root diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index acf715b3..a0166409 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -15,6 +15,9 @@ import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.AccountManagmentBinding +import com.lagradost.cloudstream3.databinding.AccountSwitchBinding +import com.lagradost.cloudstream3.databinding.AddAccountInputBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi @@ -31,9 +34,6 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.account_managment.* -import kotlinx.android.synthetic.main.account_switch.* -import kotlinx.android.synthetic.main.add_account_input.* class SettingsAccount : PreferenceFragmentCompat() { companion object { @@ -43,15 +43,18 @@ class SettingsAccount : PreferenceFragmentCompat() { api: AccountManager, info: AuthAPI.LoginInfo ) { + if (activity == null) return + val binding: AccountManagmentBinding = + AccountManagmentBinding.inflate(activity.layoutInflater, null, false) val builder = - AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom) - .setView(R.layout.account_managment) + AlertDialog.Builder(activity, R.style.AlertDialogCustom) + .setView(binding.root) val dialog = builder.show() - dialog.account_main_profile_picture_holder?.isVisible = - dialog.account_main_profile_picture?.setImage(info.profilePicture) == true + binding.accountMainProfilePictureHolder.isVisible = + binding.accountMainProfilePicture.setImage(info.profilePicture) - dialog.account_logout?.setOnClickListener { + binding.accountLogout.setOnClickListener { api.logOut() dialog.dismissSafe(activity) } @@ -60,26 +63,28 @@ class SettingsAccount : PreferenceFragmentCompat() { dialog.findViewById(R.id.account_name)?.text = it } - dialog.account_site?.text = api.name - dialog.account_switch_account?.setOnClickListener { + binding.accountSite.text = api.name + binding.accountSwitchAccount.setOnClickListener { dialog.dismissSafe(activity) showAccountSwitch(activity, api) } if (isTvSettings()) { - dialog.account_switch_account?.requestFocus() + binding.accountSwitchAccount.requestFocus() } } - fun showAccountSwitch(activity: FragmentActivity, api: AccountManager) { + private fun showAccountSwitch(activity: FragmentActivity, api: AccountManager) { val accounts = api.getAccounts() ?: return + val binding: AccountSwitchBinding = + AccountSwitchBinding.inflate(activity.layoutInflater, null, false) val builder = AlertDialog.Builder(activity, R.style.AlertDialogCustom) - .setView(R.layout.account_switch) + .setView(binding.root) val dialog = builder.show() - dialog.account_add?.setOnClickListener { + binding.accountAdd.setOnClickListener { addAccount(activity, api) dialog?.dismissSafe(activity) } @@ -111,17 +116,21 @@ class SettingsAccount : PreferenceFragmentCompat() { is OAuth2API -> { api.authenticate(activity) } + is InAppAuthAPI -> { + if (activity == null) return + val binding: AddAccountInputBinding = + AddAccountInputBinding.inflate(activity.layoutInflater, null, false) val builder = - AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom) - .setView(R.layout.add_account_input) + AlertDialog.Builder(activity, R.style.AlertDialogCustom) + .setView(binding.root) val dialog = builder.show() - val visibilityMap = mapOf( - dialog.login_email_input to api.requiresEmail, - dialog.login_password_input to api.requiresPassword, - dialog.login_server_input to api.requiresServer, - dialog.login_username_input to api.requiresUsername + val visibilityMap = listOf( + binding.loginEmailInput to api.requiresEmail, + binding.loginPasswordInput to api.requiresPassword, + binding.loginServerInput to api.requiresServer, + binding.loginUsernameInput to api.requiresUsername ) if (isTvSettings()) { @@ -145,12 +154,12 @@ class SettingsAccount : PreferenceFragmentCompat() { } } - dialog.login_email_input?.isVisible = api.requiresEmail - dialog.login_password_input?.isVisible = api.requiresPassword - dialog.login_server_input?.isVisible = api.requiresServer - dialog.login_username_input?.isVisible = api.requiresUsername - dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank() - dialog.create_account?.setOnClickListener { + binding.loginEmailInput.isVisible = api.requiresEmail + binding.loginPasswordInput.isVisible = api.requiresPassword + binding.loginServerInput.isVisible = api.requiresServer + binding.loginUsernameInput.isVisible = api.requiresUsername + binding.createAccount.isGone = api.createAccountUrl.isNullOrBlank() + binding.createAccount.setOnClickListener { openBrowser( api.createAccountUrl ?: return@setOnClickListener, activity @@ -159,43 +168,43 @@ class SettingsAccount : PreferenceFragmentCompat() { } val displayedItems = listOf( - dialog.login_username_input, - dialog.login_email_input, - dialog.login_server_input, - dialog.login_password_input + binding.loginUsernameInput, + binding.loginEmailInput, + binding.loginServerInput, + binding.loginPasswordInput ).filter { it.isVisible } displayedItems.foldRight(displayedItems.firstOrNull()) { item, previous -> - item?.id?.let { previous?.nextFocusDownId = it } - previous?.id?.let { item?.nextFocusUpId = it } + item.id.let { previous?.nextFocusDownId = it } + previous?.id?.let { item.nextFocusUpId = it } item } displayedItems.firstOrNull()?.let { - dialog.create_account?.nextFocusDownId = it.id - it.nextFocusUpId = dialog.create_account.id + binding.createAccount.nextFocusDownId = it.id + it.nextFocusUpId = binding.createAccount.id } - dialog.apply_btt?.id?.let { + binding.applyBtt.id.let { displayedItems.lastOrNull()?.nextFocusDownId = it } - dialog.text1?.text = api.name + binding.text1.text = api.name if (api.storesPasswordInPlainText) { api.getLatestLoginData()?.let { data -> - dialog.login_email_input?.setText(data.email ?: "") - dialog.login_server_input?.setText(data.server ?: "") - dialog.login_username_input?.setText(data.username ?: "") - dialog.login_password_input?.setText(data.password ?: "") + binding.loginEmailInput.setText(data.email ?: "") + binding.loginServerInput.setText(data.server ?: "") + binding.loginUsernameInput.setText(data.username ?: "") + binding.loginPasswordInput.setText(data.password ?: "") } } - dialog.apply_btt?.setOnClickListener { + binding.applyBtt.setOnClickListener { val loginData = InAppAuthAPI.LoginData( - username = if (api.requiresUsername) dialog.login_username_input?.text?.toString() else null, - password = if (api.requiresPassword) dialog.login_password_input?.text?.toString() else null, - email = if (api.requiresEmail) dialog.login_email_input?.text?.toString() else null, - server = if (api.requiresServer) dialog.login_server_input?.text?.toString() else null, + username = if (api.requiresUsername) binding.loginUsernameInput.text?.toString() else null, + password = if (api.requiresPassword) binding.loginPasswordInput.text?.toString() else null, + email = if (api.requiresEmail) binding.loginEmailInput.text?.toString() else null, + server = if (api.requiresServer) binding.loginServerInput.text?.toString() else null, ) ioSafe { val isSuccessful = try { @@ -220,10 +229,11 @@ class SettingsAccount : PreferenceFragmentCompat() { } dialog.dismissSafe(activity) } - dialog.cancel_btt?.setOnClickListener { + binding.cancelBtt.setOnClickListener { dialog.dismissSafe(activity) } } + else -> { throw NotImplementedError("You are trying to add an account that has an unknown login method") } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 453f93be..85afc048 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -14,7 +14,9 @@ import androidx.fragment.app.Fragment import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager +import com.google.android.material.appbar.MaterialToolbar import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.MainSettingsBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.ui.home.HomeFragment @@ -22,16 +24,14 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.android.synthetic.main.main_settings.* -import kotlinx.android.synthetic.main.standard_toolbar.* import java.io.File class SettingsFragment : Fragment() { companion object { var beneneCount = 0 - private var isTv : Boolean = false - private var isTrueTv : Boolean = false + private var isTv: Boolean = false + private var isTrueTv: Boolean = false fun PreferenceFragmentCompat?.getPref(id: Int): Preference? { if (this == null) return null @@ -55,26 +55,30 @@ class SettingsFragment : Fragment() { fun Fragment?.setUpToolbar(title: String) { if (this == null) return - settings_toolbar?.apply { + val settingsToolbar = view?.findViewById(R.id.settings_toolbar) ?: return + + settingsToolbar.apply { setTitle(title) setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) setNavigationOnClickListener { activity?.onBackPressed() } } - fixPaddingStatusbar(settings_toolbar) + fixPaddingStatusbar(settingsToolbar) } fun Fragment?.setUpToolbar(@StringRes title: Int) { if (this == null) return - settings_toolbar?.apply { + val settingsToolbar = view?.findViewById(R.id.settings_toolbar) ?: return + + settingsToolbar.apply { setTitle(title) setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) setNavigationOnClickListener { activity?.onBackPressed() } } - fixPaddingStatusbar(settings_toolbar) + fixPaddingStatusbar(settingsToolbar) } fun getFolderSize(dir: File): Long { @@ -139,12 +143,21 @@ class SettingsFragment : Fragment() { } } + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + + var binding: MainSettingsBinding? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? { - return inflater.inflate(R.layout.main_settings, container, false) + ): View { + val localBinding = MainSettingsBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + //return inflater.inflate(R.layout.main_settings, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -157,36 +170,34 @@ class SettingsFragment : Fragment() { for (syncApi in accountManagers) { val login = syncApi.loginInfo() val pic = login?.profilePicture ?: continue - if (settings_profile_pic?.setImage( + if (binding?.settingsProfilePic?.setImage( pic, errorImageDrawable = HomeFragment.errorProfilePic ) == true ) { - settings_profile_text?.text = login.name - settings_profile?.isVisible = true + binding?.settingsProfileText?.text = login.name + binding?.settingsProfile?.isVisible = true break } } - - listOf( - Pair(settings_general, R.id.action_navigation_settings_to_navigation_settings_general), - Pair(settings_player, R.id.action_navigation_settings_to_navigation_settings_player), - Pair(settings_credits, R.id.action_navigation_settings_to_navigation_settings_account), - Pair(settings_ui, R.id.action_navigation_settings_to_navigation_settings_ui), - Pair(settings_providers, R.id.action_navigation_settings_to_navigation_settings_providers), - Pair(settings_updates, R.id.action_navigation_settings_to_navigation_settings_updates), - Pair( - settings_extensions, - R.id.action_navigation_settings_to_navigation_settings_extensions - ), - ).forEach { (view, navigationId) -> - view?.apply { - setOnClickListener { - navigate(navigationId) - } - if (isTrueTv) { - isFocusable = true - isFocusableInTouchMode = true + binding?.apply { + listOf( + settingsGeneral to R.id.action_navigation_settings_to_navigation_settings_general, + settingsPlayer to R.id.action_navigation_settings_to_navigation_settings_player, + settingsCredits to R.id.action_navigation_settings_to_navigation_settings_account, + settingsUi to R.id.action_navigation_settings_to_navigation_settings_ui, + settingsProviders to R.id.action_navigation_settings_to_navigation_settings_providers, + settingsUpdates to R.id.action_navigation_settings_to_navigation_settings_updates, + settingsExtensions to R.id.action_navigation_settings_to_navigation_settings_extensions, + ).forEach { (view, navigationId) -> + view.apply { + setOnClickListener { + navigate(navigationId) + } + if (isTrueTv) { + isFocusable = true + isFocusableInTouchMode = true + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index ee262eec..ef194b57 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -22,6 +22,8 @@ import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.databinding.AddRemoveSitesBinding +import com.lagradost.cloudstream3.databinding.AddSiteInputBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.network.initClient @@ -38,8 +40,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath -import kotlinx.android.synthetic.main.add_remove_sites.* -import kotlinx.android.synthetic.main.add_site_input.* import java.io.File fun getCurrentLocale(context: Context): String { @@ -197,18 +197,20 @@ class SettingsGeneral : PreferenceFragmentCompat() { {}) { selection -> val provider = providers.getOrNull(selection) ?: return@showDialog + val binding : AddSiteInputBinding = AddSiteInputBinding.inflate(layoutInflater,null,false) + val builder = AlertDialog.Builder(context ?: return@showDialog, R.style.AlertDialogCustom) - .setView(R.layout.add_site_input) + .setView(binding.root) val dialog = builder.create() dialog.show() - dialog.text2?.text = provider.name - dialog.apply_btt?.setOnClickListener { - val name = dialog.site_name_input?.text?.toString() - val url = dialog.site_url_input?.text?.toString() - val lang = dialog.site_lang_input?.text?.toString() + binding.text2.text = provider.name + binding.applyBtt.setOnClickListener { + val name = binding.siteNameInput.text?.toString() + val url = binding.siteUrlInput.text?.toString() + val lang = binding.siteLangInput.text?.toString() val realLang = if (lang.isNullOrBlank()) provider.lang else lang if (url.isNullOrBlank() || name.isNullOrBlank() || realLang.length != 2) { showToast(activity, R.string.error_invalid_data, Toast.LENGTH_SHORT) @@ -222,7 +224,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { dialog.dismissSafe(activity) } - dialog.cancel_btt?.setOnClickListener { + binding.cancelBtt.setOnClickListener { dialog.dismissSafe(activity) } } @@ -242,18 +244,19 @@ class SettingsGeneral : PreferenceFragmentCompat() { } fun showAddOrDelete() { + val binding : AddRemoveSitesBinding = AddRemoveSitesBinding.inflate(layoutInflater,null,false) val builder = AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom) - .setView(R.layout.add_remove_sites) + .setView(binding.root) val dialog = builder.create() dialog.show() - dialog.add_site?.setOnClickListener { + binding.addSite.setOnClickListener { showAdd() dialog.dismissSafe(activity) } - dialog.remove_site?.setOnClickListener { + binding.removeSite.setOnClickListener { showDelete() dialog.dismissSafe(activity) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index f9ac3fee..4208f965 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -13,6 +13,7 @@ import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.LogcatBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom @@ -25,7 +26,6 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.VideoDownloadManager -import kotlinx.android.synthetic.main.logcat.* import okhttp3.internal.closeQuietly import java.io.BufferedReader import java.io.InputStreamReader @@ -60,7 +60,9 @@ class SettingsUpdates : PreferenceFragmentCompat() { getPref(R.string.show_logcat_key)?.setOnPreferenceClickListener { pref -> val builder = AlertDialog.Builder(pref.context, R.style.AlertDialogCustom) - .setView(R.layout.logcat) + + val binding = LogcatBinding.inflate(layoutInflater,null,false ) + builder.setView(binding.root) val dialog = builder.create() dialog.show() @@ -81,9 +83,9 @@ class SettingsUpdates : PreferenceFragmentCompat() { } val text = log.toString() - dialog.text1?.text = text + binding.text1.text = text - dialog.copy_btt?.setOnClickListener { + binding.copyBtt.setOnClickListener { // Can crash on too much text try { val serviceClipboard = @@ -96,11 +98,11 @@ class SettingsUpdates : PreferenceFragmentCompat() { showToast(activity, R.string.clipboard_too_large) } } - dialog.clear_btt?.setOnClickListener { + binding.clearBtt.setOnClickListener { Runtime.getRuntime().exec("logcat -c") dialog.dismissSafe(activity) } - dialog.save_btt?.setOnClickListener { + binding.saveBtt.setOnClickListener { var fileStream: OutputStream? = null try { fileStream = @@ -119,7 +121,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { dialog.dismissSafe(activity) } } - dialog.close_btt?.setOnClickListener { + binding.closeBtt.setOnClickListener { dialog.dismissSafe(activity) } return@setOnPreferenceClickListener true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 75ff8305..711026c6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -18,6 +18,8 @@ import androidx.navigation.fragment.findNavController import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.AddRepoInputBinding +import com.lagradost.cloudstream3.databinding.FragmentExtensionsBinding import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.plugins.RepositoryManager @@ -30,16 +32,22 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.widget.LinearRecycleViewLayoutManager -import kotlinx.android.synthetic.main.add_repo_input.* -import kotlinx.android.synthetic.main.fragment_extensions.* class ExtensionsFragment : Fragment() { + var binding: FragmentExtensionsBinding? = null + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? { - return inflater.inflate(R.layout.fragment_extensions, container, false) + ): View { + val localBinding = FragmentExtensionsBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root//inflater.inflate(R.layout.fragment_extensions, container, false) } private fun View.setLayoutWidth(weight: Int) { @@ -74,7 +82,7 @@ class ExtensionsFragment : Fragment() { setUpToolbar(R.string.extensions) - repo_recycler_view?.adapter = RepoAdapter(false, { + binding?.repoRecyclerView?.adapter = RepoAdapter(false, { findNavController().navigate( R.id.navigation_settings_extensions_to_navigation_settings_plugins, PluginsFragment.newInstance( @@ -113,12 +121,12 @@ class ExtensionsFragment : Fragment() { }) observe(extensionViewModel.repositories) { - repo_recycler_view?.isVisible = it.isNotEmpty() - blank_repo_screen?.isVisible = it.isEmpty() - (repo_recycler_view?.adapter as? RepoAdapter)?.updateList(it) + binding?.repoRecyclerView?.isVisible = it.isNotEmpty() + binding?.blankRepoScreen?.isVisible = it.isEmpty() + (binding?.repoRecyclerView?.adapter as? RepoAdapter)?.updateList(it) } - repo_recycler_view?.apply { + binding?.repoRecyclerView?.apply { context?.let { ctx -> layoutManager = LinearRecycleViewLayoutManager(ctx, nextFocusUpId, nextFocusDownId) } @@ -140,28 +148,30 @@ class ExtensionsFragment : Fragment() { // } observeNullable(extensionViewModel.pluginStats) { value -> - if (value == null) { - plugin_storage_appbar?.isVisible = false + binding?.apply { + if (value == null) { + pluginStorageAppbar.isVisible = false - return@observeNullable - } + return@observeNullable + } - plugin_storage_appbar?.isVisible = true - if (value.total == 0) { - plugin_download?.setLayoutWidth(1) - plugin_disabled?.setLayoutWidth(0) - plugin_not_downloaded?.setLayoutWidth(0) - } else { - plugin_download?.setLayoutWidth(value.downloaded) - plugin_disabled?.setLayoutWidth(value.disabled) - plugin_not_downloaded?.setLayoutWidth(value.notDownloaded) + pluginStorageAppbar.isVisible = true + if (value.total == 0) { + pluginDownload.setLayoutWidth(1) + pluginDisabled.setLayoutWidth(0) + pluginNotDownloaded.setLayoutWidth(0) + } else { + pluginDownload.setLayoutWidth(value.downloaded) + pluginDisabled.setLayoutWidth(value.disabled) + pluginNotDownloaded.setLayoutWidth(value.notDownloaded) + } + pluginNotDownloadedTxt.setText(value.notDownloadedText) + pluginDisabledTxt.setText(value.disabledText) + pluginDownloadTxt.setText(value.downloadedText) } - plugin_not_downloaded_txt.setText(value.notDownloadedText) - plugin_disabled_txt.setText(value.disabledText) - plugin_download_txt.setText(value.downloadedText) } - plugin_storage_appbar?.setOnClickListener { + binding?.pluginStorageAppbar?.setOnClickListener { findNavController().navigate( R.id.navigation_settings_extensions_to_navigation_settings_plugins, PluginsFragment.newInstance( @@ -173,16 +183,18 @@ class ExtensionsFragment : Fragment() { } val addRepositoryClick = View.OnClickListener { + val ctx = context ?: return@OnClickListener + val binding = AddRepoInputBinding.inflate(LayoutInflater.from(ctx), null, false) val builder = - AlertDialog.Builder(context ?: return@OnClickListener, R.style.AlertDialogCustom) - .setView(R.layout.add_repo_input) + AlertDialog.Builder(ctx, R.style.AlertDialogCustom) + .setView(binding.root) val dialog = builder.create() dialog.show() (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)?.primaryClip?.getItemAt( 0 )?.text?.toString()?.let { copy -> - dialog.repo_url_input?.setText(copy) + binding.repoUrlInput.setText(copy) } // dialog.list_repositories?.setOnClickListener { @@ -192,10 +204,10 @@ class ExtensionsFragment : Fragment() { // } // dialog.text2?.text = provider.name - dialog.apply_btt?.setOnClickListener secondListener@{ - val name = dialog.repo_name_input?.text?.toString() + binding.applyBtt.setOnClickListener secondListener@{ + val name = binding.repoNameInput.text?.toString() ioSafe { - val url = dialog.repo_url_input?.text?.toString() + val url = binding.repoUrlInput.text?.toString() ?.let { it1 -> RepositoryManager.parseRepoUrl(it1) } if (url.isNullOrBlank()) { main { @@ -214,22 +226,23 @@ class ExtensionsFragment : Fragment() { } dialog.dismissSafe(activity) } - dialog.cancel_btt?.setOnClickListener { + binding.cancelBtt.setOnClickListener { dialog.dismissSafe(activity) } } val isTv = isTrueTvSettings() - add_repo_button?.isGone = isTv - add_repo_button_imageview_holder?.isVisible = isTv + binding?.apply { + addRepoButton.isGone = isTv + addRepoButtonImageviewHolder.isVisible = isTv - // Band-aid for Fire TV - plugin_storage_appbar?.isFocusableInTouchMode = isTv - add_repo_button_imageview?.isFocusableInTouchMode = isTv - - add_repo_button?.setOnClickListener(addRepositoryClick) - add_repo_button_imageview?.setOnClickListener(addRepositoryClick) + // Band-aid for Fire TV + pluginStorageAppbar.isFocusableInTouchMode = isTv + addRepoButtonImageview.isFocusableInTouchMode = isTv + addRepoButton.setOnClickListener(addRepositoryClick) + addRepoButtonImageview.setOnClickListener(addRepositoryClick) + } reloadRepositories() } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt index e90166a8..602b45e4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt @@ -1,14 +1,15 @@ package com.lagradost.cloudstream3.ui.settings.extensions import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.RepositoryItemBinding +import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings -import kotlinx.android.synthetic.main.repository_item.view.* class RepoAdapter( val isSetup: Boolean, @@ -20,9 +21,17 @@ class RepoAdapter( private val repositories: MutableList = mutableListOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val layout = if(isTrueTvSettings()) R.layout.repository_item_tv else R.layout.repository_item + val layout = if (isTrueTvSettings()) RepositoryItemTvBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) else RepositoryItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) //R.layout.repository_item_tv else R.layout.repository_item return RepoViewHolder( - LayoutInflater.from(parent.context).inflate(layout, parent, false) + layout ) } @@ -57,30 +66,57 @@ class RepoAdapter( diffResult.dispatchUpdatesTo(this) } - inner class RepoViewHolder(itemView: View) : - RecyclerView.ViewHolder(itemView) { + inner class RepoViewHolder( + val binding: ViewBinding + ) : + RecyclerView.ViewHolder(binding.root) { fun bind( repositoryData: RepositoryData ) { val isPrebuilt = PREBUILT_REPOSITORIES.contains(repositoryData) val drawable = if (isSetup) R.drawable.netflix_download else R.drawable.ic_baseline_delete_outline_24 + when (binding) { + is RepositoryItemTvBinding -> { + binding.apply { + // Only shows icon if on setup or if it isn't a prebuilt repo. + // No delete buttons on prebuilt repos. + if (!isPrebuilt || isSetup) { + actionButton.setImageResource(drawable) + } - // Only shows icon if on setup or if it isn't a prebuilt repo. - // No delete buttons on prebuilt repos. - if (!isPrebuilt || isSetup) { - itemView.action_button?.setImageResource(drawable) - } + actionButton.setOnClickListener { + imageClickCallback(repositoryData) + } - itemView.action_button?.setOnClickListener { - imageClickCallback(repositoryData) - } + repositoryItemRoot.setOnClickListener { + clickCallback(repositoryData) + } + mainText.text = repositoryData.name + subText.text = repositoryData.url + } + } - itemView.repository_item_root?.setOnClickListener { - clickCallback(repositoryData) + is RepositoryItemBinding -> { + binding.apply { + // Only shows icon if on setup or if it isn't a prebuilt repo. + // No delete buttons on prebuilt repos. + if (!isPrebuilt || isSetup) { + actionButton.setImageResource(drawable) + } + + actionButton.setOnClickListener { + imageClickCallback(repositoryData) + } + + repositoryItemRoot.setOnClickListener { + clickCallback(repositoryData) + } + mainText.text = repositoryData.name + subText.text = repositoryData.url + } + } } - itemView.main_text?.text = repositoryData.name - itemView.sub_text?.text = repositoryData.url } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt index 34cd67cd..59b1b856 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt @@ -1,97 +1,105 @@ package com.lagradost.cloudstream3.ui.settings.testing import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentTestingBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import kotlinx.android.synthetic.main.fragment_testing.* -import kotlinx.android.synthetic.main.view_test.* class TestFragment : Fragment() { private val testViewModel: TestViewModel by activityViewModels() + var binding: FragmentTestingBinding? = null + + override fun onDestroyView() { + binding = null + super.onDestroyView() + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { setUpToolbar(R.string.category_provider_test) super.onViewCreated(view, savedInstanceState) - provider_test_recycler_view?.adapter = TestResultAdapter( - mutableListOf() - ) + binding?.apply { + providerTestRecyclerView.adapter = TestResultAdapter( + mutableListOf() + ) - testViewModel.init() - if (testViewModel.isRunningTest) { - provider_test?.setState(TestView.TestState.Running) - } - - observe(testViewModel.providerProgress) { (passed, failed, total) -> - provider_test?.setProgress(passed, failed, total) - } - - observeNullable(testViewModel.providerResults) { - normalSafeApiCall { - val newItems = it.sortedBy { api -> api.first.name } - (provider_test_recycler_view?.adapter as? TestResultAdapter)?.updateList( - newItems - ) + testViewModel.init() + if (testViewModel.isRunningTest) { + providerTest.setState(TestView.TestState.Running) } - } - provider_test?.setOnPlayButtonListener { state -> - when (state) { - TestView.TestState.Stopped -> testViewModel.stopTest() - TestView.TestState.Running -> testViewModel.startTest() - TestView.TestState.None -> testViewModel.startTest() + observe(testViewModel.providerProgress) { (passed, failed, total) -> + providerTest.setProgress(passed, failed, total) } - } - if (isTrueTvSettings()) { - tests_play_pause?.isFocusableInTouchMode = true - tests_play_pause?.requestFocus() - } - - provider_test?.playPauseButton?.setOnFocusChangeListener { _, hasFocus -> - if (hasFocus) { - provider_test_appbar?.setExpanded(true, true) + observeNullable(testViewModel.providerResults) { + normalSafeApiCall { + val newItems = it.sortedBy { api -> api.first.name } + (providerTestRecyclerView.adapter as? TestResultAdapter)?.updateList( + newItems + ) + } + } + + providerTest.setOnPlayButtonListener { state -> + when (state) { + TestView.TestState.Stopped -> testViewModel.stopTest() + TestView.TestState.Running -> testViewModel.startTest() + TestView.TestState.None -> testViewModel.startTest() + } } - } - fun focusRecyclerView() { - // Hack to make it possible to focus the recyclerview. if (isTrueTvSettings()) { - provider_test_recycler_view?.requestFocus() - provider_test_appbar?.setExpanded(false, true) + providerTest.playPauseButton?.isFocusableInTouchMode = true + providerTest.playPauseButton?.requestFocus() } - } - provider_test?.setOnMainClick { - testViewModel.setFilterMethod(TestViewModel.ProviderFilter.All) - focusRecyclerView() - } - provider_test?.setOnFailedClick { - testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Failed) - focusRecyclerView() - } - provider_test?.setOnPassedClick { - testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Passed) - focusRecyclerView() + providerTest.playPauseButton?.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + providerTestAppbar.setExpanded(true, true) + } + } + + fun focusRecyclerView() { + // Hack to make it possible to focus the recyclerview. + if (isTrueTvSettings()) { + providerTestRecyclerView.requestFocus() + providerTestAppbar.setExpanded(false, true) + } + } + + providerTest.setOnMainClick { + testViewModel.setFilterMethod(TestViewModel.ProviderFilter.All) + focusRecyclerView() + } + providerTest.setOnFailedClick { + testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Failed) + focusRecyclerView() + } + providerTest.setOnPassedClick { + testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Passed) + focusRecyclerView() + } } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_testing, container, false) + ): View { + val localBinding = FragmentTestingBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root//inflater.inflate(R.layout.fragment_testing, container, false) } } \ No newline at end of file From c3296f3210bf5268b1f098d2a7a86e2457cec6ea Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Fri, 14 Jul 2023 21:33:14 +0200 Subject: [PATCH 010/156] fixed bug with source priority --- .../com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index fd29d998..da0e43d3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -209,7 +209,7 @@ class GeneratorPlayer : FullScreenPlayer() { closestQuality(linkData?.quality) ) val sourcePriority = - QualityDataHelper.getSourcePriority(qualityProfile, linkData?.name) + QualityDataHelper.getSourcePriority(qualityProfile, linkData?.source) // negative because we want to sort highest quality first return qualityPriority + sourcePriority @@ -1410,4 +1410,4 @@ class GeneratorPlayer : FullScreenPlayer() { } } } -} \ No newline at end of file +} From 647e91bc4b850bcae227e28e4a638b7162d8fc7b Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 14 Jul 2023 21:43:46 +0200 Subject: [PATCH 011/156] more views + MainActivity viewbindings --- app/build.gradle.kts | 4 +- .../lagradost/cloudstream3/AcraApplication.kt | 4 +- .../lagradost/cloudstream3/MainActivity.kt | 140 +++++++++++------- .../cloudstream3/ui/home/HomeFragment.kt | 22 +-- .../ui/library/LibraryScrollTransformer.kt | 4 +- .../source_priority/SourcePriorityDialog.kt | 27 ++-- .../cloudstream3/ui/search/SearchAdaptor.kt | 14 +- .../ui/settings/extensions/PluginAdapter.kt | 82 +++++----- .../lagradost/cloudstream3/utils/UIHelper.kt | 51 ++++++- app/src/main/res/layout/activity_main_tv.xml | 16 ++ app/src/main/res/values/strings.xml | 1 + 11 files changed, 237 insertions(+), 128 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 86d91147..d5364045 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -208,7 +208,7 @@ dependencies { implementation("com.github.discord:OverlappingPanels:0.1.3") // debugImplementation because LeakCanary should only run in debug builds. - // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' + // debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7") // for shimmer when loading implementation("com.facebook.shimmer:shimmer:0.5.0") @@ -228,7 +228,7 @@ dependencies { // Library/extensions searching with Levenshtein distance implementation("me.xdrop:fuzzywuzzy:1.4.0") - // color pallette for images -> colors + // color palette for images -> colors implementation("androidx.palette:palette-ktx:1.0.0") } diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 0351b1ff..76b2321f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -51,8 +51,8 @@ class CustomReportSender : ReportSender { thread { // to not run it on main thread runBlocking { suspendSafeApiCall { - val post = app.post(url, data = data) - println("Report response: $post") + app.post(url, data = data) + //println("Report response: $post") } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index f409c10f..4a7a28ad 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -32,6 +32,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.google.android.gms.cast.framework.* +import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.snackbar.Snackbar @@ -49,6 +50,9 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.updateLocale +import com.lagradost.cloudstream3.databinding.ActivityMainBinding +import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding +import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.plugins.PluginManager @@ -74,6 +78,7 @@ import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST import com.lagradost.cloudstream3.ui.result.setImage import com.lagradost.cloudstream3.ui.result.setText +import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.SearchFragment import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings @@ -110,9 +115,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser -import kotlinx.android.synthetic.main.activity_main.* -import kotlinx.android.synthetic.main.bottom_resultview_preview.* -import kotlinx.android.synthetic.main.fragment_result_swipe.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.io.File @@ -334,8 +336,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // Use both navigation views to support both layouts. // It might be better to use the QuickSearch. - nav_view?.selectedItemId = R.id.navigation_search - nav_rail_view?.selectedItemId = R.id.navigation_search + activity?.findViewById(R.id.nav_view)?.selectedItemId = + R.id.navigation_search + activity?.findViewById(R.id.nav_rail_view)?.selectedItemId = + R.id.navigation_search } else if (safeURI(str)?.scheme == appStringPlayer) { val uri = Uri.parse(str) val name = uri.getQueryParameter("name") @@ -412,7 +416,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { this.hideKeyboard() // Fucks up anime info layout since that has its own layout - cast_mini_controller_holder?.isVisible = + binding?.castMiniControllerHolder?.isVisible = !listOf( R.id.navigation_results_phone, R.id.navigation_results_tv, @@ -448,7 +452,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { R.id.navigation_player, ).contains(destination.id) - nav_host_fragment?.apply { + binding?.navHostFragment?.apply { val params = layoutParams as ConstraintLayout.LayoutParams params.setMargins( @@ -464,21 +468,24 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { Configuration.ORIENTATION_LANDSCAPE -> { true } + Configuration.ORIENTATION_PORTRAIT -> { - false + isTvSettings() } + else -> { false } } + binding?.apply { + navView.isVisible = isNavVisible && !landscape + navRailView.isVisible = isNavVisible && landscape - nav_view?.isVisible = isNavVisible && !landscape - nav_rail_view?.isVisible = isNavVisible && landscape - - // Hide library on TV since it is not supported yet :( - val isTrueTv = isTrueTvSettings() - nav_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv - nav_rail_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + // Hide library on TV since it is not supported yet :( + val isTrueTv = isTrueTvSettings() + navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + } } //private var mCastSession: CastSession? = null @@ -691,28 +698,37 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } private fun hidePreviewPopupDialog() { - viewModel.clear() bottomPreviewPopup.dismissSafe(this) + bottomPreviewPopup = null + bottomPreviewBinding = null } - var bottomPreviewPopup: BottomSheetDialog? = null - private fun showPreviewPopupDialog(): BottomSheetDialog { - val ret = (bottomPreviewPopup ?: run { + private var bottomPreviewPopup: BottomSheetDialog? = null + private var bottomPreviewBinding: BottomResultviewPreviewBinding? = null + private fun showPreviewPopupDialog(): BottomResultviewPreviewBinding { + val ret = (bottomPreviewBinding ?: run { val builder = BottomSheetDialog(this) - builder.setContentView(R.layout.bottom_resultview_preview) + val binding: BottomResultviewPreviewBinding = + BottomResultviewPreviewBinding.inflate(builder.layoutInflater, null, false) + bottomPreviewBinding = binding + builder.setContentView(binding.root) builder.setOnDismissListener { bottomPreviewPopup = null + bottomPreviewBinding = null viewModel.clear() } builder.setCanceledOnTouchOutside(true) builder.show() - builder + bottomPreviewPopup = builder + binding }) - bottomPreviewPopup = ret + return ret } + var binding: ActivityMainBinding? = null + override fun onCreate(savedInstanceState: Bundle?) { app.initClient(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) @@ -744,10 +760,20 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) updateTv() - if (isTvSettings()) { - setContentView(R.layout.activity_main_tv) - } else { - setContentView(R.layout.activity_main) + // just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH + binding = try { + if (isTvSettings()) { + val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false) + setContentView(newLocalBinding.root) + ActivityMainBinding.bind(newLocalBinding.root) // this may crash + } else { + val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false) + setContentView(newLocalBinding.root) + newLocalBinding + } + } catch (t: Throwable) { + showToast(this, txt(R.string.unable_to_inflate, t.message ?: ""), Toast.LENGTH_LONG) + null } changeStatusBarState(isEmulatorSettings()) @@ -832,41 +858,44 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { observeNullable(viewModel.page) { resource -> if (resource == null) { - bottomPreviewPopup.dismissSafe(this) + hidePreviewPopupDialog() return@observeNullable } when (resource) { is Resource.Failure -> { showToast(this, R.string.error) + viewModel.clear() hidePreviewPopupDialog() } + is Resource.Loading -> { showPreviewPopupDialog().apply { - resultview_preview_loading?.isVisible = true - resultview_preview_result?.isVisible = false - resultview_preview_loading_shimmer?.startShimmer() + resultviewPreviewLoading.isVisible = true + resultviewPreviewResult.isVisible = false + resultviewPreviewLoadingShimmer.startShimmer() } } + is Resource.Success -> { val d = resource.value showPreviewPopupDialog().apply { - resultview_preview_loading?.isVisible = false - resultview_preview_result?.isVisible = true - resultview_preview_loading_shimmer?.stopShimmer() + resultviewPreviewLoading.isVisible = false + resultviewPreviewResult.isVisible = true + resultviewPreviewLoadingShimmer.stopShimmer() - resultview_preview_title?.text = d.title + resultviewPreviewTitle.text = d.title - resultview_preview_meta_type.setText(d.typeText) - resultview_preview_meta_year.setText(d.yearText) - resultview_preview_meta_duration.setText(d.durationText) - resultview_preview_meta_rating.setText(d.ratingText) + resultviewPreviewMetaType.setText(d.typeText) + resultviewPreviewMetaYear.setText(d.yearText) + resultviewPreviewMetaDuration.setText(d.durationText) + resultviewPreviewMetaRating.setText(d.ratingText) - resultview_preview_description?.setText(d.plotText) - resultview_preview_poster?.setImage( + resultviewPreviewDescription.setText(d.plotText) + resultviewPreviewPoster.setImage( d.posterImage ?: d.posterBackgroundImage ) - resultview_preview_poster?.setOnClickListener { + resultviewPreviewPoster.setOnClickListener { //viewModel.updateWatchStatus(WatchType.PLANTOWATCH) val value = viewModel.watchStatus.value ?: WatchType.NONE @@ -882,7 +911,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } if (!isTvSettings()) // dont want this clickable on tv layout - resultview_preview_description?.setOnClickListener { view -> + resultviewPreviewDescription.setOnClickListener { view -> view.context?.let { ctx -> val builder: AlertDialog.Builder = AlertDialog.Builder(ctx, R.style.AlertDialogCustom) @@ -892,7 +921,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } - resultview_preview_more_info?.setOnClickListener { + resultviewPreviewMoreInfo.setOnClickListener { + viewModel.clear() hidePreviewPopupDialog() lastPopup?.let { loadSearchResult(it) @@ -964,22 +994,22 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { .setPopExitAnim(R.anim.nav_pop_exit) .setPopUpTo(navController.graph.startDestination, false) .build()*/ - nav_view?.setupWithNavController(navController) - val nav_rail = findViewById(R.id.nav_rail_view) - nav_rail?.setupWithNavController(navController) + binding?.navView?.setupWithNavController(navController) + val navRail = findViewById(R.id.nav_rail_view) + navRail?.setupWithNavController(navController) if (isTvSettings()) { - nav_rail?.background?.alpha = 200 + navRail?.background?.alpha = 200 } else { - nav_rail?.background?.alpha = 255 + navRail?.background?.alpha = 255 } - nav_rail?.setOnItemSelectedListener { item -> + navRail?.setOnItemSelectedListener { item -> onNavDestinationSelected( item, navController ) } - nav_view?.setOnItemSelectedListener { item -> + binding?.navView?.setOnItemSelectedListener { item -> onNavDestinationSelected( item, navController @@ -1010,16 +1040,16 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { }*/ val rippleColor = ColorStateList.valueOf(getResourceColor(R.attr.colorPrimary, 0.1f)) - nav_view?.itemRippleColor = rippleColor - nav_rail?.itemRippleColor = rippleColor - nav_rail?.itemActiveIndicatorColor = rippleColor - nav_view?.itemActiveIndicatorColor = rippleColor + binding?.navView?.itemRippleColor = rippleColor + navRail?.itemRippleColor = rippleColor + navRail?.itemActiveIndicatorColor = rippleColor + binding?.navView?.itemActiveIndicatorColor = rippleColor if (!checkWrite()) { requestRW() if (checkWrite()) return } - CastButtonFactory.setUpMediaRouteButton(this, media_route_button) + //CastButtonFactory.setUpMediaRouteButton(this, media_route_button) // THIS IS CURRENTLY REMOVED BECAUSE HIGHER VERS OF ANDROID NEEDS A NOTIFICATION //if (!VideoDownloadManager.isMyServiceRunning(this, VideoDownloadKeepAliveService::class.java)) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 99ce7c3b..f47432dc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -31,6 +31,7 @@ import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent import com.lagradost.cloudstream3.MainActivity.Companion.bookmarksUpdatedEvent import com.lagradost.cloudstream3.MainActivity.Companion.mainPluginsLoadedEvent @@ -45,6 +46,7 @@ import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment +import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.* import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings @@ -431,22 +433,20 @@ class HomeFragment : Fragment() { ): View? { //homeViewModel = // ViewModelProvider(this).get(HomeViewModel::class.java) + bottomSheetDialog?.ownShow() val layout = if (isTvSettings()) R.layout.fragment_home_tv else R.layout.fragment_home - - /* val binding = FragmentHomeTvBinding.inflate(layout, container, false) - binding.homeLoadingError - - val binding2 = FragmentHomeBinding.inflate(layout, container, false) - binding2.homeLoadingError*/ val root = inflater.inflate(layout, container, false) - binding = FragmentHomeBinding.bind(root) - //val localBinding = FragmentHomeBinding.inflate(inflater) - //binding = localBinding - return root + binding = try { + FragmentHomeBinding.bind(root) + } catch (t : Throwable) { + showToast(activity, txt(R.string.unable_to_inflate, t.message ?: ""), Toast.LENGTH_LONG) + logError(t) + null + } - //return inflater.inflate(layout, container, false) + return root } override fun onDestroyView() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryScrollTransformer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryScrollTransformer.kt index 8aafbdd6..c3cee183 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryScrollTransformer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryScrollTransformer.kt @@ -2,13 +2,13 @@ package com.lagradost.cloudstream3.ui.library import android.view.View import androidx.viewpager2.widget.ViewPager2 -import kotlinx.android.synthetic.main.library_viewpager_page.view.* +import com.lagradost.cloudstream3.R import kotlin.math.roundToInt class LibraryScrollTransformer : ViewPager2.PageTransformer { override fun transformPage(page: View, position: Float) { val padding = (-position * page.width).roundToInt() - page.page_recyclerview.setPadding( + page.findViewById(R.id.page_recyclerview).setPadding( padding, 0, -padding, 0 ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt index efc1f1b8..1b59882e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt @@ -2,24 +2,18 @@ package com.lagradost.cloudstream3.ui.player.source_priority import android.app.Dialog import android.content.Context -import android.view.View -import android.widget.EditText -import android.widget.TextView +import android.view.LayoutInflater import androidx.annotation.StyleRes import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.FragmentActivity -import androidx.recyclerview.widget.RecyclerView -import androidx.work.impl.constraints.controllers.ConstraintController import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.PlayerSelectSourcePriorityBinding import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import kotlinx.android.synthetic.main.player_select_source_priority.* class SourcePriorityDialog( - ctx: Context, + val ctx: Context, @StyleRes themeRes: Int, val links: List, private val profile: QualityDataHelper.QualityProfile, @@ -30,13 +24,14 @@ class SourcePriorityDialog( private val updatedCallback: () -> Unit ) : Dialog(ctx, themeRes) { override fun show() { - setContentView(R.layout.player_select_source_priority) - val sourcesRecyclerView: RecyclerView = sort_sources - val qualitiesRecyclerView: RecyclerView = sort_qualities - val profileText: EditText = profile_text_editable - val saveBtt: View = save_btt - val exitBtt: View = close_btt - val helpBtt: View = help_btt + val binding = PlayerSelectSourcePriorityBinding.inflate(LayoutInflater.from(ctx), null, false) + setContentView(binding.root) + val sourcesRecyclerView = binding.sortSources + val qualitiesRecyclerView = binding.sortQualities + val profileText = binding.profileTextEditable + val saveBtt = binding.saveBtt + val exitBtt = binding.closeBtt + val helpBtt = binding.helpBtt profileText.setText(QualityDataHelper.getProfileName(profile.id).asString(context)) profileText.hint = txt(R.string.profile_number, profile.id).asString(context) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt index 233614dd..b516348d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt @@ -38,15 +38,15 @@ class SearchAdapter( var hasNext: Boolean = false override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - + val inflater = LayoutInflater.from(parent.context) val layout = if (parent.context.IsBottomLayout()) SearchResultGridExpandedBinding.inflate( - LayoutInflater.from(parent.context), + inflater, parent, false ) else SearchResultGridBinding.inflate( - LayoutInflater.from(parent.context), + inflater, parent, false ) //R.layout.search_result_grid_expanded else R.layout.search_result_grid @@ -95,9 +95,15 @@ class SearchAdapter( private val coverHeight: Int = if (compactView) 80.toPx else (resView.itemWidth / 0.68).roundToInt() + private val cardView = when(binding) { + is SearchResultGridExpandedBinding -> binding.imageView + is SearchResultGridBinding -> binding.imageView + else -> null + } + fun bind(card: SearchResponse, position: Int) { if (!compactView) { - binding.root.apply { + cardView?.apply { layoutParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, coverHeight diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index 0c3d481b..eb0082b8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.ui.settings.extensions import android.text.format.Formatter.formatShortFileSize import android.util.Log import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone @@ -13,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.RepositoryItemBinding import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.VotingApi.getVotes import com.lagradost.cloudstream3.ui.result.setText @@ -26,10 +26,11 @@ import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.android.synthetic.main.repository_item.view.* import org.junit.Assert import org.junit.Test import java.text.DecimalFormat +import kotlin.math.floor +import kotlin.math.log10 data class PluginViewData( @@ -45,8 +46,10 @@ class PluginAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val layout = if(isTrueTvSettings()) R.layout.repository_item_tv else R.layout.repository_item + val inflated = LayoutInflater.from(parent.context).inflate(layout, parent, false) + return PluginViewHolder( - LayoutInflater.from(parent.context).inflate(layout, parent, false) + RepositoryItemBinding.bind(inflated) // may crash ) } @@ -82,8 +85,10 @@ class PluginAdapter( // Clear glide image because setImageResource doesn't override override fun onViewRecycled(holder: RecyclerView.ViewHolder) { - holder.itemView.entry_icon?.let { pluginIcon -> - GlideApp.with(pluginIcon).clear(pluginIcon) + if (holder is PluginViewHolder) { + holder.binding.entryIcon.let { pluginIcon -> + GlideApp.with(pluginIcon).clear(pluginIcon) + } } super.onViewRecycled(holder) } @@ -112,7 +117,7 @@ class PluginAdapter( fun prettyCount(number: Number): String? { val suffix = charArrayOf(' ', 'k', 'M', 'B', 'T', 'P', 'E') val numValue = number.toLong() - val value = Math.floor(Math.log10(numValue.toDouble())).toInt() + val value = floor(log10(numValue.toDouble())).toInt() val base = value / 3 return if (value >= 3 && base < suffix.size) { DecimalFormat("#0.00").format( @@ -127,8 +132,8 @@ class PluginAdapter( } } - inner class PluginViewHolder(itemView: View) : - RecyclerView.ViewHolder(itemView) { + inner class PluginViewHolder(val binding: RepositoryItemBinding) : + RecyclerView.ViewHolder(binding.root) { fun bind( data: PluginViewData, @@ -138,17 +143,17 @@ class PluginAdapter( val name = metadata.name.removeSuffix("Provider") val alpha = if (disabled) 0.6f else 1f val isLocal = !data.plugin.second.url.startsWith("http") - itemView.main_text?.alpha = alpha - itemView.sub_text?.alpha = alpha + binding.mainText.alpha = alpha + binding.subText.alpha = alpha val drawableInt = if (data.isDownloaded) R.drawable.ic_baseline_delete_outline_24 else R.drawable.netflix_download - itemView.nsfw_marker?.isVisible = metadata.tvTypes?.contains("NSFW") ?: false - itemView.action_button?.setImageResource(drawableInt) + binding.nsfwMarker.isVisible = metadata.tvTypes?.contains("NSFW") ?: false + binding.actionButton.setImageResource(drawableInt) - itemView.action_button?.setOnClickListener { + binding.actionButton.setOnClickListener { iconClickCallback.invoke(data.plugin) } itemView.setOnClickListener { @@ -169,10 +174,11 @@ class PluginAdapter( if (data.isDownloaded) { // On local plugins page the filepath is provided instead of url. - val plugin = PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url] + val plugin = + PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url] if (plugin?.openSettings != null) { - itemView.action_settings?.isVisible = true - itemView.action_settings.setOnClickListener { + binding.actionSettings.isVisible = true + binding.actionSettings.setOnClickListener { try { plugin.openSettings!!.invoke(itemView.context) } catch (e: Throwable) { @@ -185,13 +191,13 @@ class PluginAdapter( } } } else { - itemView.action_settings?.isVisible = false + binding.actionSettings.isVisible = false } } else { - itemView.action_settings?.isVisible = false + binding.actionSettings.isVisible = false } - if (itemView.entry_icon?.setImage(//itemView.entry_icon?.height ?: + if (!binding.entryIcon.setImage(//itemView.entry_icon?.height ?: metadata.iconUrl?.replace( "%size%", "$iconSize" @@ -201,41 +207,47 @@ class PluginAdapter( ), null, errorImageDrawable = R.drawable.ic_baseline_extension_24 - ) != true + ) ) { - itemView.entry_icon?.setImageResource(R.drawable.ic_baseline_extension_24) + binding.entryIcon.setImageResource(R.drawable.ic_baseline_extension_24) } - itemView.ext_version?.isVisible = true - itemView.ext_version?.text = "v${metadata.version}" + binding.extVersion.isVisible = true + binding.extVersion.text = "v${metadata.version}" if (metadata.language.isNullOrBlank()) { - itemView.lang_icon?.isVisible = false + binding.langIcon.isVisible = false } else { - itemView.lang_icon?.isVisible = true - itemView.lang_icon.text = "${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}" + binding.langIcon.isVisible = true + binding.langIcon.text = + "${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}" } - itemView.ext_votes?.isVisible = false + binding.extVotes.isVisible = false if (!isLocal) { ioSafe { metadata.getVotes().main { - itemView.ext_votes?.setText(txt(R.string.extension_rating, prettyCount(it))) - itemView.ext_votes?.isVisible = true + binding.extVotes.setText(txt(R.string.extension_rating, prettyCount(it))) + binding.extVotes.isVisible = true } } } if (metadata.fileSize != null) { - itemView.ext_filesize?.isVisible = true - itemView.ext_filesize?.text = formatShortFileSize(itemView.context, metadata.fileSize) + binding.extFilesize.isVisible = true + binding.extFilesize.text = formatShortFileSize(itemView.context, metadata.fileSize) } else { - itemView.ext_filesize?.isVisible = false + binding.extFilesize.isVisible = false } - itemView.main_text.setText(if(disabled) txt(R.string.single_plugin_disabled, name) else txt(name)) - itemView.sub_text?.isGone = metadata.description.isNullOrBlank() - itemView.sub_text?.text = metadata.description.html() + binding.mainText.setText( + if (disabled) txt( + R.string.single_plugin_disabled, + name + ) else txt(name) + ) + binding.subText.isGone = metadata.description.isNullOrBlank() + binding.subText.text = metadata.description.html() } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index 4ed1aee6..3be4b190 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -181,6 +181,50 @@ object UIHelper { } } + /*inline fun bindViewBinding( + inflater: LayoutInflater?, + container: ViewGroup?, + layout: Int + ): Pair { + return try { + val localInflater = inflater ?: container?.context?.let { LayoutInflater.from(it) } + ?: return null to txt( + R.string.unable_to_inflate, + "Requires inflater OR container" + )//throw IllegalArgumentException("Requires inflater OR container")) + + //println("methods: ${T::class.java.methods.map { it.name }}") + val bind = T::class.java.methods.first { it.name == "bind" } + //val inflate = T::class.java.methods.first { it.name == "inflate" } + val root = localInflater.inflate(layout, container, false) + bind.invoke(null, root) as T to null + } catch (t: Throwable) { + logError(t) + val message = txt(R.string.unable_to_inflate, t.message ?: "Primary constructor") + // if the desired layout is not found then we inflate the casted layout + /*try { + val localInflater = inflater ?: container?.context?.let { LayoutInflater.from(it) } + ?: return null to txt( + R.string.unable_to_inflate, + "Requires inflater OR container" + )//throw IllegalArgumentException("Requires inflater OR container")) + + // we don't know what method to use as there are 2, but first *should* always be true + return try { + val inflate = T::class.java.methods.first { it.name == "inflate" } + inflate.invoke(null, localInflater, container, false) as T + } catch (_: Throwable) { + val inflate = T::class.java.methods.last { it.name == "inflate" } + inflate.invoke(null, localInflater, container, false) as T + } to message + } catch (t: Throwable) { + logError(t) + }*/ + + null to message + } + }*/ + fun ImageView?.setImage( url: String?, headers: Map? = null, @@ -190,7 +234,12 @@ object UIHelper { colorCallback: ((Palette) -> Unit)? = null ): Boolean { if (url.isNullOrBlank()) return false - this.setImage(UiImage.Image(url, headers, errorImageDrawable), errorImageDrawable, fadeIn, colorCallback) + this.setImage( + UiImage.Image(url, headers, errorImageDrawable), + errorImageDrawable, + fadeIn, + colorCallback + ) return true } diff --git a/app/src/main/res/layout/activity_main_tv.xml b/app/src/main/res/layout/activity_main_tv.xml index dc29dec9..4e50f464 100644 --- a/app/src/main/res/layout/activity_main_tv.xml +++ b/app/src/main/res/layout/activity_main_tv.xml @@ -41,6 +41,22 @@ + + Qualities Profile background + UI was unable to be created correctly, this is a MAJOR BUG and should be reported immediately %s From 273a947f8ef46c294ff14b4be0aeae1081868e92 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 14 Jul 2023 22:05:13 +0200 Subject: [PATCH 012/156] more views --- .../cloudstream3/ui/home/HomeScrollAdapter.kt | 6 +- .../player/source_priority/PriorityAdapter.kt | 25 +- .../player/source_priority/ProfilesAdapter.kt | 29 +- .../source_priority/QualityProfileDialog.kt | 129 ++-- .../extensions/PluginDetailsFragment.kt | 130 ++-- .../ui/settings/testing/TestResultAdapter.kt | 19 +- .../ui/subtitles/SubtitlesFragment.kt | 607 +++++++++--------- 7 files changed, 491 insertions(+), 454 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt index f296e53d..5902132e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt @@ -11,9 +11,9 @@ import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.fragment_home_head_tv.* -import kotlinx.android.synthetic.main.fragment_home_head_tv.view.* -import kotlinx.android.synthetic.main.home_scroll_view.view.* +import kotlinx.android.synthetic.main.home_scroll_view.view.home_scroll_preview +import kotlinx.android.synthetic.main.home_scroll_view.view.home_scroll_preview_tags +import kotlinx.android.synthetic.main.home_scroll_view.view.home_scroll_preview_title class HomeScrollAdapter( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt index 8e0ce67c..fb60ccce 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt @@ -1,14 +1,10 @@ package com.lagradost.cloudstream3.ui.player.source_priority import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.PlayerPrioritizeItemBinding import com.lagradost.cloudstream3.utils.AppUtils -import kotlinx.android.synthetic.main.player_prioritize_item.view.* data class SourcePriority( val data: T, @@ -20,7 +16,8 @@ class PriorityAdapter(override val items: MutableList>) : AppUtils.DiffAdapter>(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return PriorityViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false) + PlayerPrioritizeItemBinding.inflate(LayoutInflater.from(parent.context),parent,false), + //LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false) ) } @@ -31,27 +28,27 @@ class PriorityAdapter(override val items: MutableList>) : } class PriorityViewHolder( - itemView: View, - ) : RecyclerView.ViewHolder(itemView) { + val binding: PlayerPrioritizeItemBinding, + ) : RecyclerView.ViewHolder(binding.root) { fun bind(item: SourcePriority) { - val plusButton: ImageView = itemView.add_button + /* val plusButton: ImageView = itemView.add_button val subtractButton: ImageView = itemView.subtract_button val priorityText: TextView = itemView.priority_text - val priorityNumber: TextView = itemView.priority_number - priorityText.text = item.name + val priorityNumber: TextView = itemView.priority_number*/ + binding.priorityText.text = item.name fun updatePriority() { - priorityNumber.text = item.priority.toString() + binding.priorityNumber.text = item.priority.toString() } updatePriority() - plusButton.setOnClickListener { + binding.addButton.setOnClickListener { // If someone clicks til the integer limit then they deserve to crash. item.priority++ updatePriority() } - subtractButton.setOnClickListener { + binding.subtractButton.setOnClickListener { item.priority-- updatePriority() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt index ff84c1f5..8153d7a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt @@ -8,19 +8,13 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.core.content.ContextCompat -import androidx.core.content.res.ResourcesCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.PlayerQualityProfileItemBinding import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.player_quality_profile_item.view.card_view -import kotlinx.android.synthetic.main.player_quality_profile_item.view.outline -import kotlinx.android.synthetic.main.player_quality_profile_item.view.profile_image_background -import kotlinx.android.synthetic.main.player_quality_profile_item.view.profile_text -import kotlinx.android.synthetic.main.player_quality_profile_item.view.text_is_mobile_data -import kotlinx.android.synthetic.main.player_quality_profile_item.view.text_is_wifi class ProfilesAdapter( override val items: MutableList, @@ -34,8 +28,9 @@ class ProfilesAdapter( }) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProfilesViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.player_quality_profile_item, parent, false) + PlayerQualityProfileItemBinding.inflate(LayoutInflater.from(parent.context),parent,false) + //LayoutInflater.from(parent.context) + // .inflate(R.layout.player_quality_profile_item, parent, false) ) } @@ -52,8 +47,8 @@ class ProfilesAdapter( } inner class ProfilesViewHolder( - itemView: View, - ) : RecyclerView.ViewHolder(itemView) { + val binding: PlayerQualityProfileItemBinding, + ) : RecyclerView.ViewHolder(binding.root) { private val art = listOf( R.drawable.profile_bg_teal, R.drawable.profile_bg_blue, @@ -65,12 +60,12 @@ class ProfilesAdapter( ) fun bind(item: QualityDataHelper.QualityProfile, index: Int) { - val priorityText: TextView = itemView.profile_text - val profileBg: ImageView = itemView.profile_image_background - val wifiText: TextView = itemView.text_is_wifi - val dataText: TextView = itemView.text_is_mobile_data - val outline: View = itemView.outline - val cardView: View = itemView.card_view + val priorityText: TextView = binding.profileText + val profileBg: ImageView = binding.profileImageBackground + val wifiText: TextView = binding.textIsWifi + val dataText: TextView = binding.textIsMobileData + val outline: View = binding.outline + val cardView: View = binding.cardView priorityText.text = item.name.asString(itemView.context) dataText.isVisible = item.type == QualityDataHelper.QualityProfileType.Data diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt index 28a6365f..e3629158 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt @@ -1,20 +1,16 @@ package com.lagradost.cloudstream3.ui.player.source_priority import android.app.Dialog -import android.view.View -import android.widget.TextView import androidx.annotation.StyleRes -import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity -import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.PlayerQualityProfileDialogBinding import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfileName import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfiles import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import kotlinx.android.synthetic.main.player_quality_profile_dialog.* class QualityProfileDialog( val activity: FragmentActivity, @@ -24,83 +20,86 @@ class QualityProfileDialog( private val profileSelectionCallback: (QualityDataHelper.QualityProfile) -> Unit ) : Dialog(activity, themeRes) { override fun show() { - setContentView(R.layout.player_quality_profile_dialog) - val profilesRecyclerView: RecyclerView = profiles_recyclerview + + val binding = PlayerQualityProfileDialogBinding.inflate(this.layoutInflater, null, false) + + setContentView(binding.root)//R.layout.player_quality_profile_dialog) + /*val profilesRecyclerView: RecyclerView = profiles_recyclerview val useBtt: View = use_btt val editBtt: View = edit_btt val cancelBtt: View = cancel_btt val defaultBtt: View = set_default_btt val currentProfileText: TextView = currently_selected_profile_text - val selectedItemActionsHolder: View = selected_item_holder - - fun getCurrentProfile(): QualityDataHelper.QualityProfile? { - return (profilesRecyclerView.adapter as? ProfilesAdapter)?.getCurrentProfile() - } - - fun refreshProfiles() { - currentProfileText.text = getProfileName(usedProfile).asString(context) - (profilesRecyclerView.adapter as? ProfilesAdapter)?.updateList(getProfiles()) - } - - profilesRecyclerView.adapter = ProfilesAdapter( - mutableListOf(), - usedProfile, - ) { oldIndex: Int?, newIndex: Int -> - profilesRecyclerView.adapter?.notifyItemChanged(newIndex) - selectedItemActionsHolder.alpha = 1f - if (oldIndex != null) { - profilesRecyclerView.adapter?.notifyItemChanged(oldIndex) + val selectedItemActionsHolder: View = selected_item_holder*/ + binding.apply { + fun getCurrentProfile(): QualityDataHelper.QualityProfile? { + return (profilesRecyclerview.adapter as? ProfilesAdapter)?.getCurrentProfile() } - } - refreshProfiles() - - editBtt.setOnClickListener { - getCurrentProfile()?.let { profile -> - SourcePriorityDialog(context, themeRes, links, profile) { - refreshProfiles() - }.show() + fun refreshProfiles() { + currentlySelectedProfileText.text = getProfileName(usedProfile).asString(context) + (profilesRecyclerview.adapter as? ProfilesAdapter)?.updateList(getProfiles()) + } + + profilesRecyclerview.adapter = ProfilesAdapter( + mutableListOf(), + usedProfile, + ) { oldIndex: Int?, newIndex: Int -> + profilesRecyclerview.adapter?.notifyItemChanged(newIndex) + selectedItemHolder.alpha = 1f + if (oldIndex != null) { + profilesRecyclerview.adapter?.notifyItemChanged(oldIndex) + } + } + + refreshProfiles() + + editBtt.setOnClickListener { + getCurrentProfile()?.let { profile -> + SourcePriorityDialog(context, themeRes, links, profile) { + refreshProfiles() + }.show() + } } - } - defaultBtt.setOnClickListener { - val currentProfile = getCurrentProfile() ?: return@setOnClickListener - val choices = QualityDataHelper.QualityProfileType.values() - .filter { it != QualityDataHelper.QualityProfileType.None } - val choiceNames = choices.map { txt(it.stringRes).asString(context) } + setDefaultBtt.setOnClickListener { + val currentProfile = getCurrentProfile() ?: return@setOnClickListener + val choices = QualityDataHelper.QualityProfileType.values() + .filter { it != QualityDataHelper.QualityProfileType.None } + val choiceNames = choices.map { txt(it.stringRes).asString(context) } - activity.showBottomDialog( - choiceNames, - choices.indexOf(currentProfile.type), - txt(R.string.set_default).asString(context), - false, - {}, - { index -> - val pickedChoice = choices.getOrNull(index) ?: return@showBottomDialog - // Remove previous picks - if (pickedChoice.unique) { - getProfiles().filter { it.type == pickedChoice }.forEach { - QualityDataHelper.setQualityProfileType(it.id, null) + activity.showBottomDialog( + choiceNames, + choices.indexOf(currentProfile.type), + txt(R.string.set_default).asString(context), + false, + {}, + { index -> + val pickedChoice = choices.getOrNull(index) ?: return@showBottomDialog + // Remove previous picks + if (pickedChoice.unique) { + getProfiles().filter { it.type == pickedChoice }.forEach { + QualityDataHelper.setQualityProfileType(it.id, null) + } } - } - QualityDataHelper.setQualityProfileType(currentProfile.id, pickedChoice) - refreshProfiles() - }) - } + QualityDataHelper.setQualityProfileType(currentProfile.id, pickedChoice) + refreshProfiles() + }) + } - cancelBtt.setOnClickListener { - this.dismissSafe() - } + cancelBtt.setOnClickListener { + this@QualityProfileDialog.dismissSafe() + } - useBtt.setOnClickListener { - getCurrentProfile()?.let { - profileSelectionCallback.invoke(it) - this.dismissSafe() + useBtt.setOnClickListener { + getCurrentProfile()?.let { + profileSelectionCallback.invoke(it) + this@QualityProfileDialog.dismissSafe() + } } } - super.show() } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt index 9729b4de..00e1806d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt @@ -2,30 +2,29 @@ package com.lagradost.cloudstream3.ui.settings.extensions import android.content.res.ColorStateList import android.os.Bundle -import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import android.text.format.Formatter.formatFileSize +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.UIHelper.setImage -import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.android.synthetic.main.fragment_plugin_details.* -import android.text.format.Formatter.formatFileSize -import android.util.Log import androidx.core.view.isVisible +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentPluginDetailsBinding +import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.VotingApi +import com.lagradost.cloudstream3.plugins.VotingApi.canVote import com.lagradost.cloudstream3.plugins.VotingApi.getVoteType import com.lagradost.cloudstream3.plugins.VotingApi.getVotes import com.lagradost.cloudstream3.plugins.VotingApi.vote import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main -import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute -import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser -import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.plugins.VotingApi.canVote import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso -import kotlinx.android.synthetic.main.repository_item.view.* +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute +import com.lagradost.cloudstream3.utils.UIHelper.setImage +import com.lagradost.cloudstream3.utils.UIHelper.toPx class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragment() { @@ -43,18 +42,27 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen } } + override fun onDestroyView() { + binding = null + super.onDestroyView() + } + + var binding: FragmentPluginDetailsBinding? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_plugin_details, container, false) - + ): View { + val localBinding = FragmentPluginDetailsBinding.inflate(inflater, container, false) + binding = localBinding + return localBinding.root + //return inflater.inflate(R.layout.fragment_plugin_details, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val metadata = data.plugin.second - if (plugin_icon?.setImage(//plugin_icon?.height ?: + binding?.apply { + if (!pluginIcon.setImage(//plugin_icon?.height ?: metadata.iconUrl?.replace( "%size%", "$iconSize" @@ -64,23 +72,33 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen ), null, errorImageDrawable = R.drawable.ic_baseline_extension_24 - ) != true + ) ) { - plugin_icon?.setImageResource(R.drawable.ic_baseline_extension_24) + pluginIcon.setImageResource(R.drawable.ic_baseline_extension_24) } - plugin_name?.text = metadata.name.removeSuffix("Provider") - plugin_version?.text = metadata.version.toString() - plugin_description?.text = metadata.description ?: getString(R.string.no_data) - plugin_size?.text = if (metadata.fileSize == null) getString(R.string.no_data) else formatFileSize(context, metadata.fileSize) - plugin_author?.text = if (metadata.authors.isEmpty()) getString(R.string.no_data) else metadata.authors.joinToString(", ") - plugin_status?.text = resources.getStringArray(R.array.extension_statuses)[metadata.status] - plugin_types?.text = if ((metadata.tvTypes == null) || metadata.tvTypes.isEmpty()) getString(R.string.no_data) else metadata.tvTypes.joinToString(", ") - plugin_lang?.text = if (metadata.language == null) - getString(R.string.no_data) + pluginName.text = metadata.name.removeSuffix("Provider") + pluginVersion.text = metadata.version.toString() + pluginDescription.text = metadata.description ?: getString(R.string.no_data) + pluginSize.text = + if (metadata.fileSize == null) getString(R.string.no_data) else formatFileSize( + context, + metadata.fileSize + ) + pluginAuthor.text = + if (metadata.authors.isEmpty()) getString(R.string.no_data) else metadata.authors.joinToString( + ", " + ) + pluginStatus.text = resources.getStringArray(R.array.extension_statuses)[metadata.status] + pluginTypes.text = + if (metadata.tvTypes.isNullOrEmpty()) getString(R.string.no_data) else metadata.tvTypes.joinToString( + ", " + ) + pluginLang.text = if (metadata.language == null) + getString(R.string.no_data) else - "${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}" + "${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}" - github_btn.setOnClickListener { + githubBtn.setOnClickListener { if (metadata.repositoryUrl != null) { openBrowser(metadata.repositoryUrl) } @@ -93,10 +111,11 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen if (data.isDownloaded) { // On local plugins page the filepath is provided instead of url. - val plugin = PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url] + val plugin = + PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url] if (plugin?.openSettings != null && context != null) { - action_settings?.isVisible = true - action_settings.setOnClickListener { + actionSettings.isVisible = true + actionSettings.setOnClickListener { try { plugin.openSettings!!.invoke(requireContext()) } catch (e: Throwable) { @@ -109,10 +128,10 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen } } } else { - action_settings?.isVisible = false + actionSettings.isVisible = false } } else { - action_settings?.isVisible = false + actionSettings.isVisible = false } upvote.setOnClickListener { @@ -136,23 +155,40 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen updateVoting(it) } } + } } private fun updateVoting(value: Int) { val metadata = data.plugin.second - plugin_votes.text = value.toString() - when (metadata.getVoteType()) { - VotingApi.VoteType.UPVOTE -> { - upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary) - downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white) - } - VotingApi.VoteType.DOWNVOTE -> { - downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary) - upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white) - } - VotingApi.VoteType.NONE -> { - upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white) - downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white) + binding?.apply { + pluginVotes.text = value.toString() + when (metadata.getVoteType()) { + VotingApi.VoteType.UPVOTE -> { + upvote.imageTintList = ColorStateList.valueOf( + context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary + ) + downvote.imageTintList = ColorStateList.valueOf( + context?.colorFromAttribute(R.attr.white) ?: R.color.white + ) + } + + VotingApi.VoteType.DOWNVOTE -> { + downvote.imageTintList = ColorStateList.valueOf( + context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary + ) + upvote.imageTintList = ColorStateList.valueOf( + context?.colorFromAttribute(R.attr.white) ?: R.color.white + ) + } + + VotingApi.VoteType.NONE -> { + upvote.imageTintList = ColorStateList.valueOf( + context?.colorFromAttribute(R.attr.white) ?: R.color.white + ) + downvote.imageTintList = ColorStateList.valueOf( + context?.colorFromAttribute(R.attr.white) ?: R.color.white + ) + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt index d04e2379..83480542 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt @@ -10,19 +10,20 @@ import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.ProviderTestItemBinding import com.lagradost.cloudstream3.mvvm.getAllMessages import com.lagradost.cloudstream3.mvvm.getStackTracePretty import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.TestingUtils -import kotlinx.android.synthetic.main.provider_test_item.view.* class TestResultAdapter(override val items: MutableList>) : AppUtils.DiffAdapter>(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProviderTestViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.provider_test_item, parent, false), + ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent,false) + //LayoutInflater.from(parent.context) + // .inflate(R.layout.provider_test_item, parent, false), ) } @@ -35,12 +36,12 @@ class TestResultAdapter(override val items: MutableList - val suffix = "dp" - val elevationTypes = listOf( - Pair(0, textView.context.getString(R.string.none)), - Pair(10, "10$suffix"), - Pair(20, "20$suffix"), - Pair(30, "30$suffix"), - Pair(40, "40$suffix"), - Pair(50, "50$suffix"), - Pair(60, "60$suffix"), - Pair(70, "70$suffix"), - Pair(80, "80$suffix"), - Pair(90, "90$suffix"), - Pair(100, "100$suffix"), - ) - - //showBottomDialog - activity?.showDialog( - elevationTypes.map { it.second }, - elevationTypes.map { it.first }.indexOf(state.elevation), - (textView as TextView).text.toString(), - false, - dismissCallback - ) { index -> - state.elevation = elevationTypes.map { it.first }[index] - textView.context.updateState() + val dismissCallback = { if (hide) activity?.hideSystemUI() } - } - subs_subtitle_elevation.setOnLongClickListener { - state.elevation = DEF_SUBS_ELEVATION - it.context.updateState() - showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) - return@setOnLongClickListener true - } + subsSubtitleElevation.setFocusableInTv() + subsSubtitleElevation.setOnClickListener { textView -> + val suffix = "dp" + val elevationTypes = listOf( + Pair(0, textView.context.getString(R.string.none)), + Pair(10, "10$suffix"), + Pair(20, "20$suffix"), + Pair(30, "30$suffix"), + Pair(40, "40$suffix"), + Pair(50, "50$suffix"), + Pair(60, "60$suffix"), + Pair(70, "70$suffix"), + Pair(80, "80$suffix"), + Pair(90, "90$suffix"), + Pair(100, "100$suffix"), + ) - subs_edge_type.setFocusableInTv() - subs_edge_type.setOnClickListener { textView -> - val edgeTypes = listOf( - Pair( - CaptionStyleCompat.EDGE_TYPE_NONE, - textView.context.getString(R.string.subtitles_none) - ), - Pair( - CaptionStyleCompat.EDGE_TYPE_OUTLINE, - textView.context.getString(R.string.subtitles_outline) - ), - Pair( - CaptionStyleCompat.EDGE_TYPE_DEPRESSED, - textView.context.getString(R.string.subtitles_depressed) - ), - Pair( - CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW, - textView.context.getString(R.string.subtitles_shadow) - ), - Pair( - CaptionStyleCompat.EDGE_TYPE_RAISED, - textView.context.getString(R.string.subtitles_raised) - ), - ) - - //showBottomDialog - activity?.showDialog( - edgeTypes.map { it.second }, - edgeTypes.map { it.first }.indexOf(state.edgeType), - (textView as TextView).text.toString(), - false, - dismissCallback - ) { index -> - state.edgeType = edgeTypes.map { it.first }[index] - textView.context.updateState() - } - } - - subs_edge_type.setOnLongClickListener { - state.edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE - it.context.updateState() - showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) - return@setOnLongClickListener true - } - - subs_font_size.setFocusableInTv() - subs_font_size.setOnClickListener { textView -> - val suffix = "sp" - val fontSizes = listOf( - Pair(null, textView.context.getString(R.string.normal)), - Pair(6f, "6$suffix"), - Pair(7f, "7$suffix"), - Pair(8f, "8$suffix"), - Pair(9f, "9$suffix"), - Pair(10f, "10$suffix"), - Pair(11f, "11$suffix"), - Pair(12f, "12$suffix"), - Pair(13f, "13$suffix"), - Pair(14f, "14$suffix"), - Pair(15f, "15$suffix"), - Pair(16f, "16$suffix"), - Pair(17f, "17$suffix"), - Pair(18f, "18$suffix"), - Pair(19f, "19$suffix"), - Pair(20f, "20$suffix"), - Pair(21f, "21$suffix"), - Pair(22f, "22$suffix"), - Pair(23f, "23$suffix"), - Pair(24f, "24$suffix"), - Pair(25f, "25$suffix"), - Pair(26f, "26$suffix"), - Pair(28f, "28$suffix"), - Pair(30f, "30$suffix"), - Pair(32f, "32$suffix"), - Pair(34f, "34$suffix"), - Pair(36f, "36$suffix"), - Pair(38f, "38$suffix"), - Pair(40f, "40$suffix"), - Pair(42f, "42$suffix"), - Pair(44f, "44$suffix"), - Pair(48f, "48$suffix"), - Pair(60f, "60$suffix"), - ) - - //showBottomDialog - activity?.showDialog( - fontSizes.map { it.second }, - fontSizes.map { it.first }.indexOf(state.fixedTextSize), - (textView as TextView).text.toString(), - false, - dismissCallback - ) { index -> - state.fixedTextSize = fontSizes.map { it.first }[index] - //textView.context.updateState() // font size not changed - } - } - - subtitles_remove_bloat?.isChecked = state.removeBloat - subtitles_remove_bloat?.setOnCheckedChangeListener { _, b -> - state.removeBloat = b - } - subtitles_uppercase?.isChecked = state.upperCase - subtitles_uppercase?.setOnCheckedChangeListener { _, b -> - state.upperCase = b - context?.updateState() - } - - subtitles_remove_captions?.isChecked = state.removeCaptions - subtitles_remove_captions?.setOnCheckedChangeListener { _, b -> - state.removeCaptions = b - } - - subs_font_size.setOnLongClickListener { _ -> - state.fixedTextSize = null - //textView.context.updateState() // font size not changed - showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) - return@setOnLongClickListener true - } - - //Fetch current value from preference - context?.let { ctx -> - subtitles_filter_sub_lang?.isChecked = - PreferenceManager.getDefaultSharedPreferences(ctx) - .getBoolean(getString(R.string.filter_sub_lang_key), false) - } - - subtitles_filter_sub_lang?.setOnCheckedChangeListener { _, b -> - context?.let { ctx -> - PreferenceManager.getDefaultSharedPreferences(ctx) - .edit() - .putBoolean(getString(R.string.filter_sub_lang_key), b) - .apply() - } - } - - subs_font.setFocusableInTv() - subs_font.setOnClickListener { textView -> - val fontTypes = listOf( - Pair(null, textView.context.getString(R.string.normal)), - Pair(R.font.trebuchet_ms, "Trebuchet MS"), - Pair(R.font.netflix_sans, "Netflix Sans"), - Pair(R.font.google_sans, "Google Sans"), - Pair(R.font.open_sans, "Open Sans"), - Pair(R.font.futura, "Futura"), - Pair(R.font.consola, "Consola"), - Pair(R.font.gotham, "Gotham"), - Pair(R.font.lucida_grande, "Lucida Grande"), - Pair(R.font.stix_general, "STIX General"), - Pair(R.font.times_new_roman, "Times New Roman"), - Pair(R.font.verdana, "Verdana"), - Pair(R.font.ubuntu_regular, "Ubuntu"), - Pair(R.font.comic_sans, "Comic Sans"), - Pair(R.font.poppins_regular, "Poppins"), - ) - val savedFontTypes = textView.context.getSavedFonts() - - val currentIndex = - savedFontTypes.indexOfFirst { it.absolutePath == state.typefaceFilePath } - .let { index -> - if (index == -1) - fontTypes.indexOfFirst { it.first == state.typeface } - else index + fontTypes.size - } - - //showBottomDialog - activity?.showDialog( - fontTypes.map { it.second } + savedFontTypes.map { it.name }, - currentIndex, - (textView as TextView).text.toString(), - false, - dismissCallback - ) { index -> - if (index < fontTypes.size) { - state.typeface = fontTypes[index].first - state.typefaceFilePath = null - } else { - state.typefaceFilePath = savedFontTypes[index - fontTypes.size].absolutePath - state.typeface = null + //showBottomDialog + activity?.showDialog( + elevationTypes.map { it.second }, + elevationTypes.map { it.first }.indexOf(state.elevation), + (textView as TextView).text.toString(), + false, + dismissCallback + ) { index -> + state.elevation = elevationTypes.map { it.first }[index] + textView.context.updateState() + if (hide) + activity?.hideSystemUI() } + } + + subsSubtitleElevation.setOnLongClickListener { + state.elevation = DEF_SUBS_ELEVATION + it.context.updateState() + showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) + return@setOnLongClickListener true + } + + subsEdgeType.setFocusableInTv() + subsEdgeType.setOnClickListener { textView -> + val edgeTypes = listOf( + Pair( + CaptionStyleCompat.EDGE_TYPE_NONE, + textView.context.getString(R.string.subtitles_none) + ), + Pair( + CaptionStyleCompat.EDGE_TYPE_OUTLINE, + textView.context.getString(R.string.subtitles_outline) + ), + Pair( + CaptionStyleCompat.EDGE_TYPE_DEPRESSED, + textView.context.getString(R.string.subtitles_depressed) + ), + Pair( + CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW, + textView.context.getString(R.string.subtitles_shadow) + ), + Pair( + CaptionStyleCompat.EDGE_TYPE_RAISED, + textView.context.getString(R.string.subtitles_raised) + ), + ) + + //showBottomDialog + activity?.showDialog( + edgeTypes.map { it.second }, + edgeTypes.map { it.first }.indexOf(state.edgeType), + (textView as TextView).text.toString(), + false, + dismissCallback + ) { index -> + state.edgeType = edgeTypes.map { it.first }[index] + textView.context.updateState() + } + } + + subsEdgeType.setOnLongClickListener { + state.edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE + it.context.updateState() + showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) + return@setOnLongClickListener true + } + + subsFontSize.setFocusableInTv() + subsFontSize.setOnClickListener { textView -> + val suffix = "sp" + val fontSizes = listOf( + Pair(null, textView.context.getString(R.string.normal)), + Pair(6f, "6$suffix"), + Pair(7f, "7$suffix"), + Pair(8f, "8$suffix"), + Pair(9f, "9$suffix"), + Pair(10f, "10$suffix"), + Pair(11f, "11$suffix"), + Pair(12f, "12$suffix"), + Pair(13f, "13$suffix"), + Pair(14f, "14$suffix"), + Pair(15f, "15$suffix"), + Pair(16f, "16$suffix"), + Pair(17f, "17$suffix"), + Pair(18f, "18$suffix"), + Pair(19f, "19$suffix"), + Pair(20f, "20$suffix"), + Pair(21f, "21$suffix"), + Pair(22f, "22$suffix"), + Pair(23f, "23$suffix"), + Pair(24f, "24$suffix"), + Pair(25f, "25$suffix"), + Pair(26f, "26$suffix"), + Pair(28f, "28$suffix"), + Pair(30f, "30$suffix"), + Pair(32f, "32$suffix"), + Pair(34f, "34$suffix"), + Pair(36f, "36$suffix"), + Pair(38f, "38$suffix"), + Pair(40f, "40$suffix"), + Pair(42f, "42$suffix"), + Pair(44f, "44$suffix"), + Pair(48f, "48$suffix"), + Pair(60f, "60$suffix"), + ) + + //showBottomDialog + activity?.showDialog( + fontSizes.map { it.second }, + fontSizes.map { it.first }.indexOf(state.fixedTextSize), + (textView as TextView).text.toString(), + false, + dismissCallback + ) { index -> + state.fixedTextSize = fontSizes.map { it.first }[index] + //textView.context.updateState() // font size not changed + } + } + + subtitlesRemoveBloat.isChecked = state.removeBloat + subtitlesRemoveBloat.setOnCheckedChangeListener { _, b -> + state.removeBloat = b + } + subtitlesUppercase.isChecked = state.upperCase + subtitlesUppercase.setOnCheckedChangeListener { _, b -> + state.upperCase = b + context?.updateState() + } + + subtitlesRemoveCaptions.isChecked = state.removeCaptions + subtitlesRemoveCaptions.setOnCheckedChangeListener { _, b -> + state.removeCaptions = b + } + + subsFontSize.setOnLongClickListener { _ -> + state.fixedTextSize = null + //textView.context.updateState() // font size not changed + showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) + return@setOnLongClickListener true + } + + //Fetch current value from preference + context?.let { ctx -> + subtitlesFilterSubLang.isChecked = + PreferenceManager.getDefaultSharedPreferences(ctx) + .getBoolean(getString(R.string.filter_sub_lang_key), false) + } + + subtitlesFilterSubLang.setOnCheckedChangeListener { _, b -> + context?.let { ctx -> + PreferenceManager.getDefaultSharedPreferences(ctx) + .edit() + .putBoolean(getString(R.string.filter_sub_lang_key), b) + .apply() + } + } + + subsFont.setFocusableInTv() + subsFont.setOnClickListener { textView -> + val fontTypes = listOf( + Pair(null, textView.context.getString(R.string.normal)), + Pair(R.font.trebuchet_ms, "Trebuchet MS"), + Pair(R.font.netflix_sans, "Netflix Sans"), + Pair(R.font.google_sans, "Google Sans"), + Pair(R.font.open_sans, "Open Sans"), + Pair(R.font.futura, "Futura"), + Pair(R.font.consola, "Consola"), + Pair(R.font.gotham, "Gotham"), + Pair(R.font.lucida_grande, "Lucida Grande"), + Pair(R.font.stix_general, "STIX General"), + Pair(R.font.times_new_roman, "Times New Roman"), + Pair(R.font.verdana, "Verdana"), + Pair(R.font.ubuntu_regular, "Ubuntu"), + Pair(R.font.comic_sans, "Comic Sans"), + Pair(R.font.poppins_regular, "Poppins"), + ) + val savedFontTypes = textView.context.getSavedFonts() + + val currentIndex = + savedFontTypes.indexOfFirst { it.absolutePath == state.typefaceFilePath } + .let { index -> + if (index == -1) + fontTypes.indexOfFirst { it.first == state.typeface } + else index + fontTypes.size + } + + //showBottomDialog + activity?.showDialog( + fontTypes.map { it.second } + savedFontTypes.map { it.name }, + currentIndex, + (textView as TextView).text.toString(), + false, + dismissCallback + ) { index -> + if (index < fontTypes.size) { + state.typeface = fontTypes[index].first + state.typefaceFilePath = null + } else { + state.typefaceFilePath = savedFontTypes[index - fontTypes.size].absolutePath + state.typeface = null + } + textView.context.updateState() + } + } + + subsFont.setOnLongClickListener { textView -> + state.typeface = null + state.typefaceFilePath = null textView.context.updateState() + showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) + return@setOnLongClickListener true } - } - subs_font.setOnLongClickListener { textView -> - state.typeface = null - state.typefaceFilePath = null - textView.context.updateState() - showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) - return@setOnLongClickListener true - } + subsAutoSelectLanguage.setFocusableInTv() + subsAutoSelectLanguage.setOnClickListener { textView -> + val langMap = arrayListOf( + SubtitleHelper.Language639( + textView.context.getString(R.string.none), + textView.context.getString(R.string.none), + "", + "", + "", + "", + "" + ), + ) + langMap.addAll(SubtitleHelper.languages) - subs_auto_select_language.setFocusableInTv() - subs_auto_select_language.setOnClickListener { textView -> - val langMap = arrayListOf( - SubtitleHelper.Language639( - textView.context.getString(R.string.none), - textView.context.getString(R.string.none), - "", - "", - "", - "", - "" - ), - ) - langMap.addAll(SubtitleHelper.languages) - - val lang639_1 = langMap.map { it.ISO_639_1 } - activity?.showDialog( - langMap.map { it.languageName }, - lang639_1.indexOf(getAutoSelectLanguageISO639_1()), - (textView as TextView).text.toString(), - true, - dismissCallback - ) { index -> - setKey(SUBTITLE_AUTO_SELECT_KEY, lang639_1[index]) + val lang639_1 = langMap.map { it.ISO_639_1 } + activity?.showDialog( + langMap.map { it.languageName }, + lang639_1.indexOf(getAutoSelectLanguageISO639_1()), + (textView as TextView).text.toString(), + true, + dismissCallback + ) { index -> + setKey(SUBTITLE_AUTO_SELECT_KEY, lang639_1[index]) + } } - } - subs_auto_select_language.setOnLongClickListener { - setKey(SUBTITLE_AUTO_SELECT_KEY, "en") - showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) - return@setOnLongClickListener true - } - - subs_download_languages.setFocusableInTv() - subs_download_languages.setOnClickListener { textView -> - val langMap = SubtitleHelper.languages - val lang639_1 = langMap.map { it.ISO_639_1 } - val keys = getDownloadSubsLanguageISO639_1() - val keyMap = keys.map { lang639_1.indexOf(it) }.filter { it >= 0 } - - activity?.showMultiDialog( - langMap.map { it.languageName }, - keyMap, - (textView as TextView).text.toString(), - dismissCallback - ) { indexList -> - setKey(SUBTITLE_DOWNLOAD_KEY, indexList.map { lang639_1[it] }.toList()) + subsAutoSelectLanguage.setOnLongClickListener { + setKey(SUBTITLE_AUTO_SELECT_KEY, "en") + showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) + return@setOnLongClickListener true } - } - subs_download_languages.setOnLongClickListener { - setKey(SUBTITLE_DOWNLOAD_KEY, listOf("en")) + subsDownloadLanguages.setFocusableInTv() + subsDownloadLanguages.setOnClickListener { textView -> + val langMap = SubtitleHelper.languages + val lang639_1 = langMap.map { it.ISO_639_1 } + val keys = getDownloadSubsLanguageISO639_1() + val keyMap = keys.map { lang639_1.indexOf(it) }.filter { it >= 0 } - showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) - return@setOnLongClickListener true - } + activity?.showMultiDialog( + langMap.map { it.languageName }, + keyMap, + (textView as TextView).text.toString(), + dismissCallback + ) { indexList -> + setKey(SUBTITLE_DOWNLOAD_KEY, indexList.map { lang639_1[it] }.toList()) + } + } - cancel_btt.setOnClickListener { - activity?.popCurrentPage() - } + subsDownloadLanguages.setOnLongClickListener { + setKey(SUBTITLE_DOWNLOAD_KEY, listOf("en")) - apply_btt.setOnClickListener { - it.context.saveStyle(state) - applyStyleEvent.invoke(state) - it.context.fromSaveToStyle(state) - activity?.popCurrentPage() + showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) + return@setOnLongClickListener true + } + + cancelBtt.setOnClickListener { + activity?.popCurrentPage() + } + + applyBtt.setOnClickListener { + it.context.saveStyle(state) + applyStyleEvent.invoke(state) + it.context.fromSaveToStyle(state) + activity?.popCurrentPage() + } } } } From 04f52f4a6d3eab60b714479e3bdc647d54439209 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 15 Jul 2023 03:25:32 +0200 Subject: [PATCH 013/156] added tests for layout --- app/build.gradle.kts | 1 + .../cloudstream3/ExampleInstrumentedTest.kt | 63 +++++++++++++++++++ .../lagradost/cloudstream3/MainActivity.kt | 17 ++++- .../ui/home/HomeChildItemAdapter.kt | 2 +- .../ui/home/HomeParentItemAdapterPreview.kt | 19 +++++- .../res/layout/home_result_grid_expanded.xml | 13 +++- 6 files changed, 107 insertions(+), 8 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d5364045..288add26 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -142,6 +142,7 @@ dependencies { testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.3") androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") + androidTestImplementation("androidx.test:core") //implementation("io.karn:khttp-android:0.1.2") //okhttp instead // implementation("org.jsoup:jsoup:1.13.1") diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index 92042d60..f28018d1 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -1,6 +1,19 @@ package com.lagradost.cloudstream3 +import android.app.Activity +import android.os.Bundle +import android.os.PersistableBundle +import android.view.LayoutInflater +import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.viewbinding.ViewBinding +import com.lagradost.cloudstream3.databinding.FragmentHomeBinding +import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding +import com.lagradost.cloudstream3.databinding.FragmentSearchBinding +import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding +import com.lagradost.cloudstream3.databinding.HomeResultGridBinding +import com.lagradost.cloudstream3.databinding.RepositoryItemBinding +import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.TestingUtils import kotlinx.coroutines.runBlocking @@ -8,11 +21,18 @@ import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith + /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ +class TestApplication : Activity() { + override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { + super.onCreate(savedInstanceState, persistentState) + } +} + @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { private fun getAllProviders(): List { @@ -26,6 +46,49 @@ class ExampleInstrumentedTest { println("Done providersExist") } + @Throws + private inline fun testAllLayouts( + activity: Activity, + vararg layouts: Int + ) { + + val bind = T::class.java.methods.first { it.name == "bind" } + val inflater = LayoutInflater.from(activity) + for (layout in layouts) { + val root = inflater.inflate(layout, null, false) + bind.invoke(null, root) + } + } + + @Test + @Throws + fun layoutTest() { + ActivityScenario.launch(MainActivity::class.java).use { scenario -> + scenario.onActivity { activity: MainActivity -> + // FragmentHomeHeadBinding and FragmentHomeHeadTvBinding CANT be the same + //testAllLayouts(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv) + //testAllLayouts(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv) + + // main cant be tested + // testAllLayouts(activity,R.layout.activity_main, R.layout.activity_main_tv) + // testAllLayouts(activity,R.layout.activity_main, R.layout.activity_main_tv) + //testAllLayouts(activity, R.layout.activity_main_tv) + + testAllLayouts(activity, R.layout.repository_item_tv, R.layout.repository_item) + testAllLayouts(activity, R.layout.repository_item_tv, R.layout.repository_item) + + testAllLayouts(activity, R.layout.fragment_home_tv, R.layout.fragment_home) + testAllLayouts(activity, R.layout.fragment_home_tv, R.layout.fragment_home) + + testAllLayouts(activity, R.layout.fragment_search_tv, R.layout.fragment_search) + testAllLayouts(activity, R.layout.fragment_search_tv, R.layout.fragment_search) + + testAllLayouts(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid) + //testAllLayouts(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid) ??? fails ??? + } + } + } + @Test @Throws(AssertionError::class) fun providerCorrectData() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 4a7a28ad..c1223415 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -97,6 +97,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadRepository import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -753,13 +754,25 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (isCastApiAvailable()) { mSessionManager = CastContext.getSharedInstance(this).sessionManager } - } catch (e: Exception) { - logError(e) + } catch (t: Throwable) { + logError(t) } window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) updateTv() + // backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting? + try { + val appVer = BuildConfig.VERSION_NAME + val lastAppAutoBackup = getKey("VERSION_NAME") ?: 0 + if (appVer != lastAppAutoBackup) { + setKey("VERSION_NAME", BuildConfig.VERSION_NAME) + backup() + } + } catch (t : Throwable) { + logError(t) + } + // just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH binding = try { if (isTvSettings()) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt index b90a4e43..1e04acf0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt @@ -87,7 +87,7 @@ class HomeChildItemAdapter( else -> null } - (itemView.image_holder ?: itemView.background_card)?.apply { + (itemView.background_card)?.apply { val min = 114.toPx val max = 180.toPx diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 715f1867..9bbfbb37 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -32,15 +32,28 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectSt import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.activity_main.view.* -import kotlinx.android.synthetic.main.fragment_home_head.view.* +import kotlinx.android.synthetic.main.activity_main.view.nav_rail_view +import kotlinx.android.synthetic.main.fragment_home_head.view.home_bookmark_parent_item_title import kotlinx.android.synthetic.main.fragment_home_head.view.home_bookmarked_child_recyclerview +import kotlinx.android.synthetic.main.fragment_home_head.view.home_preview_bookmark +import kotlinx.android.synthetic.main.fragment_home_head.view.home_preview_image +import kotlinx.android.synthetic.main.fragment_home_head.view.home_preview_info +import kotlinx.android.synthetic.main.fragment_home_head.view.home_preview_play +import kotlinx.android.synthetic.main.fragment_home_head.view.home_search import kotlinx.android.synthetic.main.fragment_home_head.view.home_watch_parent_item_title -import kotlinx.android.synthetic.main.fragment_home_head_tv.view.* import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_bookmarked_holder import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_none_padding import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_plan_to_watch_btt import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_change_api +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_change_api2 +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_description +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_hidden_next_focus +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_hidden_prev_focus +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_info_btt +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_play_btt +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_tags +import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_text import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_viewpager import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_completed_btt import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_dropped_btt diff --git a/app/src/main/res/layout/home_result_grid_expanded.xml b/app/src/main/res/layout/home_result_grid_expanded.xml index b697c1de..3c3804a5 100644 --- a/app/src/main/res/layout/home_result_grid_expanded.xml +++ b/app/src/main/res/layout/home_result_grid_expanded.xml @@ -7,7 +7,7 @@ + Date: Sat, 15 Jul 2023 03:27:25 +0200 Subject: [PATCH 014/156] mini fix --- app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index c1223415..a2a24243 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -764,7 +764,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting? try { val appVer = BuildConfig.VERSION_NAME - val lastAppAutoBackup = getKey("VERSION_NAME") ?: 0 + val lastAppAutoBackup : String = getKey("VERSION_NAME") ?: "" if (appVer != lastAppAutoBackup) { setKey("VERSION_NAME", BuildConfig.VERSION_NAME) backup() From f209c7286e34e2640238c5d240170c8569b506da Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 15 Jul 2023 20:00:09 +0200 Subject: [PATCH 015/156] more viewbindings + result fix + more tests --- .../cloudstream3/ExampleInstrumentedTest.kt | 17 ++++ .../ui/home/HomeChildItemAdapter.kt | 22 ++--- .../ui/home/HomeParentItemAdapter.kt | 31 +++--- .../ui/home/HomeParentItemAdapterPreview.kt | 5 +- .../cloudstream3/ui/home/HomeScrollAdapter.kt | 59 +++++++----- .../cloudstream3/ui/library/PageAdapter.kt | 12 +-- .../ui/library/ViewpagerAdapter.kt | 2 +- .../ui/search/SearchResultBuilder.kt | 46 +++++---- .../utils/SingleSelectionHelper.kt | 95 +++++++++++++------ .../main/res/layout/search_result_grid.xml | 22 ++++- .../layout/search_result_grid_expanded.xml | 36 +++---- app/src/main/res/values/styles.xml | 7 +- 12 files changed, 214 insertions(+), 140 deletions(-) diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index f28018d1..68418704 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -12,8 +12,14 @@ import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding import com.lagradost.cloudstream3.databinding.FragmentSearchBinding import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding import com.lagradost.cloudstream3.databinding.HomeResultGridBinding +import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding +import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding +import com.lagradost.cloudstream3.databinding.HomepageParentBinding +import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding import com.lagradost.cloudstream3.databinding.RepositoryItemBinding import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding +import com.lagradost.cloudstream3.databinding.SearchResultGridBinding +import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.TestingUtils import kotlinx.coroutines.runBlocking @@ -85,6 +91,17 @@ class ExampleInstrumentedTest { testAllLayouts(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid) //testAllLayouts(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid) ??? fails ??? + + testAllLayouts(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded) + testAllLayouts(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded) + + + // testAllLayouts(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv) + // testAllLayouts(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv) + + testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent) + testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent) + } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt index 1e04acf0..92bc242d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt @@ -1,22 +1,20 @@ package com.lagradost.cloudstream3.ui.home import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.databinding.HomeResultGridBinding import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.toPx -import kotlinx.android.synthetic.main.home_result_grid.view.background_card -import kotlinx.android.synthetic.main.home_result_grid_expanded.view.* class HomeChildItemAdapter( val cardList: MutableList, - private val overrideLayout: Int? = null, + private val nextFocusUp: Int? = null, private val nextFocusDown: Int? = null, private val clickCallback: (SearchClickCallback) -> Unit, @@ -26,11 +24,13 @@ class HomeChildItemAdapter( var hasNext: Boolean = false override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val layout = overrideLayout - ?: if (parent.context.IsBottomLayout()) R.layout.home_result_grid_expanded else R.layout.home_result_grid + val layout = if (parent.context.IsBottomLayout()) R.layout.home_result_grid_expanded else R.layout.home_result_grid + + val root = LayoutInflater.from(parent.context).inflate(layout, parent, false) + val binding = HomeResultGridBinding.bind(root) return CardViewHolder( - LayoutInflater.from(parent.context).inflate(layout, parent, false), + binding, clickCallback, itemCount, nextFocusUp, @@ -69,14 +69,14 @@ class HomeChildItemAdapter( class CardViewHolder constructor( - itemView: View, + val binding: HomeResultGridBinding, private val clickCallback: (SearchClickCallback) -> Unit, var itemCount: Int, private val nextFocusUp: Int? = null, private val nextFocusDown: Int? = null, private val isHorizontal: Boolean = false ) : - RecyclerView.ViewHolder(itemView) { + RecyclerView.ViewHolder(binding.root) { fun bind(card: SearchResponse, position: Int) { @@ -87,7 +87,7 @@ class HomeChildItemAdapter( else -> null } - (itemView.background_card)?.apply { + binding.backgroundCard.apply { val min = 114.toPx val max = 180.toPx @@ -119,7 +119,7 @@ class HomeChildItemAdapter( itemView.tag = position if (position == 0) { // to fix tv - itemView.background_card?.nextFocusLeftId = R.id.nav_rail_view + binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view } //val ani = ScaleAnimation(0.9f, 1.0f, 0.9f, 1f) //ani.fillAfter = true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt index 7ce9e67d..d05b4cab 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt @@ -10,18 +10,12 @@ import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.HomepageParentBinding import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable -import kotlinx.android.synthetic.main.activity_main_tv.* -import kotlinx.android.synthetic.main.activity_main_tv.view.* -import kotlinx.android.synthetic.main.fragment_home.* -import kotlinx.android.synthetic.main.fragment_home.view.* -import kotlinx.android.synthetic.main.fragment_home_head_tv.* -import kotlinx.android.synthetic.main.fragment_home_head_tv.view.* -import kotlinx.android.synthetic.main.homepage_parent.view.* class LoadClickCallback( val action: Int = 0, @@ -37,12 +31,17 @@ open class ParentItemAdapter( private val expandCallback: ((String) -> Unit)? = null, ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + + val root = LayoutInflater.from(parent.context).inflate( + if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent, + parent, + false + ) + + val binding = HomepageParentBinding.bind(root) + return ParentViewHolder( - LayoutInflater.from(parent.context).inflate( - if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent, - parent, - false - ), + binding, clickCallback, moreInfoClickCallback, expandCallback @@ -153,14 +152,14 @@ open class ParentItemAdapter( class ParentViewHolder constructor( - itemView: View, + val binding: HomepageParentBinding, private val clickCallback: (SearchClickCallback) -> Unit, private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit, private val expandCallback: ((String) -> Unit)? = null, ) : - RecyclerView.ViewHolder(itemView) { - val title: TextView = itemView.home_child_more_info - private val recyclerView: RecyclerView = itemView.home_child_recyclerview + RecyclerView.ViewHolder(binding.root) { + val title: TextView = binding.homeChildMoreInfo + private val recyclerView: RecyclerView = binding.homeChildRecyclerview fun update(expand: HomeViewModel.ExpandableHomepageList) { val info = expand.list diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 9bbfbb37..fffe590e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -409,10 +409,7 @@ class HomeParentItemAdapterPreview( // setPageTransformer(null) if (adapter == null) - adapter = HomeScrollAdapter( - if (isTvSettings()) R.layout.home_scroll_view_tv else R.layout.home_scroll_view, - if (isTvSettings()) true else null - ) + adapter = HomeScrollAdapter() } previewAdapter = previewViewpager?.adapter as? HomeScrollAdapter? // previewViewpager?.registerOnPageChangeCallback(previewCallback) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt index 5902132e..c54996c2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt @@ -2,24 +2,18 @@ package com.lagradost.cloudstream3.ui.home import android.content.res.Configuration import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import androidx.annotation.LayoutRes import androidx.core.view.isGone import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.LoadResponse -import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding +import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.home_scroll_view.view.home_scroll_preview -import kotlinx.android.synthetic.main.home_scroll_view.view.home_scroll_preview_tags -import kotlinx.android.synthetic.main.home_scroll_view.view.home_scroll_preview_title - -class HomeScrollAdapter( - @LayoutRes val layout: Int = R.layout.home_scroll_view, - private val forceHorizontalPosters: Boolean? = null -) : RecyclerView.Adapter() { +class HomeScrollAdapter : RecyclerView.Adapter() { private var items: MutableList = mutableListOf() var hasMoreItems: Boolean = false @@ -45,9 +39,16 @@ class HomeScrollAdapter( } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = if(isTvSettings()) { + HomeScrollViewBinding.inflate(inflater,parent,false) + } else { + HomeScrollViewTvBinding.inflate(inflater,parent,false) + } + return CardViewHolder( - LayoutInflater.from(parent.context).inflate(layout, parent, false), - forceHorizontalPosters + binding, + //forceHorizontalPosters ) } @@ -61,22 +62,30 @@ class HomeScrollAdapter( class CardViewHolder constructor( - itemView: View, - private val forceHorizontalPosters: Boolean? = null + val binding: ViewBinding, + //private val forceHorizontalPosters: Boolean? = null ) : - RecyclerView.ViewHolder(itemView) { + RecyclerView.ViewHolder(binding.root) { fun bind(card: LoadResponse) { - card.apply { - val isHorizontal = - (forceHorizontalPosters == true) || ((forceHorizontalPosters != false) && itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) + val isHorizontal = + binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - val posterUrl = if (isHorizontal) backgroundPosterUrl ?: posterUrl else posterUrl - ?: backgroundPosterUrl - itemView.home_scroll_preview_tags?.text = tags?.joinToString(" • ") ?: "" - itemView.home_scroll_preview_tags?.isGone = tags.isNullOrEmpty() - itemView.home_scroll_preview?.setImage(posterUrl, posterHeaders) - itemView.home_scroll_preview_title?.text = name + 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() + } + binding.homeScrollPreviewTitle.text = card.name + } + is HomeScrollViewTvBinding -> { + binding.homeScrollPreview.setImage(posterUrl) + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt index 05b05f44..d558d6a5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt @@ -84,7 +84,7 @@ class PageAdapter( binding.textRating.apply { setTextColor(ColorStateList.valueOf(fg)) } - binding.textRatingHolder.backgroundTintList = ColorStateList.valueOf(bg) + binding.textRating.backgroundTintList = ColorStateList.valueOf(bg) binding.watchProgress.apply { progressTintList = ColorStateList.valueOf(fg) progressBackgroundTintList = ColorStateList.valueOf(bg) @@ -111,16 +111,6 @@ class PageAdapter( } binding.imageText.text = item.name - - val showRating = (item.personalRating ?: 0) != 0 - binding.textRatingHolder.isVisible = showRating - if (showRating) { - // We want to show 8.5 but not 8.0 hence the replace - val rating = ((item.personalRating ?: 0).toDouble() / 10).toString() - .replace(".0", "") - - binding.textRating.text = "★ $rating" - } } } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt index 441d6adc..95fefcbe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt @@ -64,7 +64,7 @@ class ViewpagerAdapter( } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY -> + setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> val diff = scrollY - oldScrollY if (diff == 0) return@setOnScrollChangeListener diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt index 69812f22..2b2269ff 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt @@ -16,22 +16,13 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchQuality import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.isMovieType +import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.home_result_grid.view.background_card -import kotlinx.android.synthetic.main.home_result_grid.view.imageText -import kotlinx.android.synthetic.main.home_result_grid.view.imageView -import kotlinx.android.synthetic.main.home_result_grid.view.search_item_download_play -import kotlinx.android.synthetic.main.home_result_grid.view.text_flag -import kotlinx.android.synthetic.main.home_result_grid.view.text_is_dub -import kotlinx.android.synthetic.main.home_result_grid.view.text_is_sub -import kotlinx.android.synthetic.main.home_result_grid.view.text_quality -import kotlinx.android.synthetic.main.home_result_grid.view.title_shadow -import kotlinx.android.synthetic.main.home_result_grid.view.watchProgress object SearchResultBuilder { private val showCache: MutableMap = mutableMapOf() @@ -59,19 +50,21 @@ object SearchResultBuilder { nextFocusDown: Int? = null, colorCallback : ((Palette) -> Unit)? = null ) { - val cardView: ImageView = itemView.imageView - val cardText: TextView? = itemView.imageText + val cardView: ImageView = itemView.findViewById(R.id.imageView) + val cardText: TextView? = itemView.findViewById(R.id.imageText) - val textIsDub: TextView? = itemView.text_is_dub - val textIsSub: TextView? = itemView.text_is_sub - val textFlag: TextView? = itemView.text_flag - val textQuality: TextView? = itemView.text_quality - val shadow: View? = itemView.title_shadow + val textIsDub: TextView? = itemView.findViewById(R.id.text_is_dub) + val textIsSub: TextView? = itemView.findViewById(R.id.text_is_sub) + val textFlag: TextView? = itemView.findViewById(R.id.text_flag) + val rating: TextView? = itemView.findViewById(R.id.text_rating) - val bg: CardView = itemView.background_card + val textQuality: TextView? = itemView.findViewById(R.id.text_quality) + val shadow: View? = itemView.findViewById(R.id.title_shadow) - val bar: ProgressBar? = itemView.watchProgress - val playImg: ImageView? = itemView.search_item_download_play + val bg: CardView = itemView.findViewById(R.id.background_card) + + val bar: ProgressBar? = itemView.findViewById(R.id.watchProgress) + val playImg: ImageView? = itemView.findViewById(R.id.search_item_download_play) // Do logic @@ -80,12 +73,25 @@ object SearchResultBuilder { textIsDub?.isVisible = false textIsSub?.isVisible = false textFlag?.isVisible = false + rating?.isVisible = false val showSub = showCache[textIsDub?.context?.getString(R.string.show_sub_key)] ?: false val showDub = showCache[textIsDub?.context?.getString(R.string.show_dub_key)] ?: false val showTitle = showCache[cardText?.context?.getString(R.string.show_title_key)] ?: false val showHd = showCache[textQuality?.context?.getString(R.string.show_hd_key)] ?: false + if(card is SyncAPI.LibraryItem) { + val showRating = (card.personalRating ?: 0) != 0 + rating?.isVisible = showRating + if (showRating) { + // We want to show 8.5 but not 8.0 hence the replace + val ratingText = ((card.personalRating ?: 0).toDouble() / 10).toString() + .replace(".0", "") + + rating?.text = ratingText + } + } + shadow?.isVisible = showTitle when (card.quality) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt index 1f6d726d..8285b8ab 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt @@ -2,19 +2,28 @@ package com.lagradost.cloudstream3.utils import android.app.Activity import android.app.Dialog +import android.view.LayoutInflater import android.view.View -import android.widget.* +import android.widget.AbsListView +import android.widget.ArrayAdapter +import android.widget.EditText +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ListView +import android.widget.TextView import androidx.appcompat.app.AlertDialog -import androidx.core.view.* +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.core.view.marginLeft +import androidx.core.view.marginRight +import androidx.core.view.marginTop import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.setImage -import kotlinx.android.synthetic.main.add_account_input.* -import kotlinx.android.synthetic.main.add_account_input.text1 -import kotlinx.android.synthetic.main.bottom_selection_dialog_direct.* object SingleSelectionHelper { fun Activity?.showOptionSelectStringRes( @@ -82,6 +91,7 @@ object SingleSelectionHelper { } fun Activity?.showDialog( + binding: BottomSelectionDialogBinding, dialog: Dialog, items: List, selectedIndex: List, @@ -95,39 +105,39 @@ object SingleSelectionHelper { if (this == null) return val realShowApply = showApply || isMultiSelect - val listView = dialog.listview1//.findViewById(R.id.listview1)!! - val textView = dialog.text1//.findViewById(R.id.text1)!! - val applyButton = dialog.apply_btt//.findViewById(R.id.apply_btt) - val cancelButton = dialog.cancel_btt//findViewById(R.id.cancel_btt) + val listView = binding.listview1//.findViewById(R.id.listview1)!! + val textView = binding.text1//.findViewById(R.id.text1)!! + val applyButton = binding.applyBtt//.findViewById(R.id.apply_btt) + val cancelButton = binding.cancelBtt//findViewById(R.id.cancel_btt) val applyHolder = - dialog.apply_btt_holder//.findViewById(R.id.apply_btt_holder) + binding.applyBttHolder//.findViewById(R.id.apply_btt_holder) - applyHolder?.isVisible = realShowApply + applyHolder.isVisible = realShowApply if (!realShowApply) { val params = listView.layoutParams as LinearLayout.LayoutParams params.setMargins(listView.marginLeft, listView.marginTop, listView.marginRight, 0) listView.layoutParams = params } - textView?.text = name - textView?.isGone = name.isBlank() + textView.text = name + textView.isGone = name.isBlank() val arrayAdapter = ArrayAdapter(this, itemLayout) arrayAdapter.addAll(items) - listView?.adapter = arrayAdapter + listView.adapter = arrayAdapter if (isMultiSelect) { - listView?.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE + listView.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE } else { - listView?.choiceMode = AbsListView.CHOICE_MODE_SINGLE + listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE } for (select in selectedIndex) { - listView?.setItemChecked(select, true) + listView.setItemChecked(select, true) } selectedIndex.minOrNull()?.let { - listView?.setSelection(it) + listView.setSelection(it) } // var lastSelectedIndex = if(selectedIndex.isNotEmpty()) selectedIndex.first() else -1 @@ -136,7 +146,7 @@ object SingleSelectionHelper { dismissCallback.invoke() } - listView?.setOnItemClickListener { _, _, which, _ -> + listView.setOnItemClickListener { _, _, which, _ -> // lastSelectedIndex = which if (realShowApply) { if (!isMultiSelect) { @@ -148,7 +158,7 @@ object SingleSelectionHelper { } } if (realShowApply) { - applyButton?.setOnClickListener { + applyButton.setOnClickListener { val list = ArrayList() for (index in 0 until listView.count) { if (listView.checkedItemPositions[index]) @@ -157,7 +167,7 @@ object SingleSelectionHelper { callback.invoke(list) dialog.dismissSafe(this) } - cancelButton?.setOnClickListener { + cancelButton.setOnClickListener { dialog.dismissSafe(this) } } @@ -213,13 +223,26 @@ object SingleSelectionHelper { ) { if (this == null) return + val binding: BottomSelectionDialogBinding = BottomSelectionDialogBinding.inflate( + LayoutInflater.from(this) + ) val builder = AlertDialog.Builder(this, R.style.AlertDialogCustom) - .setView(R.layout.bottom_selection_dialog) + .setView(binding.root) val dialog = builder.create() dialog.show() - showDialog(dialog, items, selectedIndex, name, true, true, callback, dismissCallback) + showDialog( + binding, + dialog, + items, + selectedIndex, + name, + showApply = true, + isMultiSelect = true, + callback, + dismissCallback + ) } fun Activity?.showDialog( @@ -232,13 +255,19 @@ object SingleSelectionHelper { ) { if (this == null) return + val binding: BottomSelectionDialogBinding = BottomSelectionDialogBinding.inflate( + LayoutInflater.from(this) + ) val builder = AlertDialog.Builder(this, R.style.AlertDialogCustom) - .setView(R.layout.bottom_selection_dialog) + .setView(binding.root) val dialog = builder.create() dialog.show() + + showDialog( + binding, dialog, items, listOf(selectedIndex), @@ -260,12 +289,18 @@ object SingleSelectionHelper { callback: (Int) -> Unit, ) { if (this == null) return + + val binding: BottomSelectionDialogBinding = BottomSelectionDialogBinding.inflate( + LayoutInflater.from(this) + ) + val builder = BottomSheetDialog(this) - builder.setContentView(R.layout.bottom_selection_dialog) + builder.setContentView(binding.root) builder.show() showDialog( + binding, builder, items, listOf(selectedIndex), @@ -285,13 +320,19 @@ object SingleSelectionHelper { ): BottomSheetDialog { val builder = BottomSheetDialog(this) - builder.setContentView(R.layout.bottom_selection_dialog_direct) + val binding: BottomSelectionDialogBinding = BottomSelectionDialogBinding.inflate( + LayoutInflater.from(this) + ) + + //builder.setContentView(R.layout.bottom_selection_dialog_direct) + builder.setContentView(binding.root) builder.show() showDialog( + binding, builder, items, - listOf(), + emptyList(), name, showApply = false, isMultiSelect = false, diff --git a/app/src/main/res/layout/search_result_grid.xml b/app/src/main/res/layout/search_result_grid.xml index f3c35ca4..cec7d4ce 100644 --- a/app/src/main/res/layout/search_result_grid.xml +++ b/app/src/main/res/layout/search_result_grid.xml @@ -1,5 +1,5 @@ - + + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/search_result_grid_expanded.xml b/app/src/main/res/layout/search_result_grid_expanded.xml index cf6ab3b2..c64486b4 100644 --- a/app/src/main/res/layout/search_result_grid_expanded.xml +++ b/app/src/main/res/layout/search_result_grid_expanded.xml @@ -58,29 +58,12 @@ style="@style/SubButton" android:layout_gravity="end" /> - - - - - + tools:text="7.7" /> + @color/subColorText + + + + + + + + @@ -547,6 +548,7 @@ 0dp 0dp @drawable/outline_drawable_less + @string/tv_no_focus_tag From c987f7581e590a8f69f525fd8246f5a15224577a Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 28 Jul 2023 04:18:28 +0200 Subject: [PATCH 040/156] major focus fixes --- .../lagradost/cloudstream3/CommonActivity.kt | 79 ++++++++++++------- .../cloudstream3/ui/AutofitRecyclerView.kt | 25 ++++-- .../ui/home/HomeChildItemAdapter.kt | 42 +++++++--- .../ui/home/HomeParentItemAdapter.kt | 4 +- .../ui/home/HomeParentItemAdapterPreview.kt | 4 + .../cloudstream3/ui/result/ActorAdaptor.kt | 12 ++- .../ui/result/LinearListLayout.kt | 24 +++--- .../ui/result/ResultFragmentTv.kt | 34 ++++---- .../ui/search/SearchResultBuilder.kt | 57 ++++++++----- .../drawable/ic_baseline_arrow_back_24.xml | 1 + .../ic_baseline_arrow_back_ios_24.xml | 14 +++- .../drawable/ic_baseline_arrow_forward_24.xml | 1 + .../ic_baseline_keyboard_arrow_left_24.xml | 14 +++- .../res/drawable/ic_baseline_language_24.xml | 13 ++- .../res/drawable/ic_baseline_more_vert_24.xml | 13 ++- .../ic_baseline_notifications_active_24.xml | 13 ++- .../main/res/drawable/netflix_skip_back.xml | 32 ++++---- .../res/drawable/outline_drawable_forced.xml | 5 ++ app/src/main/res/layout/activity_main_tv.xml | 1 - .../main/res/layout/fragment_home_head_tv.xml | 4 +- .../main/res/layout/fragment_result_tv.xml | 3 +- ...sort_bottom_single_choice_no_checkmark.xml | 12 +-- 22 files changed, 268 insertions(+), 139 deletions(-) create mode 100644 app/src/main/res/drawable/outline_drawable_forced.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 7eb1bf6d..684e2269 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.ui.player.PlayerEventType import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv +import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.UIHelper @@ -299,22 +300,29 @@ object CommonActivity { view: View?, direction: FocusDirection, depth: Int = 0 - ): Int? { + ): View? { + // if input is invalid let android decide + depth test to not crash if loop is found if (view == null || depth >= 10 || act == null) { return null } val nextId = when (direction) { - FocusDirection.Left -> { - view.nextFocusLeftId + FocusDirection.Start -> { + if (view.isRtl()) + view.nextFocusRightId + else + view.nextFocusLeftId } FocusDirection.Up -> { view.nextFocusUpId } - FocusDirection.Right -> { - view.nextFocusRightId + FocusDirection.End -> { + if (view.isRtl()) + view.nextFocusLeftId + else + view.nextFocusRightId } FocusDirection.Down -> { @@ -322,27 +330,41 @@ object CommonActivity { } } - return if (nextId != -1) { - val next = act.findViewById(nextId) - //println("NAME: ${next.accessibilityClassName} | ${next?.isShown}" ) + // if view not found then return + if (nextId == -1) return null + var next = act.findViewById(nextId) ?: return null - if (next?.isShown == false) { - getNextFocus(act, next, direction, depth + 1) - } else { - if (depth == 0) { - null - } else { - nextId - } + // because we want closes find, aka when multiple have the same id, we go to parent + // until the correct one is found + /*var currentLook: View = view + while (true) { + val tmpNext = currentLook.findViewById(nextId) + if (tmpNext != null) { + next = tmpNext + break } - } else { - null + + currentLook = currentLook.parent as? View ?: break + }*/ + + var currentLook: View = view + while (currentLook.findViewById(nextId)?.also { next = it } == null) { + currentLook = (currentLook.parent as? View) ?: break } + + // if cant focus but visible then break and let android decide + if (!next.isFocusable && next.isShown) return null + + // if not shown then continue because we will "skip" over views to get to a replacement + if (!next.isShown) return getNextFocus(act, next, direction, depth + 1) + + // nothing wrong with the view found, return it + return next } enum class FocusDirection { - Left, - Right, + Start, + End, Up, Down, } @@ -447,17 +469,17 @@ object CommonActivity { event?.keyCode?.let { keyCode -> if (currentFocus == null || event.action != KeyEvent.ACTION_DOWN) return@let - val next = when (keyCode) { + val nextView = when (keyCode) { KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus( act, currentFocus, - FocusDirection.Left + FocusDirection.Start ) KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus( act, currentFocus, - FocusDirection.Right + FocusDirection.End ) KeyEvent.KEYCODE_DPAD_UP -> getNextFocus( @@ -475,13 +497,10 @@ object CommonActivity { else -> null } - if (next != null && next != -1) { - val nextView = act.findViewById(next) - if (nextView != null) { - nextView.requestFocus() - keyEventListener?.invoke(Pair(event, true)) - return true - } + if (nextView != null) { + nextView.requestFocus() + keyEventListener?.invoke(Pair(event, true)) + return true } if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt index b4c07792..28ced48c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt @@ -24,7 +24,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : } } - override fun onRequestChildFocus( + /*override fun onRequestChildFocus( parent: RecyclerView, state: RecyclerView.State, child: View, @@ -32,13 +32,17 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : ): Boolean { // android.widget.FrameLayout$LayoutParams cannot be cast to androidx.recyclerview.widget.RecyclerView$LayoutParams return try { - val pos = maxOf(0, getPosition(focused!!) - 2) - parent.scrollToPosition(pos) + if(focused != null) { + // val pos = maxOf(0, getPosition(focused) - 2) // IDK WHY + val pos = getPosition(focused) + if(pos >= 0) parent.scrollToPosition(pos) + } + super.onRequestChildFocus(parent, state, child, focused) } catch (e: Exception) { false } - } + }*/ // Allows moving right and left with focus https://gist.github.com/vganin/8930b41f55820ec49e4d override fun onInterceptFocusSearch(focused: View, direction: Int): View? { @@ -65,8 +69,17 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : val spanCount = this.spanCount val orientation = this.orientation + // fixes arabic by inverting left and right layout focus + val correctDirection = if(this.isLayoutRTL) { + when(direction) { + View.FOCUS_RIGHT -> View.FOCUS_LEFT + View.FOCUS_LEFT -> View.FOCUS_RIGHT + else -> direction + } + } else direction + if (orientation == VERTICAL) { - when (direction) { + when (correctDirection) { View.FOCUS_DOWN -> { return spanCount } @@ -81,7 +94,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : } } } else if (orientation == HORIZONTAL) { - when (direction) { + when (correctDirection) { View.FOCUS_DOWN -> { return 1 } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt index 8b0f9003..607cda01 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt @@ -11,6 +11,7 @@ import com.lagradost.cloudstream3.databinding.HomeResultGridBinding import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder +import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.toPx @@ -27,13 +28,17 @@ class HomeChildItemAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 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 binding = HomeResultGridBinding.bind(root)*/ + val root = LayoutInflater.from(parent.context).inflate(layout, parent, false) + val binding = HomeResultGridBinding.bind(root)*/ val inflater = LayoutInflater.from(parent.context) - val binding = if(expanded) HomeResultGridExpandedBinding.inflate(inflater,parent,false) else HomeResultGridBinding.inflate(inflater,parent,false) + val binding = if (expanded) HomeResultGridExpandedBinding.inflate( + inflater, + parent, + false + ) else HomeResultGridBinding.inflate(inflater, parent, false) return CardViewHolder( @@ -42,7 +47,8 @@ class HomeChildItemAdapter( itemCount, nextFocusUp, nextFocusDown, - isHorizontal + isHorizontal, + parent.isRtl() ) } @@ -81,7 +87,8 @@ class HomeChildItemAdapter( var itemCount: Int, private val nextFocusUp: Int? = null, private val nextFocusDown: Int? = null, - private val isHorizontal: Boolean = false + private val isHorizontal: Boolean = false, + private val isRtl : Boolean ) : RecyclerView.ViewHolder(binding.root) { @@ -93,7 +100,23 @@ class HomeChildItemAdapter( itemCount - 1 -> false else -> null } - when(binding) { + + 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 @@ -114,10 +137,9 @@ class HomeChildItemAdapter( } } - if (position == 0) { // to fix tv - binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view - } + } + is HomeResultGridExpandedBinding -> { binding.backgroundCard.apply { val min = 114.toPx diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt index d4c0bd62..f6c3fead 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt @@ -177,7 +177,7 @@ open class ParentItemAdapter( ).apply { isHorizontal = info.isHorizontalImages } - //recyclerView.setLinearListLayout() + recyclerView.setLinearListLayout() } } @@ -192,7 +192,7 @@ open class ParentItemAdapter( isHorizontal = info.isHorizontalImages hasNext = expand.hasNext } - // recyclerView.setLinearListLayout() + recyclerView.setLinearListLayout() title.text = info.name recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index a304b43f..ce7b8447 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -30,6 +30,7 @@ import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST +import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA import com.lagradost.cloudstream3.ui.search.SearchClickCallback @@ -427,6 +428,9 @@ class HomeParentItemAdapterPreview( resumeRecyclerView.adapter = resumeAdapter bookmarkRecyclerView.adapter = bookmarkAdapter + resumeRecyclerView.setLinearListLayout() + bookmarkRecyclerView.setLinearListLayout() + for ((chip, watch) in toggleList) { chip.isChecked = false chip.setOnCheckedChangeListener { _, isChecked -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index 7b415d78..531cb5d2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil @@ -11,7 +12,7 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.CastItemBinding import com.lagradost.cloudstream3.utils.UIHelper.setImage -class ActorAdaptor : RecyclerView.Adapter() { +class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerView.Adapter() { data class ActorMetaData( var isInverted: Boolean, val actor: ActorData, @@ -21,7 +22,7 @@ class ActorAdaptor : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return CardViewHolder( - CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), focusCallback ) } @@ -66,6 +67,7 @@ class ActorAdaptor : RecyclerView.Adapter() { private class CardViewHolder constructor( val binding: CastItemBinding, + private val focusCallback : (View?) -> Unit = {} ) : RecyclerView.ViewHolder(binding.root) { @@ -76,6 +78,12 @@ class ActorAdaptor : RecyclerView.Adapter() { Pair(actor.voiceActor?.image, actor.actor.image) } + itemView.setOnFocusChangeListener { v, hasFocus -> + if(hasFocus) { + focusCallback(v) + } + } + itemView.setOnClickListener { callback(position) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/LinearListLayout.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/LinearListLayout.kt index 434355a2..26cb7900 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/LinearListLayout.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/LinearListLayout.kt @@ -2,19 +2,16 @@ package com.lagradost.cloudstream3.ui.result import android.content.Context import android.view.View -import android.view.View.LAYOUT_DIRECTION_LTR import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppUtils.isLtr -import com.lagradost.cloudstream3.utils.AppUtils.isRtl fun RecyclerView?.setLinearListLayout(isHorizontal: Boolean = true) { if (this == null) return this.layoutManager = this.context?.let { LinearListLayout(it).apply { if (isHorizontal) setHorizontal() else setVertical() } } - // ?: this.layoutManager + // ?: this.layoutManager } open class LinearListLayout(context: Context?) : @@ -60,7 +57,7 @@ open class LinearListLayout(context: Context?) : startSmoothScroll(linearSmoothScroller) }*/ override fun onInterceptFocusSearch(focused: View, direction: Int): View? { - var dir = if (orientation == HORIZONTAL) { + val dir = if (orientation == HORIZONTAL) { if (direction == View.FOCUS_DOWN || direction == View.FOCUS_UP) { // This scrolls the recyclerview before doing focus search, which // allows the focus search to work better. @@ -70,19 +67,28 @@ open class LinearListLayout(context: Context?) : (focused.parent as? RecyclerView)?.focusSearch(direction) return null } - if (direction == View.FOCUS_RIGHT) 1 else -1 + var ret = if (direction == View.FOCUS_RIGHT) 1 else -1 + // only flip on horizontal layout + if (this.isLayoutRTL) { + ret = -ret + } + ret } else { if (direction == View.FOCUS_RIGHT || direction == View.FOCUS_LEFT) return null if (direction == View.FOCUS_DOWN) 1 else -1 } - if(this.isLayoutRTL) { - dir = -dir - } return try { getPosition(getCorrectParent(focused))?.let { position -> val lookfor = dir + position //clamp(dir + position, 0, recyclerView.adapter?.itemCount ?: return null) + + // refocus on the same view if going out of bounds, note that we only do it + // for out of bounds one way as we may override the start where item == -1 + if (lookfor >= itemCount) { + return getViewFromPos(itemCount - 1) ?: focused + } + getViewFromPos(lookfor) ?: run { scrollToPosition(lookfor) null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index 69127e86..f62d7e73 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -1,15 +1,12 @@ package com.lagradost.cloudstream3.ui.result import android.animation.Animator -import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.app.Dialog import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.animation.AlphaAnimation -import android.view.animation.Animation import android.view.animation.DecelerateInterpolator import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone @@ -31,20 +28,17 @@ import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup -import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent +import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.html -import com.lagradost.cloudstream3.utils.AppUtils.isLtr import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.AppUtils.loadCache -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant import com.lagradost.cloudstream3.utils.UIHelper @@ -52,10 +46,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.setImage -import com.lagradost.cloudstream3.utils.UIHelper.setImageBlur -import kotlinx.coroutines.delay class ResultFragmentTv : Fragment() { protected lateinit var viewModel: ResultViewModel2 @@ -204,7 +195,7 @@ class ResultFragmentTv : Fragment() { } }) } - this.animate().translationX(if (turnVisible) 0f else 100f).apply { + this.animate().translationX(if (turnVisible) 0f else if(isRtl()) -100.0f else 100f).apply { duration = 200 interpolator = DecelerateInterpolator() } @@ -214,6 +205,13 @@ class ResultFragmentTv : Fragment() { binding?.apply { episodesShadow.fade(show) episodeHolderTv.fade(show) + if(episodesShadow.isRtl()) { + episodesShadow.scaleX = -1.0f + episodesShadow.scaleY = -1.0f + } else { + episodesShadow.scaleX = 1.0f + episodesShadow.scaleY = 1.0f + } } } @@ -238,6 +236,7 @@ class ResultFragmentTv : Fragment() { // ===== ===== ===== binding?.apply { + //episodesShadow.rotationX = 180.0f//if(episodesShadow.isRtl()) 180.0f else 0.0f val leftListener: View.OnFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> @@ -264,6 +263,8 @@ class ResultFragmentTv : Fragment() { toggleEpisodes(!episodeHolderTv.isVisible) } + // resultEpisodes.onFocusChangeListener = leftListener + redirectToPlay.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) return@setOnFocusChangeListener toggleEpisodes(false) @@ -306,7 +307,7 @@ class ResultFragmentTv : Fragment() { } } - resultEpisodes.setLinearListLayout(false)/*.layoutManager = + resultEpisodes.setLinearListLayout(isHorizontal = false)/*.layoutManager = LinearListLayout(resultEpisodes.context, resultEpisodes.isRtl()).apply { setVertical() }*/ @@ -348,7 +349,10 @@ class ResultFragmentTv : Fragment() { ArrayList(), resultRecommendationsList, ) { callback -> - SearchHelper.handleSearchClickCallback(callback) + if(callback.action == SEARCH_ACTION_FOCUSED) + toggleEpisodes(false) + else + SearchHelper.handleSearchClickCallback(callback) } resultEpisodes.adapter = @@ -381,7 +385,9 @@ class ResultFragmentTv : Fragment() { }.apply { this.orientation = RecyclerView.HORIZONTAL } - resultCastItems.adapter = ActorAdaptor() + resultCastItems.adapter = ActorAdaptor { + toggleEpisodes(false) + } } observeNullable(viewModel.resumeWatching) { resume -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt index 2b2269ff..e1b72b30 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt @@ -162,15 +162,42 @@ object SearchResultBuilder { } } - bg.setOnClickListener { - click(it) + bg.isFocusable = false + bg.isFocusableInTouchMode = false + if(!isTrueTvSettings()) { + bg.setOnClickListener { + click(it) + } + bg.setOnLongClickListener { + longClick(it) + return@setOnLongClickListener true + } } + // + // + // itemView.setOnClickListener { click(it) } - if (nextFocusUp != null) { + itemView.nextFocusUpId = nextFocusUp + } + + if (nextFocusDown != null) { + itemView.nextFocusDownId = nextFocusDown + } + + /*when (nextFocusBehavior) { + true -> itemView.nextFocusLeftId = bg.id + false -> itemView.nextFocusRightId = bg.id + null -> { + bg.nextFocusRightId = -1 + bg.nextFocusLeftId = -1 + } + }*/ + + /*if (nextFocusUp != null) { bg.nextFocusUpId = nextFocusUp } @@ -178,36 +205,26 @@ object SearchResultBuilder { bg.nextFocusDownId = nextFocusDown } - when (nextFocusBehavior) { - true -> bg.nextFocusLeftId = bg.id - false -> bg.nextFocusRightId = bg.id - null -> { - bg.nextFocusRightId = -1 - bg.nextFocusLeftId = -1 - } - } + */ if (isTrueTvSettings()) { - bg.isFocusable = true - bg.isFocusableInTouchMode = true - bg.touchscreenBlocksFocus = false + // bg.isFocusable = true + // bg.isFocusableInTouchMode = true + // bg.touchscreenBlocksFocus = false itemView.isFocusableInTouchMode = true itemView.isFocusable = true } - bg.setOnLongClickListener { - longClick(it) - return@setOnLongClickListener true - } + /**/ itemView.setOnLongClickListener { longClick(it) return@setOnLongClickListener true } - bg.setOnFocusChangeListener { view, b -> + /*bg.setOnFocusChangeListener { view, b -> focus(view, b) - } + }*/ itemView.setOnFocusChangeListener { view, b -> focus(view, b) diff --git a/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml index ebe459b2..dbda1cc0 100644 --- a/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml +++ b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml @@ -3,6 +3,7 @@ android:viewportWidth="48" android:viewportHeight="48" android:tint="?attr/white" + android:autoMirrored="true" xmlns:android="http://schemas.android.com/apk/res/android"> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml index 6c3197a6..516df382 100644 --- a/app/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml +++ b/app/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml @@ -1,5 +1,11 @@ - - + + diff --git a/app/src/main/res/drawable/ic_baseline_arrow_forward_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_forward_24.xml index 2ec8c110..48ac45e7 100644 --- a/app/src/main/res/drawable/ic_baseline_arrow_forward_24.xml +++ b/app/src/main/res/drawable/ic_baseline_arrow_forward_24.xml @@ -3,6 +3,7 @@ android:viewportWidth="48" android:viewportHeight="48" android:tint="?attr/white" + android:autoMirrored="true" xmlns:android="http://schemas.android.com/apk/res/android"> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_keyboard_arrow_left_24.xml b/app/src/main/res/drawable/ic_baseline_keyboard_arrow_left_24.xml index 916c761c..b67188db 100644 --- a/app/src/main/res/drawable/ic_baseline_keyboard_arrow_left_24.xml +++ b/app/src/main/res/drawable/ic_baseline_keyboard_arrow_left_24.xml @@ -1,5 +1,11 @@ - - + + diff --git a/app/src/main/res/drawable/ic_baseline_language_24.xml b/app/src/main/res/drawable/ic_baseline_language_24.xml index 1749952e..89b47937 100644 --- a/app/src/main/res/drawable/ic_baseline_language_24.xml +++ b/app/src/main/res/drawable/ic_baseline_language_24.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_baseline_more_vert_24.xml b/app/src/main/res/drawable/ic_baseline_more_vert_24.xml index 249fe2a2..b6908e96 100644 --- a/app/src/main/res/drawable/ic_baseline_more_vert_24.xml +++ b/app/src/main/res/drawable/ic_baseline_more_vert_24.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml b/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml index 2003bfe7..5d6045e7 100644 --- a/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml +++ b/app/src/main/res/drawable/ic_baseline_notifications_active_24.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/netflix_skip_back.xml b/app/src/main/res/drawable/netflix_skip_back.xml index bb63e948..5ad9c1a1 100644 --- a/app/src/main/res/drawable/netflix_skip_back.xml +++ b/app/src/main/res/drawable/netflix_skip_back.xml @@ -1,23 +1,23 @@ + android:width="850.39dp" + android:height="850.39dp" + android:viewportWidth="850.39" + android:viewportHeight="850.39"> + android:fillColor="#00000000" + android:pathData="M143.05,279.28A317.41,317.41 0,0 0,106.3 428c0,176.13 142.77,318.9 318.9,318.9S744.09,604.16 744.09,428 601.32,109.14 425.2,109.14q-14.15,0 -28,1.2" + android:strokeWidth="45" + android:strokeColor="#fff" /> + android:fillColor="#fff" + android:pathData="M483.083,223.108l-111.666,-111.666l25.442,-25.442l111.666,111.666z" /> + android:fillColor="#fff" + android:pathData="M371.421,111.662l111.666,-111.666l25.442,25.442l-111.666,111.666z" /> + android:fillColor="#fff" + android:pathData="M398.087,223.272l-111.666,-111.666l25.442,-25.442l111.666,111.666z" /> + android:fillColor="#fff" + android:pathData="M286.427,111.826l111.666,-111.666l25.442,25.442l-111.666,111.666z" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_drawable_forced.xml b/app/src/main/res/drawable/outline_drawable_forced.xml new file mode 100644 index 00000000..16eba83c --- /dev/null +++ b/app/src/main/res/drawable/outline_drawable_forced.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main_tv.xml b/app/src/main/res/layout/activity_main_tv.xml index 77baf1d3..a70a40cd 100644 --- a/app/src/main/res/layout/activity_main_tv.xml +++ b/app/src/main/res/layout/activity_main_tv.xml @@ -83,7 +83,6 @@ android:layout_height="match_parent"> @@ -111,7 +111,7 @@ android:nextFocusLeft="@id/home_preview_play_btt" android:nextFocusRight="@id/home_preview_hidden_next_focus" android:nextFocusUp="@id/home_preview_change_api" - android:nextFocusDown="@id/home_watch_parent_item_title" + android:nextFocusDown="@id/home_watch_child_recyclerview" android:text="@string/home_info" app:icon="@drawable/ic_outline_info_24" /> diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 949ef2ef..324935e5 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -537,6 +537,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit + xmlns:tools="http://schemas.android.com/tools" + android:id="@android:id/text1" + style="@style/NoCheckLabel" + android:textColor="?attr/textColor" + android:textStyle="normal" + tools:text="hello" /> From 3bdbb35754a45472aad6383df51511f45c4c0dd5 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 30 Jul 2023 05:05:13 +0200 Subject: [PATCH 041/156] alert fix + synchronized + bump + homepage load fix + small focus change --- app/build.gradle.kts | 2 +- .../cloudstream3/ExampleInstrumentedTest.kt | 6 +- .../lagradost/cloudstream3/CommonActivity.kt | 58 +++++++++----- .../com/lagradost/cloudstream3/MainAPI.kt | 36 ++++++--- .../lagradost/cloudstream3/MainActivity.kt | 76 ++++++++++--------- .../metaproviders/CrossTmdbProvider.kt | 13 ++-- .../metaproviders/MultiAnimeProvider.kt | 17 +++-- .../lagradost/cloudstream3/plugins/Plugin.kt | 16 ++-- .../cloudstream3/plugins/PluginManager.kt | 13 +++- .../cloudstream3/ui/home/HomeFragment.kt | 3 +- .../ui/home/HomeParentItemAdapterPreview.kt | 4 +- .../cloudstream3/ui/home/HomeViewModel.kt | 76 +++++++++++++------ .../ui/library/LibraryFragment.kt | 14 ++-- .../ui/result/ResultViewModel2.kt | 22 +++--- .../cloudstream3/ui/search/SearchViewModel.kt | 4 +- .../ui/settings/SettingsFragment.kt | 3 + .../ui/settings/SettingsGeneral.kt | 5 +- .../ui/settings/SettingsProviders.kt | 6 +- .../ui/settings/testing/TestViewModel.kt | 6 +- .../ui/setup/SetupFragmentExtensions.kt | 2 +- .../ui/setup/SetupFragmentProviderLanguage.kt | 4 +- .../lagradost/cloudstream3/utils/DataStore.kt | 2 + .../lagradost/cloudstream3/utils/SyncUtil.kt | 6 +- .../cloudstream3/utils/TestingUtils.kt | 2 +- .../main/res/layout/fragment_home_head_tv.xml | 22 +++--- app/src/main/res/values/styles.xml | 3 +- 26 files changed, 265 insertions(+), 156 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f4886258..27bd1e48 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -52,7 +52,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.0.1" + versionName = "4.1.1" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index 509ea4b9..df41ef91 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -46,9 +46,9 @@ class TestApplication : Activity() { @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { - private fun getAllProviders(): List { + private fun getAllProviders(): Array { println("Providers: ${APIHolder.allProviders.size}") - return APIHolder.allProviders //.filter { !it.usesWebView } + return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView } } @Test @@ -147,7 +147,7 @@ class ExampleInstrumentedTest { @Test fun providerCorrectHomepage() { runBlocking { - getAllProviders().amap { api -> + getAllProviders().toList().amap { api -> TestingUtils.testHomepage(api, ::println) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 684e2269..9c7c319e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -9,6 +9,7 @@ import android.content.res.Resources import android.os.Build import android.util.Log import android.view.* +import android.view.View.NO_ID import android.widget.TextView import android.widget.Toast import androidx.activity.ComponentActivity @@ -295,6 +296,30 @@ object CommonActivity { ) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW } + /** because we want closes find, aka when multiple have the same id, we go to parent + until the correct one is found */ + private fun localLook(from: View, id: Int): View? { + if (id == NO_ID) return null + var currentLook: View = from + while (true) { + currentLook.findViewById(id)?.let { return it } + currentLook = (currentLook.parent as? View) ?: break + } + return null + } + /*var currentLook: View = view + while (true) { + val tmpNext = currentLook.findViewById(nextId) + if (tmpNext != null) { + next = tmpNext + break + } + currentLook = currentLook.parent as? View ?: break + }*/ + + /** recursively looks for a next focus up to a depth of 10, + * this is used to override the normal shit focus system + * because this application has a lot of invisible views that messes with some tv devices*/ private fun getNextFocus( act: Activity?, view: View?, @@ -306,7 +331,7 @@ object CommonActivity { return null } - val nextId = when (direction) { + var nextId = when (direction) { FocusDirection.Start -> { if (view.isRtl()) view.nextFocusRightId @@ -330,22 +355,16 @@ object CommonActivity { } } - // if view not found then return - if (nextId == -1) return null + if (nextId == NO_ID) { + // if not specified then use forward id + nextId = view.nextFocusForwardId + // if view is still not found to next focus then return and let android decide + if (nextId == NO_ID) return null + } + var next = act.findViewById(nextId) ?: return null - // because we want closes find, aka when multiple have the same id, we go to parent - // until the correct one is found - /*var currentLook: View = view - while (true) { - val tmpNext = currentLook.findViewById(nextId) - if (tmpNext != null) { - next = tmpNext - break - } - - currentLook = currentLook.parent as? View ?: break - }*/ + next = localLook(view, nextId) ?: next var currentLook: View = view while (currentLook.findViewById(nextId)?.also { next = it } == null) { @@ -362,7 +381,7 @@ object CommonActivity { return next } - enum class FocusDirection { + private enum class FocusDirection { Start, End, Up, @@ -463,6 +482,7 @@ object CommonActivity { //} } + /** overrides focus and custom key events */ fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? { if (act == null) return null val currentFocus = act.currentFocus @@ -503,7 +523,9 @@ object CommonActivity { return true } - if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)) { + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && + (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) + ) { UIHelper.showInputMethod(act.currentFocus?.findFocus()) } @@ -516,6 +538,8 @@ object CommonActivity { } + // if someone else want to override the focus then don't handle the event as it is already + // consumed. used in video player if (keyEventListener?.invoke(Pair(event, false)) == true) { return true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 86252b40..51d218bf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -50,8 +50,10 @@ object APIHolder { val allProviders = threadSafeListOf() fun initAll() { - for (api in allProviders) { - api.init() + synchronized(allProviders) { + for (api in allProviders) { + api.init() + } } apiMap = null } @@ -64,27 +66,35 @@ object APIHolder { var apiMap: Map? = null fun addPluginMapping(plugin: MainAPI) { - apis = apis + plugin + synchronized(apis) { + apis = apis + plugin + } initMap(true) } fun removePluginMapping(plugin: MainAPI) { - apis = apis.filter { it != plugin } + synchronized(apis) { + apis = apis.filter { it != plugin } + } initMap(true) } private fun initMap(forcedUpdate: Boolean = false) { - if (apiMap == null || forcedUpdate) - apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap() + synchronized(apis) { + if (apiMap == null || forcedUpdate) + apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap() + } } fun getApiFromNameNull(apiName: String?): MainAPI? { if (apiName == null) return null synchronized(allProviders) { initMap() - return apiMap?.get(apiName)?.let { apis.getOrNull(it) } - // Leave the ?. null check, it can crash regardless - ?: allProviders.firstOrNull { it.name == apiName } + synchronized(apis) { + return apiMap?.get(apiName)?.let { apis.getOrNull(it) } + // Leave the ?. null check, it can crash regardless + ?: allProviders.firstOrNull { it.name == apiName } + } } } @@ -215,7 +225,7 @@ object APIHolder { val hashSet = HashSet() val activeLangs = getApiProviderLangSettings() val hasUniversal = activeLangs.contains(AllLanguagesName) - hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) } + hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } } .map { it.name }) /*val set = settingsManager.getStringSet( @@ -314,8 +324,9 @@ object APIHolder { } ?: default val langs = this.getApiProviderLangSettings() val hasUniversal = langs.contains(AllLanguagesName) - val allApis = apis.filter { hasUniversal || langs.contains(it.lang) } - .filter { api -> api.hasMainPage || !hasHomePageIsRequired } + val allApis = synchronized(apis) { + apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } + } return if (currentPrefMedia.isEmpty()) { allApis } else { @@ -736,6 +747,7 @@ fun fixTitle(str: String): String { .replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else it } } } + /** * Get rhino context in a safe way as it needs to be initialized on the main thread. * Make sure you get the scope using: val scope: Scriptable = rhino.initSafeStandardObjects() diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 3dd5a495..1083ad49 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -382,10 +382,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { this.navigate(R.id.navigation_downloads) return true } else { - for (api in apis) { - if (str.startsWith(api.mainUrl)) { - loadResult(str, api.name) - return true + synchronized(apis) { + for (api in apis) { + if (str.startsWith(api.mainUrl)) { + loadResult(str, api.name) + return true + } } } } @@ -464,9 +466,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { binding?.navHostFragment?.apply { val params = layoutParams as ConstraintLayout.LayoutParams - val push = if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0 + val push = + if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0 - if(!this.isLtr()) { + if (!this.isLtr()) { params.setMargins( params.leftMargin, params.topMargin, @@ -695,27 +698,29 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { private fun onAllPluginsLoaded(success: Boolean = false) { ioSafe { pluginsLock.withLock { - // Load cloned sites after plugins have been loaded since clones depend on plugins. - try { - getKey>(USER_PROVIDER_API)?.let { list -> - list.forEach { custom -> - allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass } - ?.let { - allProviders.add(it.javaClass.newInstance().apply { - name = custom.name - lang = custom.lang - mainUrl = custom.url.trimEnd('/') - canBeOverridden = false - }) - } + synchronized(allProviders) { + // Load cloned sites after plugins have been loaded since clones depend on plugins. + try { + getKey>(USER_PROVIDER_API)?.let { list -> + list.forEach { custom -> + allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass } + ?.let { + allProviders.add(it.javaClass.newInstance().apply { + name = custom.name + lang = custom.lang + mainUrl = custom.url.trimEnd('/') + canBeOverridden = false + }) + } + } } + // it.hashCode() is not enough to make sure they are distinct + apis = + allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name } + APIHolder.apiMap = null + } catch (e: Exception) { + logError(e) } - // it.hashCode() is not enough to make sure they are distinct - apis = - allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name } - APIHolder.apiMap = null - } catch (e: Exception) { - logError(e) } } } @@ -814,6 +819,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { translationX = target.x translationY = target.y + bringToFront() } } @@ -839,10 +845,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val out = IntArray(2) newFocus.getLocationInWindow(out) val (screenX, screenY) = out - var (x,y) = screenX.toFloat() to screenY.toFloat() + var (x, y) = screenX.toFloat() to screenY.toFloat() val (currentX, currentY) = focusOutline.translationX to focusOutline.translationY - // println(">><<< $x $y $currentX $currentY") - if(!newFocus.isLtr()) { + // println(">><<< $x $y $currentX $currentY") + if (!newFocus.isLtr()) { x = x - focusOutline.rootView.width + newFocus.measuredWidth } @@ -1195,7 +1201,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { ioSafe { initAll() // No duplicates (which can happen by registerMainAPI) - apis = allProviders.distinctBy { it } + apis = synchronized(allProviders) { + allProviders.distinctBy { it } + } } // val navView: BottomNavigationView = findViewById(R.id.nav_view) @@ -1347,8 +1355,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { }*/ if (BuildConfig.DEBUG) { - try { - var providersAndroidManifestString = "Current androidmanifest should be:\n" + var providersAndroidManifestString = "Current androidmanifest should be:\n" + synchronized(allProviders) { for (api in allProviders) { providersAndroidManifestString += "\n" } - println(providersAndroidManifestString) - - } catch (t: Throwable) { - logError(t) } - + println(providersAndroidManifestString) } handleAppIntent(intent) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt index 07aa904e..5bbb4538 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt @@ -21,10 +21,11 @@ class CrossTmdbProvider : TmdbProvider() { return Regex("""[^a-zA-Z0-9-]""").replace(name, "") } - private val validApis by lazy { - apis.filter { it.lang == this.lang && it::class.java != this::class.java } - //.distinctBy { it.uniqueId } - } + private val validApis + get() = + synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } } + //.distinctBy { it.uniqueId } + data class CrossMetaData( @JsonProperty("isSuccess") val isSuccess: Boolean, @@ -60,7 +61,8 @@ class CrossTmdbProvider : TmdbProvider() { override suspend fun load(url: String): LoadResponse? { val base = super.load(url)?.apply { - this.recommendations = this.recommendations?.filterIsInstance() // TODO REMOVE + this.recommendations = + this.recommendations?.filterIsInstance() // TODO REMOVE val matchName = filterName(this.name) when (this) { is MovieLoadResponse -> { @@ -98,6 +100,7 @@ class CrossTmdbProvider : TmdbProvider() { this.dataUrl = CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson() } + else -> { throw ErrorLoadingException("Nothing besides movies are implemented for this provider") } diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt index e8ac1876..8cfe1e9a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt @@ -25,13 +25,16 @@ class MultiAnimeProvider : MainAPI() { } } - private val validApis by lazy { - APIHolder.apis.filter { - it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains( - TvType.Anime - ) - } - } + private val validApis + get() = + synchronized(APIHolder.apis) { + APIHolder.apis.filter { + it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains( + TvType.Anime + ) + } + } + private fun filterName(name: String): String { return Regex("""[^a-zA-Z0-9-]""").replace(name, "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index 242baf59..6b7dc90b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -36,7 +36,9 @@ abstract class Plugin { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI") element.sourcePlugin = this.__filename // Race condition causing which would case duplicates if not for distinctBy - APIHolder.allProviders.add(element) + synchronized(APIHolder.allProviders) { + APIHolder.allProviders.add(element) + } APIHolder.addPluginMapping(element) } @@ -51,10 +53,14 @@ abstract class Plugin { } class Manifest { - @JsonProperty("name") var name: String? = null - @JsonProperty("pluginClassName") var pluginClassName: String? = null - @JsonProperty("version") var version: Int? = null - @JsonProperty("requiresResources") var requiresResources: Boolean = false + @JsonProperty("name") + var name: String? = null + @JsonProperty("pluginClassName") + var pluginClassName: String? = null + @JsonProperty("version") + var version: Int? = null + @JsonProperty("requiresResources") + var requiresResources: Boolean = false } /** diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index 0dee57eb..49b5a752 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -163,7 +163,8 @@ object PluginManager { private val classLoaders: MutableMap = HashMap() - private var loadedLocalPlugins = false + var loadedLocalPlugins = false + private set private val gson = Gson() private suspend fun maybeLoadPlugin(context: Context, file: File) { @@ -531,10 +532,14 @@ object PluginManager { } // remove all registered apis - APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach { - removePluginMapping(it) + synchronized(APIHolder.apis) { + APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach { + removePluginMapping(it) + } + } + 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 } classLoaders.values.removeIf { v -> v == plugin } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index eb12c411..a6e1b5e6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -462,7 +462,7 @@ class HomeFragment : Fragment() { private val apiChangeClickListener = View.OnClickListener { view -> view.context.selectHomepage(currentApiName) { api -> - homeViewModel.loadAndCancel(api) + homeViewModel.loadAndCancel(api, forceReload = true,fromUI = true) } /*val validAPIs = view.context?.filterProviderByPreferredMedia()?.toMutableList() ?: mutableListOf() @@ -652,6 +652,7 @@ class HomeFragment : Fragment() { } homeViewModel.reloadStored() + homeViewModel.loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false) //loadHomePage(false) // nice profile pic on homepage diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index ce7b8447..fd2412da 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -447,12 +447,12 @@ class HomeParentItemAdapterPreview( (binding as? FragmentHomeHeadTvBinding)?.apply { homePreviewChangeApi.setOnClickListener { view -> view.context.selectHomepage(viewModel.repo?.name) { api -> - viewModel.loadAndCancel(api) + viewModel.loadAndCancel(api, forceReload = true, fromUI = true) } } homePreviewChangeApi2.setOnClickListener { view -> view.context.selectHomepage(viewModel.repo?.name) { api -> - viewModel.loadAndCancel(api) + viewModel.loadAndCancel(api, forceReload = true, fromUI = true) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index 563a326e..a2dc9821 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.filterHomePageListByFilmQuality import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia @@ -15,12 +14,22 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.mvvm.* +import com.lagradost.cloudstream3.HomePageList +import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.MainActivity +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.amap +import com.lagradost.cloudstream3.mvvm.Resource +import com.lagradost.cloudstream3.mvvm.debugAssert +import com.lagradost.cloudstream3.mvvm.debugWarning +import com.lagradost.cloudstream3.mvvm.launchSafe +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi import com.lagradost.cloudstream3.ui.WatchType -import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SearchClickCallback @@ -30,8 +39,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE -import com.lagradost.cloudstream3.utils.DataStore.getKey -import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds @@ -44,7 +51,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.withContext -import java.util.* +import java.util.EnumSet import kotlin.collections.set class HomeViewModel : ViewModel() { @@ -95,7 +102,7 @@ class HomeViewModel : ViewModel() { private var currentShuffledList: List = listOf() private fun autoloadRepo(): APIRepository { - return APIRepository(apis.first { it.hasMainPage }) + return APIRepository(synchronized(apis) { apis.first { it.hasMainPage }}) } private val _availableWatchStatusTypes = @@ -177,8 +184,10 @@ class HomeViewModel : ViewModel() { } private var onGoingLoad: Job? = null - private fun loadAndCancel(api: MainAPI?) { + private var isCurrentlyLoadingName : String? = null + private fun loadAndCancel(api: MainAPI) { onGoingLoad?.cancel() + isCurrentlyLoadingName = api.name onGoingLoad = load(api) } @@ -280,12 +289,12 @@ class HomeViewModel : ViewModel() { } } - private fun load(api: MainAPI?) = ioSafe { - repo = if (api != null) { + private fun load(api: MainAPI) : Job = ioSafe { + repo = //if (api != null) { APIRepository(api) - } else { - autoloadRepo() - } + //} else { + // autoloadRepo() + //} _apiName.postValue(repo?.name) _randomItems.postValue(listOf()) @@ -299,6 +308,7 @@ class HomeViewModel : ViewModel() { _page.postValue(Resource.Loading()) _preview.postValue(Resource.Loading()) + // cancel the current preview expand as that is no longer relevant addJob?.cancel() when (val data = repo?.getMainPage(1, null)) { @@ -370,7 +380,7 @@ class HomeViewModel : ViewModel() { else -> Unit } - onGoingLoad = null + isCurrentlyLoadingName = null } fun click(callback: SearchClickCallback) { @@ -437,33 +447,51 @@ class HomeViewModel : ViewModel() { loadResult(load.response.url, load.response.apiName, load.action) } - fun loadAndCancel(preferredApiName: String?, forceReload: Boolean = true) = - viewModelScope.launchSafe { + // only save the key if it is from UI, as we don't want internal functions changing the setting + fun loadAndCancel( + preferredApiName: String?, + forceReload: Boolean = true, + fromUI: Boolean = false + ) = + ioSafe { // Since plugins are loaded in stages this function can get called multiple times. // The issue with this is that the homepage may be fetched multiple times while the first request is loading val api = getApiFromNameNull(preferredApiName) - if (!forceReload && api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true) { - return@launchSafe + // api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true + val currentPage = page.value + + // if we don't need to reload and we have a valid homepage or currently loading the same thing then return + val currentLoading = isCurrentlyLoadingName + if (!forceReload && (currentPage is Resource.Success && currentPage.value.isNotEmpty() || (currentLoading != null && currentLoading == preferredApiName))) { + return@ioSafe } if (preferredApiName == noneApi.name) { - setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) + // just set to random + if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) loadAndCancel(noneApi) } else if (preferredApiName == randomApi.name) { + // randomize the api, if none exist like if not loaded or not installed + // then use nothing val validAPIs = context?.filterProviderByPreferredMedia() if (validAPIs.isNullOrEmpty()) { - // Do not set USER_SELECTED_HOMEPAGE_API when there is no plugins loaded loadAndCancel(noneApi) } else { val apiRandom = validAPIs.random() loadAndCancel(apiRandom) - setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name) + if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name) } - // If the plugin isn't loaded yet. (Does not set the key) } else if (api == null) { - loadAndCancel(noneApi) + // API is not found aka not loaded or removed, post the loading + // progress if waiting for plugins, otherwise nothing + if(PluginManager.loadedLocalPlugins) { + loadAndCancel(noneApi) + } else { + _page.postValue(Resource.Loading()) + } } else { - setKey(USER_SELECTED_HOMEPAGE_API, api.name) + // if the api is found, then set it to it and save key + if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, api.name) loadAndCancel(api) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index a20cd5c6..04ef3d96 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -163,12 +163,14 @@ class LibraryFragment : Fragment() { syncId: SyncIdName, apiName: String? = null, ) { - val availableProviders = allProviders.filter { - it.supportedSyncNames.contains(syncId) - }.map { it.name } + - // Add the api if it exists - (APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } ?: emptyList()) - + val availableProviders = synchronized(allProviders) { + allProviders.filter { + it.supportedSyncNames.contains(syncId) + }.map { it.name } + + // Add the api if it exists + (APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } + ?: emptyList()) + } val baseOptions = listOf( LibraryOpenerType.Default, LibraryOpenerType.None, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 88f55444..011d133d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -321,7 +321,7 @@ data class ExtractedTrailerData( class ResultViewModel2 : ViewModel() { private var currentResponse: LoadResponse? = null - var EPISODE_RANGE_SIZE : Int = 20 + var EPISODE_RANGE_SIZE: Int = 20 fun clear() { currentResponse = null _page.postValue(null) @@ -456,7 +456,7 @@ class ResultViewModel2 : ViewModel() { currentResponse.year ) ) - if(currentWatchType != status) { + if (currentWatchType != status) { MainActivity.bookmarksUpdatedEvent(true) } } @@ -477,7 +477,10 @@ class ResultViewModel2 : ViewModel() { ) ) - private fun getRanges(allEpisodes: Map>, EPISODE_RANGE_SIZE : Int): Map> { + private fun getRanges( + allEpisodes: Map>, + EPISODE_RANGE_SIZE: Int + ): Map> { return allEpisodes.keys.mapNotNull { index -> val episodes = allEpisodes[index] ?: return@mapNotNull null // this should never happened @@ -1505,13 +1508,14 @@ class ResultViewModel2 : ViewModel() { } val realRecommendations = ArrayList() - val apiNames = apis.filter { - it.name.contains("gogoanime", true) || - it.name.contains("9anime", true) - }.map { - it.name + val apiNames = synchronized(apis) { + apis.filter { + it.name.contains("gogoanime", true) || + it.name.contains("9anime", true) + }.map { + it.name + } } - meta.recommendations?.forEach { rec -> apiNames.forEach { name -> realRecommendations.add(rec.copy(apiName = name)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt index aceda644..320687f8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt @@ -37,7 +37,7 @@ class SearchViewModel : ViewModel() { private val _currentHistory: MutableLiveData> = MutableLiveData() val currentHistory: LiveData> get() = _currentHistory - private var repos = apis.map { APIRepository(it) } + private var repos = synchronized(apis) { apis.map { APIRepository(it) } } fun clearSearch() { _searchResponse.postValue(Resource.Success(ArrayList())) @@ -48,7 +48,7 @@ class SearchViewModel : ViewModel() { private var onGoingSearch: Job? = null fun reloadRepos() { - repos = apis.map { APIRepository(it) } + repos = synchronized(apis) { apis.map { APIRepository(it) } } } fun searchAndCancel( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 070389b0..e53fa91a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -8,7 +8,9 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageView import androidx.annotation.StringRes +import androidx.core.view.children import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.preference.Preference @@ -74,6 +76,7 @@ class SettingsFragment : Fragment() { settingsToolbar.apply { setTitle(title) setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) setNavigationOnClickListener { activity?.onBackPressed() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index b308efc7..85dd9540 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -20,6 +20,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.databinding.AddRemoveSitesBinding @@ -188,7 +189,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { fun showAdd() { - val providers = allProviders.distinctBy { it.javaClass }.sortedBy { it.name } + val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } } activity?.showDialog( providers.map { "${it.name} (${it.mainUrl})" }, -1, @@ -221,6 +222,8 @@ class SettingsGeneral : PreferenceFragmentCompat() { val newSite = CustomSite(provider.javaClass.simpleName, name, url, realLang) current.add(newSite) setKey(USER_PROVIDER_API, current.toTypedArray()) + // reload apis + MainActivity.afterPluginsLoadedEvent.invoke(false) dialog.dismissSafe(activity) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index 42a864a6..0bef5e9a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -105,8 +105,10 @@ class SettingsProviders : PreferenceFragmentCompat() { getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener { activity?.getApiProviderLangSettings()?.let { current -> - val languages = APIHolder.apis.map { it.lang }.toSet() - .sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName + val languages = synchronized(APIHolder.apis) { + APIHolder.apis.map { it.lang }.toSet() + .sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName + } val currentList = current.map { languages.indexOf(it) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt index 2e05baff..4fd24afe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt @@ -10,6 +10,7 @@ import com.lagradost.cloudstream3.utils.TestingUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel +import okhttp3.internal.toImmutableList class TestViewModel : ViewModel() { data class TestProgress( @@ -81,15 +82,14 @@ class TestViewModel : ViewModel() { } fun init() { - val apis = APIHolder.allProviders - total = apis.size + total = synchronized(APIHolder.allProviders) { APIHolder.allProviders.size } updateProgress() } fun startTest() { scope = CoroutineScope(Dispatchers.Default) - val apis = APIHolder.allProviders + val apis = synchronized(APIHolder.allProviders) { APIHolder.allProviders.toTypedArray() } total = apis.size failed = 0 passed = 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt index 138a31a3..4369b22f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt @@ -107,7 +107,7 @@ class SetupFragmentExtensions : Fragment() { if (isSetup) if ( // If any available languages - apis.distinctBy { it.lang }.size > 1 + synchronized(apis) { apis.distinctBy { it.lang }.size > 1 } ) { findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt index 8637fc99..59dcc402 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt @@ -51,8 +51,8 @@ class SetupFragmentProviderLanguage : Fragment() { ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) val current = ctx.getApiProviderLangSettings() - val langs = APIHolder.apis.map { it.lang }.toSet() - .sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName + val langs = synchronized(APIHolder.apis) { APIHolder.apis.map { it.lang }.toSet() + .sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName} val currentList = current.map { langs.indexOf(it) }.filter { it != -1 } // TODO LOOK INTO diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index e1cedd39..dd2b40a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -18,6 +18,8 @@ const val USER_PROVIDER_API = "user_custom_sites" const val PREFERENCES_NAME = "rebuild_preference" +// TODO degelgate by value for get & set + object DataStore { val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt index 817e9235..71d3a1ef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -96,8 +96,10 @@ object SyncUtil { .mapNotNull { it.url }.toMutableList() if (type == "anilist") { // TODO MAKE BETTER - apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { - current.add("${it.mainUrl}/anime/$id") + synchronized(apis) { + apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { + current.add("${it.mainUrl}/anime/$id") + } } } return current diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index 66e1e504..dd973538 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -211,7 +211,7 @@ object TestingUtils { fun getDeferredProviderTests( scope: CoroutineScope, - providers: List, + providers: Array, logger: (String) -> Unit, callback: (MainAPI, TestResultProvider) -> Unit ) { diff --git a/app/src/main/res/layout/fragment_home_head_tv.xml b/app/src/main/res/layout/fragment_home_head_tv.xml index 0a2c52b2..8592daea 100644 --- a/app/src/main/res/layout/fragment_home_head_tv.xml +++ b/app/src/main/res/layout/fragment_home_head_tv.xml @@ -1,13 +1,13 @@ - + android:orientation="vertical"> @@ -39,7 +39,9 @@ android:layout_width="wrap_content" android:layout_gravity="top|start" android:layout_marginStart="@dimen/navbar_width" - android:minWidth="150dp" /> + android:minWidth="150dp" + android:nextFocusLeft="@id/nav_rail_view" + android:nextFocusDown="@id/home_preview_play_btt" /> + android:minWidth="150dp" + android:nextFocusLeft="@id/nav_rail_view" + android:nextFocusDown="@id/home_watch_child_recyclerview" /> @null - @drawable/outline_drawable_less + @drawable/outline_drawable_forced false @@ -572,6 +572,7 @@ @drawable/ic_baseline_check_24_listview + + + - \ No newline at end of file + From 3ac462ae9625f8a546e0c95f947da00c6283eba3 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 12 Aug 2023 21:20:51 +0200 Subject: [PATCH 073/156] changed UI a bit for flashbang + fixed crash inf loading bug --- app/build.gradle.kts | 2 +- .../lagradost/cloudstream3/MainActivity.kt | 30 ++-- .../cloudstream3/ui/home/HomeFragment.kt | 23 ++- .../ui/home/HomeParentItemAdapterPreview.kt | 55 +++---- .../cloudstream3/ui/home/HomeViewModel.kt | 3 +- .../ui/settings/SettingsUpdates.kt | 4 +- .../lagradost/cloudstream3/utils/UIHelper.kt | 7 +- .../res/color/player_on_button_tv_attr.xml | 5 + app/src/main/res/color/white_attr_20.xml | 4 + .../res/drawable/player_button_tv_attr.xml | 15 ++ .../drawable/player_button_tv_attr_no_bg.xml | 9 ++ app/src/main/res/layout/fragment_home.xml | 47 ++++-- .../main/res/layout/fragment_home_head.xml | 24 ++-- .../main/res/layout/fragment_home_head_tv.xml | 135 +++++++++++------- app/src/main/res/layout/fragment_home_tv.xml | 52 ++++--- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/colors.xml | 1 - app/src/main/res/values/strings.xml | 1 - app/src/main/res/values/styles.xml | 19 ++- 19 files changed, 252 insertions(+), 185 deletions(-) create mode 100644 app/src/main/res/color/player_on_button_tv_attr.xml create mode 100644 app/src/main/res/color/white_attr_20.xml create mode 100644 app/src/main/res/drawable/player_button_tv_attr.xml create mode 100644 app/src/main/res/drawable/player_button_tv_attr_no_bg.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9300775c..cfe89c05 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -51,7 +51,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.1.2" + versionName = "4.1.3" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index d6e275ed..a8160d33 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -278,7 +278,7 @@ var app = Requests(responseParser = object : ResponseParser { class MainActivity : AppCompatActivity(), ColorPickerDialogListener { companion object { const val TAG = "MAINACT" - + var lastError: String? = null /** * Setting this will automatically enter the query in the search * next time the search fragment is opened. @@ -599,22 +599,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } override fun dispatchKeyEvent(event: KeyEvent?): Boolean { - val start = System.currentTimeMillis() - try { - val response = CommonActivity.dispatchKeyEvent(this, event) - - if (response != null) - return response - } finally { - debugAssert({ - val end = System.currentTimeMillis() - val delta = end - start - delta > 100 - }) { - "Took over 100ms to navigate, smth is VERY wrong" - } - } - + val response = CommonActivity.dispatchKeyEvent(this, event) + if (response != null) + return response return super.dispatchKeyEvent(event) } @@ -1054,10 +1041,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val errorFile = filesDir.resolve("last_error") - var lastError: String? = null if (errorFile.exists() && errorFile.isFile) { lastError = errorFile.readText(Charset.defaultCharset()) errorFile.delete() + } else { + lastError = null } val settingsForProvider = SettingsJson() @@ -1167,16 +1155,16 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } //Automatically download not existing plugins, using mode specified. - val auto_download_plugin = AutoDownloadMode.getEnum( + val autoDownloadPlugin = AutoDownloadMode.getEnum( settingsManager.getInt( getString(R.string.auto_download_plugins_key), 0 ) ) ?: AutoDownloadMode.Disable - if (auto_download_plugin != AutoDownloadMode.Disable) { + if (autoDownloadPlugin != AutoDownloadMode.Disable) { PluginManager.downloadNotExistingPluginsAndLoad( this@MainActivity, - auto_download_plugin + autoDownloadPlugin ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 6f9a1654..fa0b6dfb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -513,9 +513,13 @@ class HomeFragment : Fragment() { fixGrid() binding?.apply { - homeChangeApiLoading.setOnClickListener(apiChangeClickListener) + //homeChangeApiLoading.setOnClickListener(apiChangeClickListener) //homeChangeApiLoading.setOnClickListener(apiChangeClickListener) homeApiFab.setOnClickListener(apiChangeClickListener) + homeChangeApi.setOnClickListener(apiChangeClickListener) + homeSwitchAccount.setOnClickListener { v -> + DataStoreHelper.showWhoIsWatching(v?.context ?: return@setOnClickListener) + } homeRandom.setOnClickListener { if (listHomepageItems.isNotEmpty()) { activity.loadSearchResult(listHomepageItems.random()) @@ -527,21 +531,9 @@ class HomeFragment : Fragment() { mutableListOf(), homeViewModel ) - fixPaddingStatusbar(homeLoadingStatusbar) + //fixPaddingStatusbar(homeLoadingStatusbar) - if (isTvSettings()) { - homeApiFab.isVisible = false - if (isTrueTvSettings()) { - homeChangeApiLoading.isVisible = true - homeChangeApiLoading.isFocusable = true - homeChangeApiLoading.isFocusableInTouchMode = true - } - // home_bookmark_select?.isFocusable = true - // home_bookmark_select?.isFocusableInTouchMode = true - } else { - homeApiFab.isVisible = true - homeChangeApiLoading.isVisible = false - } + homeApiFab.isVisible = !isTvSettings() homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { @@ -574,6 +566,7 @@ class HomeFragment : Fragment() { observe(homeViewModel.apiName) { apiName -> currentApiName = apiName binding?.homeApiFab?.text = apiName + binding?.homeChangeApi?.text = apiName } observe(homeViewModel.page) { data -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 1684dfe5..943f784a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.ui.home import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.FrameLayout import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat import androidx.core.view.isGone @@ -13,7 +12,6 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipGroup import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity @@ -41,10 +39,9 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSet import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes -import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarMargin import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView -import com.lagradost.cloudstream3.utils.UIHelper.setImage +import com.lagradost.cloudstream3.utils.UIHelper.populateChips class HomeParentItemAdapterPreview( items: MutableList, @@ -245,7 +242,11 @@ class HomeParentItemAdapterPreview( private val previewViewpager: ViewPager2 = itemView.findViewById(R.id.home_preview_viewpager) - private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview) + + private val previewViewpagerText: ViewGroup = + itemView.findViewById(R.id.home_preview_viewpager_text) + + // private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview) private var resumeHolder: View = itemView.findViewById(R.id.home_watch_holder) private var resumeRecyclerView: RecyclerView = itemView.findViewById(R.id.home_watch_child_recyclerview) @@ -254,7 +255,7 @@ class HomeParentItemAdapterPreview( itemView.findViewById(R.id.home_bookmarked_child_recyclerview) private var homeAccount: View? = - itemView.findViewById(R.id.home_switch_account) + itemView.findViewById(R.id.home_preview_switch_account) private var topPadding : View? = itemView.findViewById(R.id.home_padding) @@ -282,26 +283,8 @@ class HomeParentItemAdapterPreview( item.plot ?: "" homePreviewText.text = item.name - homePreviewTags.apply { - removeAllViews() - item.tags?.forEach { tag -> - val chip = Chip(context) - val chipDrawable = - ChipDrawable.createFromAttributes( - context, - null, - 0, - R.style.ChipFilledSemiTransparent - ) - chip.setChipDrawable(chipDrawable) - chip.text = tag - chip.isChecked = false - chip.isCheckable = false - chip.isFocusable = false - chip.isClickable = false - addView(chip) - } - } + populateChips(homePreviewTags,item.tags ?: emptyList(), R.style.ChipFilledSemiTransparent) + homePreviewTags.isGone = item.tags.isNullOrEmpty() @@ -324,7 +307,7 @@ class HomeParentItemAdapterPreview( } (binding as? FragmentHomeHeadBinding)?.apply { - homePreviewImage.setImage(item.posterUrl, item.posterHeaders) + //homePreviewImage.setImage(item.posterUrl, item.posterHeaders) homePreviewPlay.setOnClickListener { view -> viewModel.click( @@ -402,7 +385,6 @@ class HomeParentItemAdapterPreview( if (binding is FragmentHomeHeadTvBinding) { observe(viewModel.apiName) { name -> binding.homePreviewChangeApi.text = name - binding.homePreviewChangeApi2.text = name } } observe(viewModel.resumeWatching) { @@ -468,11 +450,6 @@ class HomeParentItemAdapterPreview( viewModel.loadAndCancel(api, forceReload = true, fromUI = true) } } - homePreviewChangeApi2.setOnClickListener { view -> - view.context.selectHomepage(viewModel.repo?.name) { api -> - viewModel.loadAndCancel(api, forceReload = true, fromUI = true) - } - } // This makes the hidden next buttons only available when on the info button // Otherwise you might be able to go to the next item without being at the info button @@ -517,10 +494,6 @@ class HomeParentItemAdapterPreview( } private fun updatePreview(preview: Resource>>) { - if (binding is FragmentHomeHeadTvBinding) { - binding.homePreviewChangeApi2.isGone = preview is Resource.Success - } - if (preview is Resource.Success) { homeNonePadding.apply { val params = layoutParams @@ -545,14 +518,18 @@ class HomeParentItemAdapterPreview( previewViewpager.fakeDragBy(1f) previewViewpager.endFakeDrag() previewCallback.onPageSelected(0) - previewHeader.isVisible = true + previewViewpager.isVisible = true + previewViewpagerText.isVisible = true + //previewHeader.isVisible = true } } else -> { previewAdapter.setItems(listOf(), false) previewViewpager.setCurrentItem(0, false) - previewHeader.isVisible = false + previewViewpager.isVisible = false + previewViewpagerText.isVisible = false + //previewHeader.isVisible = false } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index a2dc9821..b1ced59e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -18,6 +18,7 @@ import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.MainActivity +import com.lagradost.cloudstream3.MainActivity.Companion.lastError import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.mvvm.Resource @@ -484,7 +485,7 @@ class HomeViewModel : ViewModel() { } else if (api == null) { // API is not found aka not loaded or removed, post the loading // progress if waiting for plugins, otherwise nothing - if(PluginManager.loadedLocalPlugins) { + if(PluginManager.loadedLocalPlugins || PluginManager.checkSafeModeFile() || lastError != null) { loadAndCancel(noneApi) } else { _page.postValue(Resource.Loading()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index 9227409d..c304629a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -177,7 +177,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { val prefNames = resources.getStringArray(R.array.auto_download_plugin) val prefValues = enumValues().sortedBy { x -> x.value }.map { x -> x.value } - val current = settingsManager.getInt(getString(R.string.auto_download_plugins_pref), 0) + val current = settingsManager.getInt(getString(R.string.auto_download_plugins_key), 0) activity?.showBottomDialog( prefNames.toList(), @@ -185,7 +185,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { getString(R.string.automatic_plugin_download_mode_title), true, {}) { - settingsManager.edit().putInt(getString(R.string.auto_download_plugins_pref), prefValues[it]).apply() + settingsManager.edit().putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() (context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } return@setOnPreferenceClickListener true diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index 5a393ed5..038a2f11 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -24,6 +24,7 @@ import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.annotation.IdRes +import androidx.annotation.StyleRes import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.widget.PopupMenu @@ -81,7 +82,7 @@ object UIHelper { || Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) } - fun populateChips(view: ChipGroup?, tags: List) { + fun populateChips(view: ChipGroup?, tags: List, @StyleRes style : Int = R.style.ChipFilled) { if (view == null) return view.removeAllViews() val context = view.context ?: return @@ -92,7 +93,7 @@ object UIHelper { context, null, 0, - R.style.ChipFilled + style ) chip.setChipDrawable(chipDrawable) chip.text = tag @@ -100,7 +101,7 @@ object UIHelper { chip.isCheckable = false chip.isFocusable = false chip.isClickable = false - chip.setTextColor(context.colorFromAttribute(R.attr.textColor)) + chip.setTextColor(context.colorFromAttribute(R.attr.white)) view.addView(chip) } } diff --git a/app/src/main/res/color/player_on_button_tv_attr.xml b/app/src/main/res/color/player_on_button_tv_attr.xml new file mode 100644 index 00000000..feb1eeb0 --- /dev/null +++ b/app/src/main/res/color/player_on_button_tv_attr.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/white_attr_20.xml b/app/src/main/res/color/white_attr_20.xml new file mode 100644 index 00000000..e0237df0 --- /dev/null +++ b/app/src/main/res/color/white_attr_20.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/player_button_tv_attr.xml b/app/src/main/res/drawable/player_button_tv_attr.xml new file mode 100644 index 00000000..4c90a64e --- /dev/null +++ b/app/src/main/res/drawable/player_button_tv_attr.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml b/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml new file mode 100644 index 00000000..b9b927da --- /dev/null +++ b/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index eb38e262..672a6d21 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -13,7 +13,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone" - tools:visibility="gone"> + tools:visibility="visible"> - + + + - + android:contentDescription="@string/account" + android:nextFocusLeft="@id/home_search" + android:padding="10dp" + android:src="@drawable/ic_outline_account_circle_24" /> + + tools:listitem="@layout/homepage_parent" + tools:visibility="gone" /> @@ -26,15 +26,6 @@ - - @@ -150,6 +141,7 @@ app:tint="?attr/white" /> + diff --git a/app/src/main/res/layout/fragment_home_head_tv.xml b/app/src/main/res/layout/fragment_home_head_tv.xml index d2c20bc4..03766d79 100644 --- a/app/src/main/res/layout/fragment_home_head_tv.xml +++ b/app/src/main/res/layout/fragment_home_head_tv.xml @@ -15,7 +15,6 @@ android:layout_height="0dp" /> @@ -28,7 +27,44 @@ - + + + + + + + + + - - - + + + tools:visibility="visible"> - + + + + - + android:padding="10dp" + android:src="@drawable/ic_outline_account_circle_24" + android:tag="@string/tv_no_focus_tag" + app:tint="@color/player_on_button_tv_attr" /> + - + tools:listitem="@layout/homepage_parent_tv" + tools:visibility="gone" /> + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 7dd4c989..d9258c40 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -13,7 +13,6 @@ #161616 #e9eaee - #1AFFFFFF #9ba0a4 #DCDCDC diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 74515fbf..c80e0e76 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,7 +7,6 @@ auto_update auto_update_plugins auto_download_plugins_key - auto_download_plugins_pref skip_update_key prerelease_update manual_check_update diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 3ef56c22..e2f11221 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -62,7 +62,8 @@ @color/iconGrayBackground @color/boxItemBackground @color/iconColor - #FFF + @color/white + @color/black @style/CustomPreferenceThemeOverlay @@ -99,7 +100,7 @@ @@ -117,6 +118,7 @@ @color/textColor @color/grayTextColor @color/white + @color/black @color/whiteText @@ -158,7 +160,9 @@ @color/lightItemBackground @color/lightTextColor @color/lightGrayTextColor - #000 + @color/black + @color/white + @color/blackText @@ -170,6 +174,7 @@ @color/material_dynamic_neutral90 @color/material_dynamic_neutral60 @color/material_dynamic_neutral90 + @color/material_dynamic_neutral10 @color/material_on_primary_emphasis_medium @@ -747,13 +752,13 @@ @null @color/transparent @null - @drawable/player_button_tv + @drawable/player_button_tv_attr @color/white @color/transparent - @color/player_on_button_tv - @color/player_on_button_tv - @color/player_on_button_tv + @color/player_on_button_tv_attr + @color/player_on_button_tv_attr + @color/player_on_button_tv_attr wrap_content 40dp 16dp From e43b4808d1be1dfad1b308f971e54c835ee074b8 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 12 Aug 2023 21:23:43 +0200 Subject: [PATCH 074/156] phone fix --- app/src/main/res/layout/fragment_home.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 672a6d21..ac660ccd 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -93,6 +93,7 @@ +/> Date: Sat, 12 Aug 2023 21:52:37 +0200 Subject: [PATCH 075/156] should fix an issue with auto_download_plugins_key --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c80e0e76..ded7366b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,7 +6,7 @@ search_type_list auto_update auto_update_plugins - auto_download_plugins_key + auto_download_plugins_key2 skip_update_key prerelease_update manual_check_update From d2d2e41fb31a7da70adf6ef080540833a7070657 Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Sat, 12 Aug 2023 20:25:30 +0000 Subject: [PATCH 076/156] Added Simkl (#548) --- .github/workflows/build_to_archive.yml | 2 + .github/workflows/prerelease.yml | 2 + app/build.gradle.kts | 25 +- .../com/lagradost/cloudstream3/MainAPI.kt | 29 +- .../syncproviders/AccountManager.kt | 7 +- .../cloudstream3/syncproviders/SyncAPI.kt | 31 +- .../cloudstream3/syncproviders/SyncRepo.kt | 4 +- .../syncproviders/providers/AniListApi.kt | 4 +- .../syncproviders/providers/LocalList.kt | 4 +- .../syncproviders/providers/MALApi.kt | 2 +- .../syncproviders/providers/SimklApi.kt | 848 ++++++++++++++++++ .../cloudstream3/ui/result/SyncViewModel.kt | 38 +- .../ui/settings/SettingsAccount.kt | 2 + app/src/main/res/drawable/simkl_logo.xml | 9 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/settings_account.xml | 43 +- 16 files changed, 988 insertions(+), 63 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt create mode 100644 app/src/main/res/drawable/simkl_logo.xml diff --git a/.github/workflows/build_to_archive.yml b/.github/workflows/build_to_archive.yml index 9cd2c523..3b7aa9ae 100644 --- a/.github/workflows/build_to_archive.yml +++ b/.github/workflows/build_to_archive.yml @@ -56,6 +56,8 @@ jobs: SIGNING_KEY_ALIAS: "key0" SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} + SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} + SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} - uses: actions/checkout@v3 with: repository: "recloudstream/cloudstream-archive" diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 856d267c..58009a7a 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -48,6 +48,8 @@ jobs: SIGNING_KEY_ALIAS: "key0" SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} + SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} + SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} - name: Create pre-release uses: "marvinpinto/action-automatic-releases@latest" with: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cfe89c05..3c12652a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,4 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import org.jetbrains.dokka.gradle.DokkaTask import java.io.ByteArrayOutputStream import java.net.URL @@ -54,17 +55,27 @@ android { versionName = "4.1.3" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") - resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") - resValue("bool", "is_prerelease", "false") + // Reads local.properties + val localProperties = gradleLocalProperties(rootDir) + buildConfigField( "String", "BUILDDATE", "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));" ) - + buildConfigField( + "String", + "SIMKL_CLIENT_ID", + "\"" + (System.getenv("SIMKL_CLIENT_ID") ?: localProperties["simkl.id"]) + "\"" + ) + buildConfigField( + "String", + "SIMKL_CLIENT_SECRET", + "\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\"" + ) testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" kapt { @@ -108,9 +119,9 @@ android { } } //toolchain { - // languageVersion.set(JavaLanguageVersion.of(17)) - // } - // jvmToolchain(17) + // languageVersion.set(JavaLanguageVersion.of(17)) + // } + // jvmToolchain(17) compileOptions { isCoreLibraryDesugaringEnabled = true @@ -211,7 +222,7 @@ dependencies { // Networking // implementation("com.squareup.okhttp3:okhttp:4.9.2") // implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") - implementation("com.github.Blatzar:NiceHttp:0.4.2") + implementation("com.github.Blatzar:NiceHttp:0.4.3") // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") // Util to skip the URI file fuckery 🙏 diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 76abda97..7790f047 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -11,9 +11,12 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.KotlinModule import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.syncproviders.providers.SimklApi import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson @@ -821,7 +824,8 @@ public enum class AutoDownloadMode(val value: Int) { ; companion object { - infix fun getEnum(value: Int): AutoDownloadMode? = AutoDownloadMode.values().firstOrNull { it.value == value } + infix fun getEnum(value: Int): AutoDownloadMode? = + AutoDownloadMode.values().firstOrNull { it.value == value } } } @@ -1143,6 +1147,7 @@ interface LoadResponse { companion object { private val malIdPrefix = malApi.idPrefix private val aniListIdPrefix = aniListApi.idPrefix + private val simklIdPrefix = simklApi.idPrefix var isTrailersEnabled = true fun LoadResponse.isMovie(): Boolean { @@ -1164,6 +1169,20 @@ interface LoadResponse { this.actors = actors?.map { (actor, role) -> ActorData(actor, role = role) } } + /** + * Internal helper function to add simkl ids from other databases. + */ + private fun LoadResponse.addSimklId( + database: SimklApi.Companion.SyncServices, + id: String? + ) { + normalSafeApiCall { + this.syncData[simklIdPrefix] = + SimklApi.addIdToString(this.syncData[simklIdPrefix], database, id.toString()) + ?: return@normalSafeApiCall + } + } + @JvmName("addActorsOnly") fun LoadResponse.addActors(actors: List?) { this.actors = actors?.map { actor -> ActorData(actor) } @@ -1179,10 +1198,16 @@ interface LoadResponse { fun LoadResponse.addMalId(id: Int?) { this.syncData[malIdPrefix] = (id ?: return).toString() + this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString()) } fun LoadResponse.addAniListId(id: Int?) { this.syncData[aniListIdPrefix] = (id ?: return).toString() + this.addSimklId(SimklApi.Companion.SyncServices.AniList, id.toString()) + } + + fun LoadResponse.addSimklId(id: Int?) { + this.addSimklId(SimklApi.Companion.SyncServices.Simkl, id.toString()) } fun LoadResponse.addImdbUrl(url: String?) { @@ -1264,6 +1289,7 @@ interface LoadResponse { fun LoadResponse.addImdbId(id: String?) { // TODO add imdb sync + this.addSimklId(SimklApi.Companion.SyncServices.Imdb, id) } fun LoadResponse.addTrackId(id: String?) { @@ -1276,6 +1302,7 @@ interface LoadResponse { fun LoadResponse.addTMDbId(id: String?) { // TODO add TMDb sync + this.addSimklId(SimklApi.Companion.SyncServices.Tmdb, id) } fun LoadResponse.addRating(text: String?) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index 8ce6bae2..8bf8dffa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -11,6 +11,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val malApi = MALApi(0) val aniListApi = AniListApi(0) val openSubtitlesApi = OpenSubtitlesApi(0) + val simklApi = SimklApi(0) val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() val localListApi = LocalList() @@ -18,19 +19,19 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { // used to login via app intent val OAuth2Apis get() = listOf( - malApi, aniListApi + malApi, aniListApi, simklApi ) // this needs init with context and can be accessed in settings val accountManagers get() = listOf( - malApi, aniListApi, openSubtitlesApi, //nginxApi + malApi, aniListApi, openSubtitlesApi, simklApi //nginxApi ) // used for active syncing val SyncApis get() = listOf( - SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi) + SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi), SyncRepo(simklApi) ) val inAppAuths diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt index 8c76c5bf..ed496326 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -10,7 +10,8 @@ enum class SyncIdName { MyAnimeList, Trakt, Imdb, - LocalList + Simkl, + LocalList, } interface SyncAPI : OAuth2API { @@ -35,9 +36,9 @@ interface SyncAPI : OAuth2API { 4 -> PlanToWatch 5 -> ReWatching */ - suspend fun score(id: String, status: SyncStatus): Boolean + suspend fun score(id: String, status: AbstractSyncStatus): Boolean - suspend fun getStatus(id: String): SyncStatus? + suspend fun getStatus(id: String): AbstractSyncStatus? suspend fun getResult(id: String): SyncResult? @@ -59,14 +60,24 @@ interface SyncAPI : OAuth2API { override var id: Int? = null, ) : SearchResponse - data class SyncStatus( - val status: Int, + abstract class AbstractSyncStatus { + abstract var status: Int + /** 1-10 */ - val score: Int?, - val watchedEpisodes: Int?, - var isFavorite: Boolean? = null, - var maxEpisodes: Int? = null, - ) + abstract var score: Int? + abstract var watchedEpisodes: Int? + abstract var isFavorite: Boolean? + abstract var maxEpisodes: Int? + } + + data class SyncStatus( + override var status: Int, + /** 1-10 */ + override var score: Int?, + override var watchedEpisodes: Int?, + override var isFavorite: Boolean? = null, + override var maxEpisodes: Int? = null, + ) : AbstractSyncStatus() data class SyncResult( /**Used to verify*/ diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt index 85b877e0..9363cb6f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt @@ -18,11 +18,11 @@ class SyncRepo(private val repo: SyncAPI) { repo.requireLibraryRefresh = value } - suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource { + suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Resource { return safeApiCall { repo.score(id, status) } } - suspend fun getStatus(id: String): Resource { + suspend fun getStatus(id: String): Resource { return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index 0010ce25..d0c88901 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -158,7 +158,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) } - override suspend fun getStatus(id: String): SyncAPI.SyncStatus? { + override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { val internalId = id.toIntOrNull() ?: return null val data = getDataAboutId(internalId) ?: return null @@ -171,7 +171,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) } - override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { + override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { return postDataAboutId( id.toIntOrNull() ?: return false, fromIntToAnimeStatus(status.status), diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt index 7dd43fe7..e6ca9711 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -45,11 +45,11 @@ class LocalList : SyncAPI { override val mainUrl = "" override val syncIdName = SyncIdName.LocalList - override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { + override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { return true } - override suspend fun getStatus(id: String): SyncAPI.SyncStatus? { + override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { return null } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index 5164b606..02826401 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -91,7 +91,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { return Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first() } - override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { + override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { return setScoreRequest( id.toIntOrNull() ?: return false, fromIntToAnimeStatus(status.status), diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt new file mode 100644 index 00000000..64afb0e2 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -0,0 +1,848 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import androidx.annotation.StringRes +import androidx.core.net.toUri +import androidx.fragment.app.FragmentActivity +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.debugAssert +import com.lagradost.cloudstream3.mvvm.debugPrint +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall +import com.lagradost.cloudstream3.syncproviders.AccountManager +import com.lagradost.cloudstream3.syncproviders.AuthAPI +import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.ui.library.ListSorting +import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import okhttp3.Interceptor +import okhttp3.Response +import java.math.BigInteger +import java.security.SecureRandom +import java.text.SimpleDateFormat +import java.time.Instant +import java.util.Date +import java.util.TimeZone + +class SimklApi(index: Int) : AccountManager(index), SyncAPI { + override var name = "Simkl" + override val key = "simkl-key" + override val redirectUrl = "simkl" + override val idPrefix = "simkl" + override var requireLibraryRefresh = true + override var mainUrl = "https://api.simkl.com" + override val icon = R.drawable.simkl_logo + override val requiresLogin = false + override val createAccountUrl = "$mainUrl/signup" + override val syncIdName = SyncIdName.Simkl + private val token: String? + get() = getKey(accountId, SIMKL_TOKEN_KEY).also { + debugAssert({ it == null }) { "No ${this.name} token!" } + } + + /** Automatically adds simkl auth headers */ + private val interceptor = HeaderInterceptor() + + /** + * This is required to override the reported last activity as simkl activites + * may not always update based on testing. + */ + private var lastScoreTime = -1L + + companion object { + private const val clientId = BuildConfig.SIMKL_CLIENT_ID + private const val clientSecret = BuildConfig.SIMKL_CLIENT_SECRET + private var lastLoginState = "" + + const val SIMKL_TOKEN_KEY: String = "simkl_token" + const val SIMKL_USER_KEY: String = "simkl_user" + const val SIMKL_CACHED_LIST: String = "simkl_cached_list" + const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time" + + /** 2014-09-01T09:10:11Z -> 1409562611 */ + private const val simklDateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + fun getUnixTime(string: String?): Long? { + return try { + SimpleDateFormat(simklDateFormat).apply { + this.timeZone = TimeZone.getTimeZone("UTC") + }.parse( + string ?: return null + )?.toInstant()?.epochSecond + } catch (e: Exception) { + logError(e) + return null + } + } + + /** 1409562611 -> 2014-09-01T09:10:11Z */ + fun getDateTime(unixTime: Long?): String? { + return try { + SimpleDateFormat(simklDateFormat).apply { + this.timeZone = TimeZone.getTimeZone("UTC") + }.format( + Date.from( + Instant.ofEpochSecond( + unixTime ?: return null + ) + ) + ) + } catch (e: Exception) { + null + } + } + + /** + * 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 */ + private fun readIdFromString(idString: String?): Map { + return tryParseJson(idString) ?: return emptyMap() + } + + fun getPosterUrl(poster: String): String { + return "https://wsrv.nl/?url=https://simkl.in/posters/${poster}_m.webp" + } + + private fun getUrlFromId(id: Int): String { + return "https://simkl.com/shows/$id" + } + + enum class SimklListStatusType( + var value: Int, + @StringRes val stringRes: Int, + val originalName: String? + ) { + Watching(0, R.string.type_watching, "watching"), + Completed(1, R.string.type_completed, "completed"), + Paused(2, R.string.type_on_hold, "hold"), + Dropped(3, R.string.type_dropped, "dropped"), + Planning(4, R.string.type_plan_to_watch, "plantowatch"), + ReWatching(5, R.string.type_re_watching, "watching"), + None(-1, R.string.none, null); + + companion object { + fun fromString(string: String): SimklListStatusType? { + return SimklListStatusType.values().firstOrNull { + it.originalName == string + } + } + } + } + + // ------------------- + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class TokenRequest( + @JsonProperty("code") val code: String, + @JsonProperty("client_id") val client_id: String = clientId, + @JsonProperty("client_secret") val client_secret: String = clientSecret, + @JsonProperty("redirect_uri") val redirect_uri: String = "$appString://simkl", + @JsonProperty("grant_type") val grant_type: String = "authorization_code" + ) + + data class TokenResponse( + /** No expiration date */ + val access_token: String, + val token_type: String, + val scope: String + ) + // ------------------- + + /** https://simkl.docs.apiary.io/#reference/users/settings/receive-settings */ + data class SettingsResponse( + val user: User + ) { + data class User( + val name: String, + /** Url */ + val avatar: String + ) + } + + // ------------------- + data class ActivitiesResponse( + val all: String?, + val tv_shows: UpdatedAt, + val anime: UpdatedAt, + val movies: UpdatedAt, + ) { + data class UpdatedAt( + val all: String?, + val removed_from_list: String?, + val rated_at: String?, + ) + } + + /** https://simkl.docs.apiary.io/#reference/tv/episodes/get-tv-show-episodes */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class EpisodeMetadata( + @JsonProperty("title") val title: String?, + @JsonProperty("description") val description: String?, + @JsonProperty("season") val season: Int?, + @JsonProperty("episode") val episode: Int, + @JsonProperty("img") val img: String? + ) { + companion object { + fun convertToEpisodes(list: List?): List { + return list?.map { + MediaObject.Season.Episode(it.episode) + } ?: emptyList() + } + + fun convertToSeasons(list: List?): List { + return list?.filter { it.season != null }?.groupBy { + it.season + }?.map { (season, episodes) -> + MediaObject.Season(season!!, convertToEpisodes(episodes)) + } ?: emptyList() + } + } + } + + /** + * https://simkl.docs.apiary.io/#introduction/about-simkl-api/standard-media-objects + * Useful for finding shows from metadata + */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + open class MediaObject( + @JsonProperty("title") val title: String?, + @JsonProperty("year") val year: Int?, + @JsonProperty("ids") val ids: Ids?, + @JsonProperty("poster") val poster: String? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("seasons") val seasons: List? = null, + @JsonProperty("episodes") val episodes: List? = null + ) { + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class Season( + @JsonProperty("number") val number: Int, + @JsonProperty("episodes") val episodes: List + ) { + data class Episode(@JsonProperty("number") val number: Int) + } + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class Ids( + @JsonProperty("simkl") val simkl: Int?, + @JsonProperty("imdb") val imdb: String? = null, + @JsonProperty("tmdb") val tmdb: String? = null, + @JsonProperty("mal") val mal: String? = null, + @JsonProperty("anilist") val anilist: String? = null, + ) { + companion object { + fun fromMap(map: Map): Ids { + return Ids( + simkl = map[SyncServices.Simkl]?.toIntOrNull(), + imdb = map[SyncServices.Imdb], + tmdb = map[SyncServices.Tmdb], + mal = map[SyncServices.Mal], + anilist = map[SyncServices.AniList] + ) + } + } + } + + fun toSyncSearchResult(): SyncAPI.SyncSearchResult? { + return SyncAPI.SyncSearchResult( + this.title ?: return null, + "Simkl", + this.ids?.simkl?.toString() ?: return null, + getUrlFromId(this.ids.simkl), + this.poster?.let { getPosterUrl(it) }, + if (this.type == "movie") TvType.Movie else TvType.TvSeries + ) + } + } + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + class RatingMediaObject( + @JsonProperty("title") title: String?, + @JsonProperty("year") year: Int?, + @JsonProperty("ids") ids: Ids?, + @JsonProperty("rating") val rating: Int, + @JsonProperty("rated_at") val rated_at: String? = getDateTime(unixTime) + ) : MediaObject(title, year, ids) + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + class StatusMediaObject( + @JsonProperty("title") title: String?, + @JsonProperty("year") year: Int?, + @JsonProperty("ids") ids: Ids?, + @JsonProperty("to") val to: String, + @JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime) + ) : MediaObject(title, year, ids) + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + class HistoryMediaObject( + @JsonProperty("title") title: String?, + @JsonProperty("year") year: Int?, + @JsonProperty("ids") ids: Ids?, + @JsonProperty("seasons") seasons: List?, + @JsonProperty("episodes") episodes: List?, + ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class StatusRequest( + @JsonProperty("movies") val movies: List, + @JsonProperty("shows") val shows: List + ) + + /** https://simkl.docs.apiary.io/#reference/sync/get-all-items/get-all-items-in-the-user's-watchlist */ + data class AllItemsResponse( + val shows: List, + val anime: List, + val movies: List, + ) { + companion object { + fun merge(first: AllItemsResponse?, second: AllItemsResponse?): AllItemsResponse { + + // Replace the first item with the same id, or add the new item + fun MutableList.replaceOrAddItem(newItem: T, predicate: (T) -> Boolean) { + for (i in this.indices) { + if (predicate(this[i])) { + this[i] = newItem + return + } + } + this.add(newItem) + } + + // + fun merge( + first: List?, + second: List? + ): List { + return (first?.toMutableList() ?: mutableListOf()).apply { + second?.forEach { secondShow -> + this.replaceOrAddItem(secondShow) { + it.getIds().simkl == secondShow.getIds().simkl + } + } + } + } + + return AllItemsResponse( + merge(first?.shows, second?.shows), + merge(first?.anime, second?.anime), + merge(first?.movies, second?.movies), + ) + } + } + + interface Metadata { + val last_watched_at: String? + val status: String? + val user_rating: Int? + val last_watched: String? + val watched_episodes_count: Int? + val total_episodes_count: Int? + + fun getIds(): ShowMetadata.Show.Ids + fun toLibraryItem(): SyncAPI.LibraryItem + } + + data class MovieMetadata( + override val last_watched_at: String?, + override val status: String, + override val user_rating: Int?, + override val last_watched: String?, + override val watched_episodes_count: Int?, + override val total_episodes_count: Int?, + val movie: ShowMetadata.Show + ) : Metadata { + override fun getIds(): ShowMetadata.Show.Ids { + return this.movie.ids + } + + override fun toLibraryItem(): SyncAPI.LibraryItem { + return SyncAPI.LibraryItem( + this.movie.title, + "https://simkl.com/tv/${movie.ids.simkl}", + movie.ids.simkl.toString(), + this.watched_episodes_count, + this.total_episodes_count, + this.user_rating?.times(10), + getUnixTime(last_watched_at) ?: 0, + "Simkl", + TvType.Movie, + this.movie.poster?.let { getPosterUrl(it) }, + null, + null, + movie.ids.simkl + ) + } + } + + data class ShowMetadata( + override val last_watched_at: String?, + override val status: String, + override val user_rating: Int?, + override val last_watched: String?, + override val watched_episodes_count: Int?, + override val total_episodes_count: Int?, + val show: Show + ) : Metadata { + override fun getIds(): Show.Ids { + return this.show.ids + } + + override fun toLibraryItem(): SyncAPI.LibraryItem { + return SyncAPI.LibraryItem( + this.show.title, + "https://simkl.com/tv/${show.ids.simkl}", + show.ids.simkl.toString(), + this.watched_episodes_count, + this.total_episodes_count, + this.user_rating?.times(10), + getUnixTime(last_watched_at) ?: 0, + "Simkl", + TvType.Anime, + this.show.poster?.let { getPosterUrl(it) }, + null, + null, + show.ids.simkl + ) + } + + data class Show( + val title: String, + val poster: String?, + val year: Int?, + val ids: Ids, + ) { + data class Ids( + val simkl: Int, + val slug: String?, + val imdb: String?, + val zap2it: String?, + val tmdb: String?, + val offen: String?, + val tvdb: String?, + val mal: String?, + val anidb: String?, + val anilist: String?, + val traktslug: String? + ) { + fun matchesId(database: SyncServices, id: String): Boolean { + return when (database) { + SyncServices.Simkl -> this.simkl == id.toIntOrNull() + SyncServices.AniList -> this.anilist == id + SyncServices.Mal -> this.mal == id + SyncServices.Tmdb -> this.tmdb == id + SyncServices.Imdb -> this.imdb == id + } + } + } + } + } + } + } + + /** + * Appends api keys to the requests + **/ + private inner class HeaderInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + debugPrint { "${this@SimklApi.name} made request to ${chain.request().url}" } + return chain.proceed( + chain.request() + .newBuilder() + .addHeader("Authorization", "Bearer $token") + .addHeader("simkl-api-key", clientId) + .build() + ) + } + } + + private suspend fun getUser(): SettingsResponse.User? { + return suspendSafeApiCall { + app.post("$mainUrl/users/settings", interceptor = interceptor) + .parsedSafe()?.user + } + } + + class SimklSyncStatus( + override var status: Int, + override var score: Int?, + override var watchedEpisodes: Int?, + val episodes: Array?, + override var isFavorite: Boolean? = null, + override var maxEpisodes: Int? = null, + ) : SyncAPI.AbstractSyncStatus() + + override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { + val realIds = readIdFromString(id) + val foundItem = getSyncListSmart()?.let { list -> + listOf(list.shows, list.anime, list.movies).flatten().firstOrNull { show -> + realIds.any { (database, id) -> + show.getIds().matchesId(database, id) + } + } + } + + // Search to get episodes + val searchResult = searchByIds(realIds)?.firstOrNull() + val episodes = getEpisodes(searchResult?.ids?.simkl, searchResult?.type) + + if (foundItem != null) { + return SimklSyncStatus( + status = foundItem.status?.let { SimklListStatusType.fromString(it)?.value } + ?: return null, + score = foundItem.user_rating, + watchedEpisodes = foundItem.watched_episodes_count, + maxEpisodes = foundItem.total_episodes_count, + episodes = episodes + ) + } else { + return if (searchResult != null) { + SimklSyncStatus( + status = SimklListStatusType.None.value, + score = 0, + watchedEpisodes = 0, + maxEpisodes = if (searchResult.type == "movie") 0 else null, + episodes = episodes + ) + } else { + null + } + } + } + + override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { + val parsedId = readIdFromString(id) + lastScoreTime = unixTime + + if (status.status == SimklListStatusType.None.value) { + return app.post( + "$mainUrl/sync/history/remove", + json = StatusRequest( + shows = listOf( + HistoryMediaObject( + null, + null, + MediaObject.Ids.fromMap(parsedId), + emptyList(), + emptyList() + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } + + val realScore = status.score + val ratingResponseSuccess = if (realScore != null) { + // Remove rating if score is 0 + val ratingsSuffix = if (realScore == 0) "/remove" else "" + debugPrint { "Rate ${this.name} item: rating=$realScore" } + app.post( + "$mainUrl/sync/ratings$ratingsSuffix", + json = StatusRequest( + // Not possible to know if TV or Movie + shows = listOf( + RatingMediaObject( + null, + null, + MediaObject.Ids.fromMap(parsedId), + realScore + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } else { + true + } + + val simklStatus = status as? SimklSyncStatus + // All episodes if marked as completed + val watchedEpisodes = if (status.status == SimklListStatusType.Completed.value) { + simklStatus?.episodes?.size + } else { + status.watchedEpisodes + } + + // Only post episodes if available episodes and the status is correct + val episodeResponseSuccess = + if (simklStatus != null && watchedEpisodes != null && !simklStatus.episodes.isNullOrEmpty() && listOf( + SimklListStatusType.Paused.value, + SimklListStatusType.Dropped.value, + SimklListStatusType.Watching.value, + SimklListStatusType.Completed.value, + SimklListStatusType.ReWatching.value + ).contains(status.status) + ) { + val cutEpisodes = simklStatus.episodes.take(watchedEpisodes) + + val (seasons, episodes) = if (cutEpisodes.any { it.season != null }) { + EpisodeMetadata.convertToSeasons(cutEpisodes) to null + } else { + null to EpisodeMetadata.convertToEpisodes(cutEpisodes) + } + + debugPrint { "Synced history for ${status.watchedEpisodes} given size of ${simklStatus.episodes.size}: seasons=${seasons?.toList()}, episodes=${episodes?.toList()}" } + val episodeResponse = app.post( + "$mainUrl/sync/history", + json = StatusRequest( + shows = listOf( + HistoryMediaObject( + null, + null, + MediaObject.Ids.fromMap(parsedId), + seasons, + episodes + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ) + episodeResponse.isSuccessful + } else true + + val newStatus = + SimklListStatusType.values().firstOrNull { it.value == status.status }?.originalName + ?: SimklListStatusType.Watching.originalName + + val statusResponseSuccess = if (newStatus != null) { + debugPrint { "Add to ${this.name} list: status=$newStatus" } + app.post( + "$mainUrl/sync/add-to-list", + json = StatusRequest( + shows = listOf( + StatusMediaObject( + null, + null, + MediaObject.Ids.fromMap(parsedId), + newStatus + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } else { + true + } + + debugPrint { "All scoring complete: rating=$ratingResponseSuccess, status=$statusResponseSuccess, episode=$episodeResponseSuccess" } + requireLibraryRefresh = true + return ratingResponseSuccess && statusResponseSuccess && episodeResponseSuccess + } + + + /** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ + suspend fun searchByIds(serviceMap: Map): Array? { + if (serviceMap.isEmpty()) return emptyArray() + + return app.get( + "$mainUrl/search/id", + params = mapOf("client_id" to clientId) + serviceMap.map { (service, id) -> + service.originalName to id + } + ).parsedSafe() + } + + suspend fun getEpisodes(simklId: Int?, type: String?): Array? { + if (simklId == null) return null + val url = when (type) { + "anime" -> "https://api.simkl.com/anime/episodes/$simklId" + "tv" -> "https://api.simkl.com/tv/episodes/$simklId" + "movie" -> return null + else -> return null + } + return app.get(url, params = mapOf("client_id" to clientId)).parsedSafe() + } + + override suspend fun search(name: String): List? { + return app.get( + "$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name) + ).parsedSafe>()?.mapNotNull { it.toSyncSearchResult() } + } + + override fun authenticate(activity: FragmentActivity?) { + lastLoginState = BigInteger(130, SecureRandom()).toString(32) + val url = + "https://simkl.com/oauth/authorize?response_type=code&client_id=$clientId&redirect_uri=$appString://${redirectUrl}&state=$lastLoginState" + openBrowser(url, activity) + } + + override fun loginInfo(): AuthAPI.LoginInfo? { + return getKey(accountId, SIMKL_USER_KEY)?.let { user -> + AuthAPI.LoginInfo( + name = user.name, + profilePicture = user.avatar, + accountIndex = accountIndex + ) + } + } + + override fun logOut() { + requireLibraryRefresh = true + removeAccountKeys() + } + + override suspend fun getResult(id: String): SyncAPI.SyncResult? { + return null + } + + private suspend fun getSyncListSince(since: Long?): AllItemsResponse { + val params = getDateTime(since)?.let { + mapOf("date_from" to it) + } ?: emptyMap() + + return app.get( + "$mainUrl/sync/all-items/", + params = params, + interceptor = interceptor + ).parsed() + } + + private suspend fun getActivities(): ActivitiesResponse? { + return app.post("$mainUrl/sync/activities", interceptor = interceptor).parsedSafe() + } + + private fun getSyncListCached(): AllItemsResponse? { + return getKey(accountId, SIMKL_CACHED_LIST) + } + + private suspend fun getSyncListSmart(): AllItemsResponse? { + if (token == null) return null + + val activities = getActivities() + val lastCacheUpdate = getKey(accountId, SIMKL_CACHED_LIST_TIME) + val lastRemoval = listOf( + activities?.tv_shows?.removed_from_list, + activities?.anime?.removed_from_list, + activities?.movies?.removed_from_list + ).maxOf { + getUnixTime(it) ?: -1 + } + val lastRealUpdate = + listOf( + activities?.tv_shows?.all, + activities?.anime?.all, + activities?.movies?.all, + ).maxOf { + getUnixTime(it) ?: -1 + } + + debugPrint { "Cache times: lastCacheUpdate=$lastCacheUpdate, lastRemoval=$lastRemoval, lastRealUpdate=$lastRealUpdate" } + val list = if (lastCacheUpdate == null || lastCacheUpdate < lastRemoval) { + debugPrint { "Full list update in ${this.name}." } + setKey(accountId, SIMKL_CACHED_LIST_TIME, lastRemoval) + getSyncListSince(null) + } else if (lastCacheUpdate < lastRealUpdate || lastCacheUpdate < lastScoreTime) { + debugPrint { "Partial list update in ${this.name}." } + setKey(accountId, SIMKL_CACHED_LIST_TIME, lastCacheUpdate) + AllItemsResponse.merge(getSyncListCached(), getSyncListSince(lastCacheUpdate)) + } else { + debugPrint { "Cached list update in ${this.name}." } + getSyncListCached() + } + debugPrint { "List sizes: movies=${list?.movies?.size}, shows=${list?.shows?.size}, anime=${list?.anime?.size}" } + + setKey(accountId, SIMKL_CACHED_LIST, list) + + return list + } + + + override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata? { + val list = getSyncListSmart() ?: return null + + val baseMap = + SimklListStatusType.values() + .filter { it.value >= 0 && it.value != SimklListStatusType.ReWatching.value } + .associate { + it.stringRes to emptyList() + } + + val syncMap = listOf(list.anime, list.movies, list.shows) + .flatten() + .groupBy { + it.status + } + .mapNotNull { (status, list) -> + val stringRes = + status?.let { SimklListStatusType.fromString(it)?.stringRes } + ?: return@mapNotNull null + val libraryList = list.map { it.toLibraryItem() } + stringRes to libraryList + }.toMap() + + return SyncAPI.LibraryMetadata( + (baseMap + syncMap).map { SyncAPI.LibraryList(txt(it.key), it.value) }, setOf( + ListSorting.AlphabeticalA, + ListSorting.AlphabeticalZ, + ListSorting.UpdatedNew, + ListSorting.UpdatedOld, + ListSorting.RatingHigh, + ListSorting.RatingLow, + ) + ) + } + + override fun getIdFromUrl(url: String): String { + val simklUrlRegex = Regex("""https://simkl\.com/[^/]*/(\d+).*""") + return simklUrlRegex.find(url)?.groupValues?.get(1) ?: "" + } + + override suspend fun handleRedirect(url: String): Boolean { + val uri = url.toUri() + val state = uri.getQueryParameter("state") + // Ensure consistent state + if (state != lastLoginState) return false + lastLoginState = "" + + val code = uri.getQueryParameter("code") ?: return false + val token = app.post( + "$mainUrl/oauth/token", json = TokenRequest(code) + ).parsedSafe() ?: return false + + switchToNewAccount() + setKey(accountId, SIMKL_TOKEN_KEY, token.access_token) + + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt index 91415d26..a3e2ed87 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt @@ -36,18 +36,18 @@ class SyncViewModel : ViewModel() { val metadata: LiveData> get() = _metaResponse - private val _userDataResponse: MutableLiveData?> = + private val _userDataResponse: MutableLiveData?> = MutableLiveData(null) - val userData: LiveData?> get() = _userDataResponse + val userData: LiveData?> get() = _userDataResponse // prefix, id - private var syncs = mutableMapOf() + private val syncs = mutableMapOf() //private val _syncIds: MutableLiveData> = // MutableLiveData(mutableMapOf()) //val syncIds: LiveData> get() = _syncIds - fun getSyncs() : Map { + fun getSyncs(): Map { return syncs } @@ -106,7 +106,7 @@ class SyncViewModel : ViewModel() { Log.i(TAG, "addFromUrl = $url") if (url == null || hasAddedFromUrl.contains(url)) return@ioSafe - if(!url.startsWith("http")) return@ioSafe + if (!url.startsWith("http")) return@ioSafe SyncUtil.getIdsFromUrl(url)?.let { (malId, aniListId) -> hasAddedFromUrl.add(url) @@ -150,7 +150,8 @@ class SyncViewModel : ViewModel() { val user = userData.value if (user is Resource.Success) { - _userDataResponse.postValue(Resource.Success(user.value.copy(watchedEpisodes = episodes))) + user.value.watchedEpisodes = episodes + _userDataResponse.postValue(Resource.Success(user.value)) } } @@ -158,7 +159,8 @@ class SyncViewModel : ViewModel() { Log.i(TAG, "setScore = $score") val user = userData.value if (user is Resource.Success) { - _userDataResponse.postValue(Resource.Success(user.value.copy(score = score))) + user.value.score = score + _userDataResponse.postValue(Resource.Success(user.value)) } } @@ -167,7 +169,8 @@ class SyncViewModel : ViewModel() { if (which < -1 || which > 5) return // validate input val user = userData.value if (user is Resource.Success) { - _userDataResponse.postValue(Resource.Success(user.value.copy(status = which))) + user.value.status = which + _userDataResponse.postValue(Resource.Success(user.value)) } } @@ -185,17 +188,16 @@ class SyncViewModel : ViewModel() { fun modifyMaxEpisode(episodeNum: Int) { Log.i(TAG, "modifyMaxEpisode = $episodeNum") modifyData { status -> - status.copy( - watchedEpisodes = maxOf( - episodeNum, - status.watchedEpisodes ?: return@modifyData null - ) + status.watchedEpisodes = maxOf( + episodeNum, + status.watchedEpisodes ?: return@modifyData null ) + status } } /// modifies the current sync data, return null if you don't want to change it - private fun modifyData(update: ((SyncAPI.SyncStatus) -> (SyncAPI.SyncStatus?))) = + private fun modifyData(update: ((SyncAPI.AbstractSyncStatus) -> (SyncAPI.AbstractSyncStatus?))) = ioSafe { syncs.amap { (prefix, id) -> repos.firstOrNull { it.idPrefix == prefix }?.let { repo -> @@ -245,8 +247,12 @@ class SyncViewModel : ViewModel() { // shitty way to sort anilist first, as it has trailers while mal does not if (syncs.containsKey(aniListApi.idPrefix)) { try { // swap can throw error - Collections.swap(current, current.indexOfFirst { it.first == aniListApi.idPrefix }, 0) - } catch (t : Throwable) { + Collections.swap( + current, + current.indexOfFirst { it.first == aniListApi.idPrefix }, + 0 + ) + } catch (t: Throwable) { logError(t) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 33316020..b3225d5c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API @@ -257,6 +258,7 @@ class SettingsAccount : PreferenceFragmentCompat() { listOf( R.string.mal_key to malApi, R.string.anilist_key to aniListApi, + R.string.simkl_key to simklApi, R.string.opensubtitles_key to openSubtitlesApi, ) diff --git a/app/src/main/res/drawable/simkl_logo.xml b/app/src/main/res/drawable/simkl_logo.xml new file mode 100644 index 00000000..eb29fb5b --- /dev/null +++ b/app/src/main/res/drawable/simkl_logo.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ded7366b..13251c7c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -449,6 +449,7 @@ Put the title under the poster anilist_key + simkl_key mal_key opensubtitles_key nginx_key diff --git a/app/src/main/res/xml/settings_account.xml b/app/src/main/res/xml/settings_account.xml index d4bae8c4..d3dbcb31 100644 --- a/app/src/main/res/xml/settings_account.xml +++ b/app/src/main/res/xml/settings_account.xml @@ -1,27 +1,32 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> + android:icon="@drawable/mal_logo" + android:key="@string/mal_key" /> - - - - + android:icon="@drawable/ic_anilist_icon" + android:key="@string/anilist_key" /> - - - - - - - - + + + + + + + + + + + + + + + \ No newline at end of file From 3ab9e11350aab59c4ada14ec33e6f72fa324f2e9 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 12 Aug 2023 23:41:53 +0200 Subject: [PATCH 077/156] fixed SimklApi subscription --- .../cloudstream3/syncproviders/providers/SimklApi.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 64afb0e2..64cebfc6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -316,9 +316,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { /** https://simkl.docs.apiary.io/#reference/sync/get-all-items/get-all-items-in-the-user's-watchlist */ data class AllItemsResponse( - val shows: List, - val anime: List, - val movies: List, + @JsonProperty("shows") + val shows: List = emptyList(), + @JsonProperty("anime") + val anime: List = emptyList(), + @JsonProperty("movies") + val movies: List = emptyList(), ) { companion object { fun merge(first: AllItemsResponse?, second: AllItemsResponse?): AllItemsResponse { From 0eb241e6cb4a6d2f5bff7728f7ea0ac7b0a0e904 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 12 Aug 2023 23:54:37 +0200 Subject: [PATCH 078/156] fixed fab expand --- .../com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index 3ddaee61..633ee762 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -905,6 +905,7 @@ open class ResultFragmentPhone : FullScreenPlayer(), observe(viewModel.watchStatus) { watchType -> binding?.resultBookmarkFab?.apply { + setText(watchType.stringRes) if (watchType == WatchType.NONE) { context?.colorFromAttribute(R.attr.white) } else { From 74867bed1cc96b3a494fb107a6ff9c251579b525 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Sun, 13 Aug 2023 21:07:36 +0530 Subject: [PATCH 079/156] Update SpeedoStream.kt (#552) Fixes YoMovies Provider. --- .../java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt index 90104ace..3f6fff2f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt @@ -13,7 +13,7 @@ class SpeedoStream1 : SpeedoStream() { open class SpeedoStream : ExtractorApi() { override val name = "SpeedoStream" - override val mainUrl = "https://speedostream.com" + override val mainUrl = "https://speedostream.mom" override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?): List { From 4d98690adbc8a903b3545e094d5b9ca7099e408a Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Tue, 15 Aug 2023 02:05:07 +0200 Subject: [PATCH 080/156] small fix to home load --- .../cloudstream3/plugins/PluginManager.kt | 4 + .../cloudstream3/ui/home/HomeViewModel.kt | 14 ++- .../settings/extensions/ExtensionsFragment.kt | 107 +++++++++++------- .../main/res/layout/fragment_extensions.xml | 1 + 4 files changed, 81 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index 4c32088a..87b0ba3b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -165,6 +165,9 @@ object PluginManager { var loadedLocalPlugins = false private set + + var loadedOnlinePlugins = false + private set private val gson = Gson() private suspend fun maybeLoadPlugin(context: Context, file: File) { @@ -278,6 +281,7 @@ object PluginManager { } // ioSafe { + loadedOnlinePlugins = true afterPluginsLoadedEvent.invoke(false) // } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index b1ced59e..e8cf8863 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -103,7 +103,7 @@ class HomeViewModel : ViewModel() { private var currentShuffledList: List = listOf() private fun autoloadRepo(): APIRepository { - return APIRepository(synchronized(apis) { apis.first { it.hasMainPage }}) + return APIRepository(synchronized(apis) { apis.first { it.hasMainPage } }) } private val _availableWatchStatusTypes = @@ -185,8 +185,9 @@ class HomeViewModel : ViewModel() { } private var onGoingLoad: Job? = null - private var isCurrentlyLoadingName : String? = null + private var isCurrentlyLoadingName: String? = null private fun loadAndCancel(api: MainAPI) { + //println("loaded ${api.name}") onGoingLoad?.cancel() isCurrentlyLoadingName = api.name onGoingLoad = load(api) @@ -290,7 +291,7 @@ class HomeViewModel : ViewModel() { } } - private fun load(api: MainAPI) : Job = ioSafe { + private fun load(api: MainAPI): Job = ioSafe { repo = //if (api != null) { APIRepository(api) //} else { @@ -455,9 +456,9 @@ class HomeViewModel : ViewModel() { fromUI: Boolean = false ) = ioSafe { + //println("trying to load $preferredApiName") // Since plugins are loaded in stages this function can get called multiple times. // The issue with this is that the homepage may be fetched multiple times while the first request is loading - val api = getApiFromNameNull(preferredApiName) // api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true val currentPage = page.value @@ -467,6 +468,7 @@ class HomeViewModel : ViewModel() { return@ioSafe } + val api = getApiFromNameNull(preferredApiName) if (preferredApiName == noneApi.name) { // just set to random if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) @@ -485,10 +487,12 @@ class HomeViewModel : ViewModel() { } else if (api == null) { // API is not found aka not loaded or removed, post the loading // progress if waiting for plugins, otherwise nothing - if(PluginManager.loadedLocalPlugins || PluginManager.checkSafeModeFile() || lastError != null) { + if (PluginManager.loadedOnlinePlugins || PluginManager.checkSafeModeFile() || lastError != null) { loadAndCancel(noneApi) } else { _page.postValue(Resource.Loading()) + if (preferredApiName != null) + _apiName.postValue(preferredApiName) } } else { // if the api is found, then set it to it and save key diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 8bc947c5..553e7675 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui.settings.extensions import android.content.ClipboardManager import android.content.Context import android.content.DialogInterface +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -12,6 +13,8 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.core.view.marginBottom +import androidx.core.view.marginTop import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController @@ -33,7 +36,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import com.lagradost.cloudstream3.widget.LinearRecycleViewLayoutManager class ExtensionsFragment : Fragment() { var binding: FragmentExtensionsBinding? = null @@ -84,51 +86,76 @@ class ExtensionsFragment : Fragment() { setUpToolbar(R.string.extensions) - binding?.repoRecyclerView?.setLinearListLayout( - isHorizontal = false, - nextUp = R.id.settings_toolbar, //FOCUS_SELF, // back has no id so we cant :pensive: - nextDown = R.id.plugin_storage_appbar, - nextRight = FOCUS_SELF, - nextLeft = R.id.nav_rail_view - ) - binding?.repoRecyclerView?.adapter = RepoAdapter(false, { - findNavController().navigate( - R.id.navigation_settings_extensions_to_navigation_settings_plugins, - PluginsFragment.newInstance( - it.name, - it.url, - false - ) + binding?.repoRecyclerView?.apply { + setLinearListLayout( + isHorizontal = false, + nextUp = R.id.settings_toolbar, //FOCUS_SELF, // back has no id so we cant :pensive: + nextDown = R.id.plugin_storage_appbar, + nextRight = FOCUS_SELF, + nextLeft = R.id.nav_rail_view ) - }, { repo -> - // Prompt user before deleting repo - main { - val builder = AlertDialog.Builder(context ?: view.context) - val dialogClickListener = - DialogInterface.OnClickListener { _, which -> - when (which) { - DialogInterface.BUTTON_POSITIVE -> { - ioSafe { - RepositoryManager.removeRepository(view.context, repo) - extensionViewModel.loadStats() - extensionViewModel.loadRepositories() - } - } - DialogInterface.BUTTON_NEGATIVE -> {} - } + if (!isTrueTvSettings()) + binding?.addRepoButton?.let { button -> + button.post { + setPadding( + paddingLeft, + paddingTop, + paddingRight, + button.measuredHeight + button.marginTop + button.marginBottom + ) } + } - builder.setTitle(R.string.delete_repository) - .setMessage( - context?.getString(R.string.delete_repository_plugins) - ) - .setPositiveButton(R.string.delete, dialogClickListener) - .setNegativeButton(R.string.cancel, dialogClickListener) - .show().setDefaultFocus() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> + val dy = scrollY - oldScrollY + if (dy > 0) { //check for scroll down + binding?.addRepoButton?.shrink() // hide + } else if (dy < -5) { + binding?.addRepoButton?.extend() // show + } + } } - }) + adapter = RepoAdapter(false, { + findNavController().navigate( + R.id.navigation_settings_extensions_to_navigation_settings_plugins, + PluginsFragment.newInstance( + it.name, + it.url, + false + ) + ) + }, { repo -> + // Prompt user before deleting repo + main { + val builder = AlertDialog.Builder(context ?: view.context) + val dialogClickListener = + DialogInterface.OnClickListener { _, which -> + when (which) { + DialogInterface.BUTTON_POSITIVE -> { + ioSafe { + RepositoryManager.removeRepository(view.context, repo) + extensionViewModel.loadStats() + extensionViewModel.loadRepositories() + } + } + + DialogInterface.BUTTON_NEGATIVE -> {} + } + } + + builder.setTitle(R.string.delete_repository) + .setMessage( + context?.getString(R.string.delete_repository_plugins) + ) + .setPositiveButton(R.string.delete, dialogClickListener) + .setNegativeButton(R.string.cancel, dialogClickListener) + .show().setDefaultFocus() + } + }) + } observe(extensionViewModel.repositories) { binding?.repoRecyclerView?.isVisible = it.isNotEmpty() diff --git a/app/src/main/res/layout/fragment_extensions.xml b/app/src/main/res/layout/fragment_extensions.xml index b3583539..71dd372b 100644 --- a/app/src/main/res/layout/fragment_extensions.xml +++ b/app/src/main/res/layout/fragment_extensions.xml @@ -11,6 +11,7 @@ Date: Tue, 15 Aug 2023 18:37:33 +0000 Subject: [PATCH 081/156] Fix episode removal in simkl (#555) --- .../syncproviders/providers/SimklApi.kt | 75 ++++++++++++------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 64cebfc6..b4a9d789 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -60,8 +60,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { private var lastScoreTime = -1L companion object { - private const val clientId = BuildConfig.SIMKL_CLIENT_ID - private const val clientSecret = BuildConfig.SIMKL_CLIENT_SECRET + private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID + private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET private var lastLoginState = "" const val SIMKL_TOKEN_KEY: String = "simkl_token" @@ -498,6 +498,9 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { val episodes: Array?, override var isFavorite: Boolean? = null, override var maxEpisodes: Int? = null, + /** Save seen episodes separately to know the change from old to new. + * Required to remove seen episodes if count decreases */ + val oldEpisodes: Int, ) : SyncAPI.AbstractSyncStatus() override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { @@ -521,7 +524,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { score = foundItem.user_rating, watchedEpisodes = foundItem.watched_episodes_count, maxEpisodes = foundItem.total_episodes_count, - episodes = episodes + episodes = episodes, + oldEpisodes = foundItem.watched_episodes_count ?: 0, ) } else { return if (searchResult != null) { @@ -530,7 +534,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { score = 0, watchedEpisodes = 0, maxEpisodes = if (searchResult.type == "movie") 0 else null, - episodes = episodes + episodes = episodes, + oldEpisodes = 0, ) } else { null @@ -604,32 +609,46 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { SimklListStatusType.ReWatching.value ).contains(status.status) ) { - val cutEpisodes = simklStatus.episodes.take(watchedEpisodes) - - val (seasons, episodes) = if (cutEpisodes.any { it.season != null }) { - EpisodeMetadata.convertToSeasons(cutEpisodes) to null - } else { - null to EpisodeMetadata.convertToEpisodes(cutEpisodes) + suspend fun postEpisodes( + url: String, + rawEpisodes: List + ): Boolean { + val (seasons, episodes) = if (rawEpisodes.any { it.season != null }) { + EpisodeMetadata.convertToSeasons(rawEpisodes) to null + } else { + null to EpisodeMetadata.convertToEpisodes(rawEpisodes) + } + debugPrint { "Synced history using $url: seasons=${seasons?.toList()}, episodes=${episodes?.toList()}" } + return app.post( + url, + json = StatusRequest( + shows = listOf( + HistoryMediaObject( + null, + null, + MediaObject.Ids.fromMap(parsedId), + seasons, + episodes + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful } - debugPrint { "Synced history for ${status.watchedEpisodes} given size of ${simklStatus.episodes.size}: seasons=${seasons?.toList()}, episodes=${episodes?.toList()}" } - val episodeResponse = app.post( - "$mainUrl/sync/history", - json = StatusRequest( - shows = listOf( - HistoryMediaObject( - null, - null, - MediaObject.Ids.fromMap(parsedId), - seasons, - episodes - ) - ), - movies = emptyList() - ), - interceptor = interceptor - ) - episodeResponse.isSuccessful + // If episodes decrease: remove all episodes beyond watched episodes. + val removeResponse = if (simklStatus.oldEpisodes > watchedEpisodes) { + val removeEpisodes = simklStatus.episodes + .drop(watchedEpisodes) + postEpisodes("$mainUrl/sync/history/remove", removeEpisodes) + } else { + true + } + val cutEpisodes = simklStatus.episodes.take(watchedEpisodes) + val addResponse = postEpisodes("$mainUrl/sync/history/", cutEpisodes) + + removeResponse && addResponse } else true val newStatus = From d536dffaf559425bdb7b02232c3bfac8a951133d Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Wed, 16 Aug 2023 19:45:39 +0530 Subject: [PATCH 082/156] Fix Trailers not Working (#559) * Fix Trailers not Working * smol tip --- app/build.gradle.kts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3c12652a..b228fea0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -165,7 +165,7 @@ dependencies { androidTestImplementation("androidx.test:core") //implementation("io.karn:khttp-android:0.1.2") //okhttp instead -// implementation("org.jsoup:jsoup:1.13.1") + // implementation("org.jsoup:jsoup:1.13.1") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") implementation("androidx.preference:preference-ktx:1.2.0") @@ -220,8 +220,8 @@ dependencies { implementation("androidx.work:work-runtime-ktx:2.8.1") // Networking -// implementation("com.squareup.okhttp3:okhttp:4.9.2") -// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") + // implementation("com.squareup.okhttp3:okhttp:4.9.2") + // implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") implementation("com.github.Blatzar:NiceHttp:0.4.3") // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") @@ -243,11 +243,9 @@ dependencies { // used for subtitle decoding https://github.com/albfernandez/juniversalchardet implementation("com.github.albfernandez:juniversalchardet:2.4.0") - // slow af yt - //implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT") - // newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204 - implementation("com.github.TeamNewPipe:NewPipeExtractor:8495ad619e") + // this should be updated frequently to avoid trailer fu*kery + implementation("com.github.TeamNewPipe:NewPipeExtractor:1f08d28") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6") // Library/extensions searching with Levenshtein distance From d247640dcf2a3985390cb0e3d5fd4c8d82e7d4ad Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Wed, 16 Aug 2023 19:48:15 +0530 Subject: [PATCH 083/156] Play n Dowload button fix for NS*W results. (#557) * Play n Dowload button fix for NS*W results. * Revert MainAPI Changes * Tweaked ResultViewModel --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 011d133d..2fe3b012 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -1707,7 +1707,7 @@ class ResultViewModel2 : ViewModel() { else -> { if (response.type.isLiveStream()) R.string.play_livestream_button - else if (response.type.isMovieType()) // this wont break compatibility as you only need to override isMovieType + else if (response.isMovie()) // this wont break compatibility as you only need to override isMovieType R.string.play_movie_button else null } @@ -2340,4 +2340,4 @@ class ResultViewModel2 : ViewModel() { } } } -} \ No newline at end of file +} From 20da3807a2cb98b60b22e4ad65a96c037c254c58 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 17 Aug 2023 01:00:43 +0200 Subject: [PATCH 084/156] fixed search query for intent --- .../com/lagradost/cloudstream3/MainActivity.kt | 12 ++++++++---- .../cloudstream3/ui/search/SearchFragment.kt | 15 +++++++++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a8160d33..15b16078 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -286,7 +286,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { * * This is a very bad solution but I was unable to find a better one. **/ - private var nextSearchQuery: String? = null + var nextSearchQuery: String? = null /** * Fires every time a new batch of plugins have been loaded, no guarantee about how often this is run and on which thread @@ -362,9 +362,14 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { loadRepository(url) return true } else if (safeURI(str)?.scheme == appStringSearch) { + val query = str.substringAfter("$appStringSearch://") nextSearchQuery = - URLDecoder.decode(str.substringAfter("$appStringSearch://"), "UTF-8") - + try { + URLDecoder.decode(query, "UTF-8") + } catch (t : Throwable) { + logError(t) + query + } // Use both navigation views to support both layouts. // It might be better to use the QuickSearch. activity?.findViewById(R.id.nav_view)?.selectedItemId = @@ -1315,7 +1320,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (navDestination.matchDestination(R.id.navigation_search) && !nextSearchQuery.isNullOrBlank()) { bundle?.apply { this.putString(SearchFragment.SEARCH_QUERY, nextSearchQuery) - nextSearchQuery = null } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index 63213eb9..bdf82377 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -84,7 +84,7 @@ class SearchFragment : Fragment() { fun newInstance(query: String): Bundle { return Bundle().apply { - putString(SEARCH_QUERY, query) + if(query.isNotBlank()) putString(SEARCH_QUERY, query) } } } @@ -211,7 +211,7 @@ class SearchFragment : Fragment() { reloadRepos() binding?.apply { - val adapter: RecyclerView.Adapter? = + val adapter: RecyclerView.Adapter = SearchAdapter( ArrayList(), searchAutofitResults, @@ -530,11 +530,18 @@ class SearchFragment : Fragment() { searchMasterRecycler.layoutManager = GridLayoutManager(context, 1) // Automatically search the specified query, this allows the app search to launch from intent - arguments?.getString(SEARCH_QUERY)?.let { query -> + var sq = arguments?.getString(SEARCH_QUERY) ?: savedInstanceState?.getString(SEARCH_QUERY) + if(sq.isNullOrBlank()) { + sq = MainActivity.nextSearchQuery + } + + sq?.let { query -> if (query.isBlank()) return@let mainSearch.setQuery(query, true) // Clear the query as to not make it request the same query every time the page is opened - arguments?.putString(SEARCH_QUERY, null) + arguments?.remove(SEARCH_QUERY) + savedInstanceState?.remove(SEARCH_QUERY) + MainActivity.nextSearchQuery = null } } From c2b951a07866077e05d15e385111d2d74fe7e542 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 17 Aug 2023 01:19:24 +0200 Subject: [PATCH 085/156] fixed #560 lock locks orientation --- .../ui/player/FullScreenPlayer.kt | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index 9739b627..0f3c189d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -2,9 +2,11 @@ package com.lagradost.cloudstream3.ui.player import android.animation.ObjectAnimator import android.annotation.SuppressLint +import android.app.Activity import android.content.Context import android.content.pm.ActivityInfo import android.content.res.ColorStateList +import android.content.res.Configuration import android.content.res.Resources import android.graphics.Color import android.media.AudioManager @@ -16,6 +18,7 @@ import android.util.DisplayMetrics import android.view.KeyEvent import android.view.LayoutInflater import android.view.MotionEvent +import android.view.Surface import android.view.View import android.view.ViewGroup import android.view.WindowManager @@ -56,6 +59,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.Vector2 import kotlin.math.* + const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking const val MINIMUM_VERTICAL_SWIPE = 2.0f // in percentage const val MINIMUM_HORIZONTAL_SWIPE = 2.0f // in percentage @@ -292,6 +296,36 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.getCurrentPreferredSubtitle() == null } + open fun lockOrientation(activity: Activity) { + val display = + (activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay + val rotation = display.rotation + val currentOrientation = activity.resources.configuration.orientation + var orientation = 0 + when (currentOrientation) { + Configuration.ORIENTATION_LANDSCAPE -> orientation = + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE + + Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED, Configuration.ORIENTATION_PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + //Configuration.ORIENTATION_PORTRAIT -> orientation = + // if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT + } + activity.requestedOrientation = orientation + } + + private fun updateOrientation() { + activity?.apply { + if(lockRotation) { + if(isLocked) { + lockOrientation(this) + } + else { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + } + } + } + } + protected fun enterFullscreen() { if (isFullScreenPlayer) { activity?.hideSystemUI() @@ -301,8 +335,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { activity?.window?.attributes = params } } - if (lockRotation) - activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + updateOrientation() } protected fun exitFullscreen() { @@ -561,6 +594,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } isLocked = !isLocked + updateOrientation() + if (isLocked && isShowing) { playerBinding?.playerHolder?.postDelayed({ if (isLocked && isShowing) { From 590c74111cb945366ebd6a11278f2f9f827043c5 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 17 Aug 2023 23:10:21 +0200 Subject: [PATCH 086/156] fuck it we ball, m3u8 download is now fixed --- .../lagradost/cloudstream3/extractors/Cda.kt | 10 +- .../cloudstream3/utils/M3u8Helper.kt | 282 +++++++++--------- .../utils/VideoDownloadManager.kt | 221 ++++++-------- 3 files changed, 237 insertions(+), 276 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt index 6a2f399d..42f6eddb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt @@ -1,13 +1,11 @@ package com.lagradost.cloudstream3.extractors -import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.USER_AGENT +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 -import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson -import android.util.Log +import com.lagradost.cloudstream3.utils.Qualities import java.net.URLDecoder open class Cda: ExtractorApi() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 6c5117b4..6770e303 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -1,17 +1,16 @@ package com.lagradost.cloudstream3.utils +import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.mvvm.logError -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.delay import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec import kotlin.math.pow - +/** backwards api surface */ class M3u8Helper { companion object { - private val generator = M3u8Helper() suspend fun generateM3u8( source: String, streamUrl: String, @@ -20,34 +19,59 @@ class M3u8Helper { headers: Map = mapOf(), name: String = source ): List { - return generator.m3u8Generation( - M3u8Stream( - streamUrl = streamUrl, - quality = quality, - headers = headers, - ), null - ) - .map { stream -> - ExtractorLink( - source, - name = name, - stream.streamUrl, - referer, - stream.quality ?: Qualities.Unknown.value, - true, - stream.headers, - ) - } + return M3u8Helper2.generateM3u8(source, streamUrl, referer, quality, headers, name) } } + + data class M3u8Stream( + val streamUrl: String, + val quality: Int? = null, + val headers: Map = mapOf() + ) + + suspend fun m3u8Generation(m3u8: M3u8Stream, returnThis: Boolean? = true): List { + return M3u8Helper2.m3u8Generation(m3u8, returnThis) + } +} + +object M3u8Helper2 { + suspend fun generateM3u8( + source: String, + streamUrl: String, + referer: String, + quality: Int? = null, + headers: Map = mapOf(), + name: String = source + ): List { + return m3u8Generation( + M3u8Helper.M3u8Stream( + streamUrl = streamUrl, + quality = quality, + headers = headers, + ), null + ) + .map { stream -> + ExtractorLink( + source, + name = name, + stream.streamUrl, + referer, + stream.quality ?: Qualities.Unknown.value, + true, + stream.headers, + ) + } + } + private val ENCRYPTION_DETECTION_REGEX = Regex("#EXT-X-KEY:METHOD=([^,]+),") private val ENCRYPTION_URL_IV_REGEX = Regex("#EXT-X-KEY:METHOD=([^,]+),URI=\"([^\"]+)\"(?:,IV=(.*))?") private val QUALITY_REGEX = Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""") private val TS_EXTENSION_REGEX = - Regex("""(.*\.ts.*|.*\.jpg.*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts + Regex("""#EXTINF:.*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways + //Regex("""(.*\.(ts|jpg|html).*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts private fun absoluteExtensionDetermination(url: String): String? { val split = url.split("/") @@ -73,7 +97,7 @@ class M3u8Helper { } }.iterator() - private fun getDecrypter( + fun getDecrypter( secretKey: ByteArray, data: ByteArray, iv: ByteArray = "".toByteArray() @@ -91,13 +115,8 @@ class M3u8Helper { return st != null && (st.value.isNotEmpty() || st.destructured.component1() != "NONE") } - data class M3u8Stream( - val streamUrl: String, - val quality: Int? = null, - val headers: Map = mapOf() - ) - private fun selectBest(qualities: List): M3u8Stream? { + private fun selectBest(qualities: List): M3u8Helper.M3u8Stream? { val result = qualities.sortedBy { if (it.quality != null && it.quality <= 1080) it.quality else 0 }.filter { @@ -113,19 +132,16 @@ class M3u8Helper { } private fun isNotCompleteUrl(url: String): Boolean { - return !url.contains("https://") && !url.contains("http://") + return !url.startsWith("https://") && !url.startsWith("http://") } - suspend fun m3u8Generation(m3u8: M3u8Stream, returnThis: Boolean? = true): List { -// return listOf(m3u8) - val list = mutableListOf() + suspend fun m3u8Generation(m3u8: M3u8Helper.M3u8Stream, returnThis: Boolean? = true): List { + val list = mutableListOf() val m3u8Parent = getParentLink(m3u8.streamUrl) val response = app.get(m3u8.streamUrl, headers = m3u8.headers, verify = false).text -// var hasAnyContent = false for (match in QUALITY_REGEX.findAll(response)) { -// hasAnyContent = true var (quality, m3u8Link, m3u8Link2) = match.destructured if (m3u8Link.isEmpty()) m3u8Link = m3u8Link2 if (absoluteExtensionDetermination(m3u8Link) == "m3u8") { @@ -136,21 +152,21 @@ class M3u8Helper { println(m3u8.streamUrl) } list += m3u8Generation( - M3u8Stream( + M3u8Helper.M3u8Stream( m3u8Link, quality.toIntOrNull(), m3u8.headers ), false ) } - list += M3u8Stream( + list += M3u8Helper.M3u8Stream( m3u8Link, quality.toIntOrNull(), m3u8.headers ) } if (returnThis != false) { - list += M3u8Stream( + list += M3u8Helper.M3u8Stream( m3u8.streamUrl, Qualities.Unknown.value, m3u8.headers @@ -160,113 +176,111 @@ class M3u8Helper { return list } + data class LazyHlsDownloadData( + private val encryptionData: ByteArray, + private val encryptionIv: ByteArray, + private val isEncrypted: Boolean, + private val allTsLinks: List, + private val relativeUrl: String, + private val headers: Map, + ) { + val size get() = allTsLinks.size - data class HlsDownloadData( - val bytes: ByteArray, - val currentIndex: Int, - val totalTs: Int, - val errored: Boolean = false - ) - - suspend fun hlsYield( - qualities: List, - startIndex: Int = 0 - ): Iterator { - if (qualities.isEmpty()) return listOf( - HlsDownloadData( - byteArrayOf(), - 1, - 1, - true - ) - ).iterator() - - var selected = selectBest(qualities) - if (selected == null) { - selected = qualities[0] + suspend fun resolveLinkSafe( + index: Int, + tries: Int = 3, + failDelay: Long = 3000 + ): ByteArray? { + for (i in 0 until tries) { + try { + return resolveLink(index) + } catch (e: IllegalArgumentException) { + return null + } catch (t: Throwable) { + delay(failDelay) + } + } + return null } + + @Throws + suspend fun resolveLink(index: Int): ByteArray { + if (index < 0 || index >= size) throw IllegalArgumentException("index must be in the bounds of the ts") + val url = allTsLinks[index] + + val tsResponse = app.get(url, headers = headers, verify = false) + val tsData = tsResponse.body.bytes() + if (tsData.isEmpty()) throw ErrorLoadingException("no data") + + return if (isEncrypted) { + getDecrypter(encryptionData, tsData, encryptionIv) + } else { + tsData + } + } + } + + @Throws + suspend fun hslLazy( + qualities: List + ): LazyHlsDownloadData { + if (qualities.isEmpty()) throw IllegalArgumentException("qualities must be non empty") + val selected = selectBest(qualities) ?: qualities.first() val headers = selected.headers - val streams = qualities.map { m3u8Generation(it, false) }.flatten() - //val sslVerification = if (headers.containsKey("ssl_verification")) headers["ssl_verification"].toBoolean() else true - + // this selects the best quality of the qualities offered, + // due to the recursive nature of m3u8, we only go 2 depth val secondSelection = selectBest(streams.ifEmpty { listOf(selected) }) - if (secondSelection != null) { - val m3u8Response = - runBlocking { - app.get( - secondSelection.streamUrl, - headers = headers, - verify = false - ).text - } + ?: throw IllegalArgumentException("qualities has no streams") - var encryptionUri: String? - var encryptionIv = byteArrayOf() - var encryptionData = byteArrayOf() + val m3u8Response = + app.get( + secondSelection.streamUrl, + headers = headers, + verify = false + ).text - val encryptionState = isEncrypted(m3u8Response) + println("m3u8Response=$m3u8Response") - if (encryptionState) { - val match = - ENCRYPTION_URL_IV_REGEX.find(m3u8Response)!!.destructured // its safe to assume that its not going to be null - encryptionUri = match.component2() + // encryption, this is because crunchy uses it + var encryptionIv = byteArrayOf() + var encryptionData = byteArrayOf() - if (isNotCompleteUrl(encryptionUri)) { - encryptionUri = "${getParentLink(secondSelection.streamUrl)}/$encryptionUri" - } + val encryptionState = isEncrypted(m3u8Response) - encryptionIv = match.component3().toByteArray() - val encryptionKeyResponse = - runBlocking { app.get(encryptionUri, headers = headers, verify = false) } - encryptionData = encryptionKeyResponse.body?.bytes() ?: byteArrayOf() + if (encryptionState) { + // its safe to assume that its not going to be null + val match = + ENCRYPTION_URL_IV_REGEX.find(m3u8Response)!!.groupValues + + var encryptionUri = match[1] + + if (isNotCompleteUrl(encryptionUri)) { + encryptionUri = "${getParentLink(secondSelection.streamUrl)}/$encryptionUri" } - val allTs = TS_EXTENSION_REGEX.findAll(m3u8Response) - val allTsList = allTs.toList() - val totalTs = allTsList.size - if (totalTs == 0) { - return listOf(HlsDownloadData(byteArrayOf(), 1, 1, true)).iterator() - } - var lastYield = 0 - - val relativeUrl = getParentLink(secondSelection.streamUrl) - var retries = 0 - val tsByteGen = sequence { - loop@ for ((index, ts) in allTs.withIndex()) { - val url = if ( - isNotCompleteUrl(ts.destructured.component1()) - ) "$relativeUrl/${ts.destructured.component1()}" else ts.destructured.component1() - val c = index + 1 + startIndex - - while (lastYield != c) { - try { - val tsResponse = - runBlocking { app.get(url, headers = headers, verify = false) } - var tsData = tsResponse.body?.bytes() ?: byteArrayOf() - - if (encryptionState) { - tsData = getDecrypter(encryptionData, tsData, encryptionIv) - yield(HlsDownloadData(tsData, c, totalTs)) - lastYield = c - break - } - yield(HlsDownloadData(tsData, c, totalTs)) - lastYield = c - } catch (e: Exception) { - logError(e) - if (retries == 3) { - yield(HlsDownloadData(byteArrayOf(), c, totalTs, true)) - break@loop - } - ++retries - Thread.sleep(2_000) - } - } - } - } - return tsByteGen.iterator() + encryptionIv = match[2].toByteArray() + val encryptionKeyResponse = app.get(encryptionUri, headers = headers, verify = false) + encryptionData = encryptionKeyResponse.body.bytes() } - return listOf(HlsDownloadData(byteArrayOf(), 1, 1, true)).iterator() + val relativeUrl = getParentLink(secondSelection.streamUrl) + val allTsList = TS_EXTENSION_REGEX.findAll(m3u8Response + "\n").map { ts -> + val value = ts.groupValues[1] + if (isNotCompleteUrl(value)) { + "$relativeUrl/${value}" + } else { + value + } + }.toList() + if (allTsList.isEmpty()) throw IllegalArgumentException("ts must be non empty") + + return LazyHlsDownloadData( + encryptionData = encryptionData, + encryptionIv = encryptionIv, + isEncrypted = encryptionState, + allTsLinks = allTsList, + relativeUrl = relativeUrl, + headers = headers + ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index c138ea75..f4eb37b7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -15,7 +15,6 @@ import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.net.toUri -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData import androidx.preference.PreferenceManager import androidx.work.Data @@ -32,18 +31,15 @@ import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly import java.io.BufferedInputStream @@ -51,11 +47,9 @@ import java.io.File import java.io.InputStream import java.io.OutputStream import java.lang.Thread.sleep -import java.net.URI import java.net.URL import java.net.URLConnection import java.util.* -import kotlin.math.roundToInt const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general" const val DOWNLOAD_CHANNEL_NAME = "Downloads" @@ -92,7 +86,7 @@ object VideoDownloadManager { @DrawableRes const val pressToStopIcon = R.drawable.exo_icon_stop - private var updateCount : Int = 0 + private var updateCount: Int = 0 private val downloadDataUpdateCount = MutableLiveData() enum class DownloadType { @@ -687,7 +681,8 @@ object VideoDownloadManager { return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream) } - fun downloadThing( + @Throws + suspend fun downloadThing( context: Context, link: IDownloadableMinimum, name: String, @@ -696,9 +691,9 @@ object VideoDownloadManager { tryResume: Boolean, parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, - ): Int { + ): Int = withContext(Dispatchers.IO) { if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) { - return ERROR_UNKNOWN + return@withContext ERROR_UNKNOWN } val basePath = context.getBasePath() @@ -714,7 +709,7 @@ object VideoDownloadManager { } val stream = setupStream(context, name, relativePath, extension, tryResume) - if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode + if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode val resume = stream.resume!! val fileStream = stream.fileStream!! @@ -766,7 +761,7 @@ object VideoDownloadManager { } val bytesTotal = contentLength + resumeLength - if (extension == "mp4" && bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG + if (extension == "mp4" && bytesTotal < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG parentId?.let { setKey( @@ -845,11 +840,13 @@ object VideoDownloadManager { DownloadActionType.Pause -> { isPaused = true; updateNotification() } + DownloadActionType.Stop -> { isStopped = true; updateNotification() removeKey(KEY_RESUME_PACKAGES, event.first.toString()) saveQueue() } + DownloadActionType.Resume -> { isPaused = false; updateNotification() } @@ -917,15 +914,17 @@ object VideoDownloadManager { } // RETURN MESSAGE - return when { + return@withContext when { isFailed -> { parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } ERROR_CONNECTION_ERROR } + isStopped -> { parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } deleteFile() } + else -> { parentId?.let { id -> downloadProgressEvent.invoke( @@ -989,6 +988,7 @@ object VideoDownloadManager { found.delete() this.createDirectory(directoryName) } + this.isDirectory -> this.createDirectory(directoryName) else -> this.parentFile?.createDirectory(directoryName) } @@ -1107,7 +1107,8 @@ object VideoDownloadManager { return SUCCESS_STOPPED } - private fun downloadHLS( + @Throws + private suspend fun downloadHLS( context: Context, link: ExtractorLink, name: String, @@ -1115,16 +1116,8 @@ object VideoDownloadManager { parentId: Int?, startIndex: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit - ): Int { + ): Int = withContext(Dispatchers.IO) { val extension = "mp4" - fun logcatPrint(vararg items: Any?) { - items.forEach { - println("[HLS]: $it") - } - } - - val m3u8Helper = M3u8Helper() - logcatPrint("initialised the HLS downloader.") val m3u8 = M3u8Helper.M3u8Stream( link.url, link.quality, mapOf("referer" to link.referer) @@ -1139,54 +1132,40 @@ object VideoDownloadManager { ) else folder val stream = setupStream(context, name, relativePath, extension, realIndex > 0) - if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode + if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - if (!stream.resume!!) realIndex = 0 - val fileLengthAdd = stream.fileLength!! - val tsIterator = runBlocking { - m3u8Helper.hlsYield(listOf(m3u8), realIndex) - } + if (stream.resume != true) realIndex = 0 + val fileLengthAdd = stream.fileLength ?: 0 + val items = M3u8Helper2.hslLazy(listOf(m3u8)) val displayName = getDisplayName(name, extension) val fileStream = stream.fileStream!! - val firstTs = tsIterator.next() - var isDone = false var isFailed = false var isPaused = false - var bytesDownloaded = firstTs.bytes.size.toLong() + fileLengthAdd - var tsProgress = 1L + realIndex - val totalTs = firstTs.totalTs.toLong() + var bytesDownloaded = fileLengthAdd + var tsProgress: Long = realIndex.toLong() + 1 // we don't want div by zero + val totalTs: Long = items.size.toLong() fun deleteFile(): Int { return delete(context, name, relativePath, extension, parentId, basePath.first) } - /* - Most of the auto generated m3u8 out there have TS of the same size. - And only the last TS might have a different size. - - But oh well, in cases of handmade m3u8 streams this will go all over the place ¯\_(ツ)_/¯ - So ya, this calculates an estimate of how many bytes the file is going to be. - - > (bytesDownloaded/tsProgress)*totalTs - */ fun updateInfo() { - parentId?.let { - setKey( - KEY_DOWNLOAD_INFO, - it.toString(), - DownloadedFileInfo( - (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), - relativePath ?: "", - displayName, - tsProgress.toString(), - basePath = basePath.second - ) + setKey( + KEY_DOWNLOAD_INFO, + (parentId ?: return).toString(), + DownloadedFileInfo( + // approx bytes + totalBytes = (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), + relativePath = relativePath ?: "", + displayName = displayName, + extraInfo = tsProgress.toString(), + basePath = basePath.second ) - } + ) } updateInfo() @@ -1210,9 +1189,7 @@ object VideoDownloadManager { (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), ) ) - } catch (e: Exception) { - // IDK MIGHT ERROR - } + } catch (_: Throwable) {} } createNotificationCallback.invoke( @@ -1226,24 +1203,6 @@ object VideoDownloadManager { ) } - fun stopIfError(ts: M3u8Helper.HlsDownloadData): Int? { - if (ts.errored || ts.bytes.isEmpty()) { - val error: Int = if (!ts.errored) { - logcatPrint("Error: No stream was found.") - ERROR_UNKNOWN - } else { - logcatPrint("Error: Failed to fetch data.") - ERROR_CONNECTION_ERROR - } - isFailed = true - fileStream.close() - deleteFile() - updateNotification() - return error - } - return null - } - val notificationCoroutine = main { while (true) { if (!isDone) { @@ -1261,11 +1220,11 @@ object VideoDownloadManager { DownloadActionType.Stop -> { isFailed = true } + DownloadActionType.Pause -> { - isPaused = - true // Pausing is not supported since well...I need to know the index of the ts it was paused at - // it may be possible to store it in a variable, but when the app restarts it will be lost + isPaused = true } + DownloadActionType.Resume -> { isPaused = false } @@ -1278,32 +1237,22 @@ object VideoDownloadManager { try { if (parentId != null) downloadEvent -= downloadEventListener - } catch (e: Exception) { - logError(e) + } catch (t: Throwable) { + logError(t) } try { parentId?.let { downloadStatus.remove(it) } - } catch (e: Exception) { - logError(e) - // IDK MIGHT ERROR + } catch (t: Throwable) { + logError(t) } notificationCoroutine.cancel() } - stopIfError(firstTs).let { - if (it != null) { - closeAll() - return it - } - } - if (parentId != null) downloadEvent += downloadEventListener - fileStream.write(firstTs.bytes) - fun onFailed() { fileStream.close() deleteFile() @@ -1311,31 +1260,29 @@ object VideoDownloadManager { closeAll() } - for (ts in tsIterator) { + for (idx in realIndex until items.size) { while (isPaused) { if (isFailed) { onFailed() - return SUCCESS_STOPPED + return@withContext SUCCESS_STOPPED } - sleep(100) + delay(100) } if (isFailed) { onFailed() - return SUCCESS_STOPPED + return@withContext SUCCESS_STOPPED } - stopIfError(ts).let { - if (it != null) { - closeAll() - return it - } + val bytes = items.resolveLinkSafe(idx) ?: run { + isFailed = true + onFailed() + return@withContext ERROR_CONNECTION_ERROR } - fileStream.write(ts.bytes) - tsProgress = ts.currentIndex.toLong() - bytesDownloaded += ts.bytes.size.toLong() - logcatPrint("Download progress ${((tsProgress.toFloat() / totalTs.toFloat()) * 100).roundToInt()}%") + fileStream.write(bytes) + tsProgress = idx.toLong() + 1 + bytesDownloaded += bytes.size.toLong() updateInfo() } isDone = true @@ -1344,7 +1291,7 @@ object VideoDownloadManager { closeAll() updateInfo() - return SUCCESS_DOWNLOAD_DONE + return@withContext SUCCESS_DOWNLOAD_DONE } fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { @@ -1379,7 +1326,7 @@ object VideoDownloadManager { ) } - private fun downloadSingleEpisode( + private suspend fun downloadSingleEpisode( context: Context, source: String?, folder: String?, @@ -1405,25 +1352,29 @@ object VideoDownloadManager { null )?.extraInfo?.toIntOrNull() } else null - return downloadHLS(context, link, name, folder, ep.id, startIndex) { meta -> - main { - createNotification( - context, - source, - link.name, - ep, - meta.type, - meta.bytesDownloaded, - meta.bytesTotal, - notificationCallback, - meta.hlsProgress, - meta.hlsTotal - ) + return suspendSafeApiCall { + downloadHLS(context, link, name, folder, ep.id, startIndex) { meta -> + main { + createNotification( + context, + source, + link.name, + ep, + meta.type, + meta.bytesDownloaded, + meta.bytesTotal, + notificationCallback, + meta.hlsProgress, + meta.hlsTotal + ) + } } - }.also { extractorJob.cancel() } + }.also { + extractorJob.cancel() + } ?: ERROR_UNKNOWN } - return normalSafeApiCall { + return suspendSafeApiCall { downloadThing(context, link, name, folder, "mp4", tryResume, ep.id) { meta -> main { createNotification( @@ -1468,17 +1419,15 @@ object VideoDownloadManager { DownloadResumePackage(item, index) ) val connectionResult = withContext(Dispatchers.IO) { - normalSafeApiCall { - downloadSingleEpisode( - context, - item.source, - item.folder, - item.ep, - link, - notificationCallback, - resume - ).also { println("Single episode finished with return code: $it") } - } + downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + resume + ).also { println("Single episode finished with return code: $it") } } if (connectionResult != null && connectionResult > 0) { // SUCCESS removeKey(KEY_RESUME_PACKAGES, id.toString()) From 61d63b17d819d7899e04314293d8f01b651ef22a Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Fri, 18 Aug 2023 02:41:59 +0530 Subject: [PATCH 087/156] chore: acra improvements and media3 bump (#562) * Acra Bump * Media3 bump --- app/build.gradle.kts | 20 +++++++++---------- .../lagradost/cloudstream3/AcraApplication.kt | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b228fea0..3b215dbc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -181,22 +181,22 @@ dependencies { // implementation("androidx.leanback:leanback-paging:1.1.0-alpha09") // Media 3 - implementation("androidx.media3:media3-common:1.1.0") - implementation("androidx.media3:media3-exoplayer:1.1.0") - implementation("androidx.media3:media3-datasource-okhttp:1.1.0") - implementation("androidx.media3:media3-ui:1.1.0") - implementation("androidx.media3:media3-session:1.1.0") - implementation("androidx.media3:media3-cast:1.1.0") - implementation("androidx.media3:media3-exoplayer-hls:1.1.0") - implementation("androidx.media3:media3-exoplayer-dash:1.1.0") + implementation("androidx.media3:media3-common:1.1.1") + implementation("androidx.media3:media3-exoplayer:1.1.1") + implementation("androidx.media3:media3-datasource-okhttp:1.1.1") + implementation("androidx.media3:media3-ui:1.1.1") + implementation("androidx.media3:media3-session:1.1.1") + implementation("androidx.media3:media3-cast:1.1.1") + implementation("androidx.media3:media3-exoplayer-hls:1.1.1") + implementation("androidx.media3:media3-exoplayer-dash:1.1.1") // Custom ffmpeg extension for audio codecs implementation("com.github.recloudstream:media-ffmpeg:1.1.0") //implementation("com.google.android.exoplayer:extension-leanback:2.14.0") // Bug reports - implementation("ch.acra:acra-core:5.8.4") - implementation("ch.acra:acra-toast:5.8.4") + implementation("ch.acra:acra-core:5.11.0") + implementation("ch.acra:acra-toast:5.11.0") compileOnly("com.google.auto.service:auto-service-annotations:1.0") //either for java sources: diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 069287b0..61d467c4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -121,10 +121,10 @@ class AcraApplication : Application() { buildConfigClass = BuildConfig::class.java reportFormat = StringFormat.JSON - reportContent = arrayOf( + reportContent = listOf( ReportField.BUILD_CONFIG, ReportField.USER_CRASH_DATE, ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL, - ReportField.STACK_TRACE + ReportField.STACK_TRACE, ) // removed this due to bug when starting the app, moved it to when it actually crashes @@ -213,4 +213,4 @@ class AcraApplication : Application() { } } -} \ No newline at end of file +} From 8f6e8a8e99c349489f05294e2d46a9ce58afc1ae Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 18 Aug 2023 01:46:29 +0200 Subject: [PATCH 088/156] fixed #547 fuck inheritance --- app/build.gradle.kts | 2 +- .../ui/result/ResultFragmentPhone.kt | 53 ++++++++++++------- .../ui/result/ResultTrailerPlayer.kt | 3 -- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3b215dbc..f72ec0b0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -231,7 +231,7 @@ dependencies { // API because cba maintaining it myself implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") - implementation("com.github.discord:OverlappingPanels:0.1.3") + implementation("com.github.discord:OverlappingPanels:0.1.5") // debugImplementation because LeakCanary should only run in debug builds. //debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index 633ee762..ae0b7419 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -23,7 +23,6 @@ import androidx.core.view.isVisible import androidx.core.widget.NestedScrollView import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.RecyclerView import com.discord.panels.OverlappingPanelsLayout import com.discord.panels.PanelsChildGestureRegionObserver import com.google.android.gms.cast.framework.CastButtonFactory @@ -62,8 +61,6 @@ import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable -import com.lagradost.cloudstream3.utils.AppUtils.isLtr -import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.openBrowser import com.lagradost.cloudstream3.utils.ExtractorLink @@ -81,8 +78,13 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.VideoDownloadHelper -open class ResultFragmentPhone : FullScreenPlayer(), - PanelsChildGestureRegionObserver.GestureRegionsListener { +open class ResultFragmentPhone : FullScreenPlayer() { + private val gestureRegionsListener = object : PanelsChildGestureRegionObserver.GestureRegionsListener { + override fun onGestureRegionsUpdate(gestureRegions: List) { + binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) + } + } + protected lateinit var viewModel: ResultViewModel2 protected lateinit var syncModel: SyncViewModel @@ -210,15 +212,20 @@ open class ResultFragmentPhone : FullScreenPlayer(), loadTrailer() } + override fun onDestroy() { + super.onDestroy() + PanelsChildGestureRegionObserver.Provider.get().apply { + resultBinding?.resultCastItems?.let { + unregister(it) + } + removeGestureRegionsUpdateListener(gestureRegionsListener) + } + } + override fun onDestroyView() { //somehow this still leaks and I dont know why???? // todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt - PanelsChildGestureRegionObserver.Provider.get().let { obs -> - resultBinding?.resultCastItems?.let { - obs.unregister(it) - } - obs.removeGestureRegionsUpdateListener(this) - } + updateUIEvent -= ::updateUI binding = null resultBinding = null @@ -287,6 +294,8 @@ open class ResultFragmentPhone : FullScreenPlayer(), it.colorFromAttribute(R.attr.primaryBlackBackground) } super.onResume() + PanelsChildGestureRegionObserver.Provider.get() + .addGestureRegionsUpdateListener(gestureRegionsListener) } override fun onStop() { @@ -323,7 +332,16 @@ open class ResultFragmentPhone : FullScreenPlayer(), setUrl(storedData.url) syncModel.addFromUrl(storedData.url) val api = APIHolder.getApiFromNameNull(storedData.apiName) - PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(this) + + PanelsChildGestureRegionObserver.Provider.get().apply { + resultBinding?.resultCastItems?.let { + register(it) + } + addGestureRegionsUpdateListener(gestureRegionsListener) + } + + + // ===== ===== ===== resultBinding?.apply { @@ -374,9 +392,8 @@ open class ResultFragmentPhone : FullScreenPlayer(), DownloadButtonSetup.handleDownloadClick(downloadClickEvent) } ) - resultCastItems.let { - PanelsChildGestureRegionObserver.Provider.get().register(it) - } + + resultScroll.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY -> val dy = scrollY - oldScrollY if (dy > 0) { //check for scroll down @@ -1055,11 +1072,7 @@ open class ResultFragmentPhone : FullScreenPlayer(), override fun onPause() { super.onPause() - PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(this) - } - - override fun onGestureRegionsUpdate(gestureRegions: List) { - binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) + PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(gestureRegionsListener) } private fun setRecommendations(rec: List?, validApiName: String?) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index 1f663e31..eb8cb9b3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -118,9 +118,6 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed { override fun onTracksInfoChanged() {} override fun exitedPipMode() {} - - override fun onGestureRegionsUpdate(gestureRegions: List) {} - private fun updateFullscreen(fullscreen: Boolean) { isFullScreenPlayer = fullscreen lockRotation = fullscreen From e95dc1db2a94a4f3f5583bc626e331129be77a38 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Fri, 18 Aug 2023 21:16:03 +0530 Subject: [PATCH 089/156] fix: cast items recycler (finally) (#564) * turn cast items visible(tools) * prevent cast gesture listener from permanent RIP in one lifecycle --- .../ui/result/ResultFragmentPhone.kt | 20 +++++++++---------- app/src/main/res/layout/fragment_result.xml | 4 ++-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index ae0b7419..a932a57c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -212,19 +212,17 @@ open class ResultFragmentPhone : FullScreenPlayer() { loadTrailer() } - override fun onDestroy() { - super.onDestroy() - PanelsChildGestureRegionObserver.Provider.get().apply { - resultBinding?.resultCastItems?.let { - unregister(it) - } - removeGestureRegionsUpdateListener(gestureRegionsListener) - } - } - override fun onDestroyView() { + //somehow this still leaks and I dont know why???? // todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt + PanelsChildGestureRegionObserver.Provider.get().let { obs -> + resultBinding?.resultCastItems?.let { + obs.unregister(it) + } + + obs.removeGestureRegionsUpdateListener(gestureRegionsListener) + } updateUIEvent -= ::updateUI binding = null @@ -1127,4 +1125,4 @@ open class ResultFragmentPhone : FullScreenPlayer() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index ee3477b0..87de7186 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -476,7 +476,7 @@ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:itemCount="2" tools:listitem="@layout/cast_item" - tools:visibility="gone" /> + tools:visibility="visible" /> --> - \ No newline at end of file + From 56cb3d718188bf95e16cc062280bc5a16e42ea24 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 00:48:00 +0200 Subject: [PATCH 090/156] refactored download system for better preference + bugfixes --- .../cloudstream3/DownloaderTestImpl.kt | 2 +- .../com/lagradost/cloudstream3/MainAPI.kt | 2 +- .../ui/download/DownloadChildAdapter.kt | 2 +- .../ui/download/EasyDownloadButton.kt | 264 ----- .../ui/download/button/BaseFetchButton.kt | 61 +- .../ui/download/button/DownloadButton.kt | 17 +- .../ui/download/button/PieFetchButton.kt | 53 +- .../cloudstream3/utils/M3u8Helper.kt | 23 +- .../cloudstream3/utils/VideoDownloadHelper.kt | 6 +- .../utils/VideoDownloadManager.kt | 1016 ++++++++--------- .../main/res/drawable/baseline_stop_24.xml | 10 + 11 files changed, 604 insertions(+), 852 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt create mode 100644 app/src/main/res/drawable/baseline_stop_24.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt index 379a91e4..0a2db2bd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt @@ -50,7 +50,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do companion object { private const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0" + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" private var instance: DownloaderTestImpl? = null /** diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 7790f047..80332445 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -29,7 +29,7 @@ import java.util.* import kotlin.math.absoluteValue const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" //val baseHeader = mapOf("User-Agent" to USER_AGENT) val mapper = JsonMapper.builder().addModule(KotlinModule()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt index b4774cf8..1d7b5a83 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt @@ -23,7 +23,7 @@ data class VisualDownloadChildCached( val data: VideoDownloadHelper.DownloadEpisodeCached, ) -data class DownloadClickEvent(val action: Int, val data: EasyDownloadButton.IMinimumData) +data class DownloadClickEvent(val action: Int, val data: VideoDownloadHelper.DownloadEpisodeCached) class DownloadChildAdapter( var cardList: List, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt deleted file mode 100644 index 77878432..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt +++ /dev/null @@ -1,264 +0,0 @@ -package com.lagradost.cloudstream3.ui.download - -import android.animation.ObjectAnimator -import android.text.format.Formatter.formatShortFileSize -import android.view.View -import android.view.animation.DecelerateInterpolator -import android.widget.ImageView -import android.widget.TextView -import androidx.core.view.isGone -import androidx.core.view.isVisible -import androidx.core.widget.ContentLoadingProgressBar -import com.google.android.material.button.MaterialButton -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.Coroutines -import com.lagradost.cloudstream3.utils.IDisposable -import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons -import com.lagradost.cloudstream3.utils.VideoDownloadManager - -class EasyDownloadButton : IDisposable { - interface IMinimumData { - val id: Int - } - - private var _clickCallback: ((DownloadClickEvent) -> Unit)? = null - private var _imageChangeCallback: ((Pair) -> Unit)? = null - - override fun dispose() { - try { - _clickCallback = null - _imageChangeCallback = null - downloadProgressEventListener?.let { VideoDownloadManager.downloadProgressEvent -= it } - downloadStatusEventListener?.let { VideoDownloadManager.downloadStatusEvent -= it } - } catch (e: Exception) { - e.printStackTrace() - } - } - - private var downloadProgressEventListener: ((Triple) -> Unit)? = null - private var downloadStatusEventListener: ((Pair) -> Unit)? = - null - - fun setUpMaterialButton( - setupCurrentBytes: Long?, - setupTotalBytes: Long?, - progressBar: ContentLoadingProgressBar, - downloadButton: MaterialButton, - textView: TextView?, - data: IMinimumData, - clickCallback: (DownloadClickEvent) -> Unit, - ) { - setUpDownloadButton( - setupCurrentBytes, - setupTotalBytes, - progressBar, - textView, - data, - downloadButton, - { - downloadButton.setIconResource(it.first) - downloadButton.text = it.second - }, - clickCallback - ) - } - - fun setUpMoreButton( - setupCurrentBytes: Long?, - setupTotalBytes: Long?, - progressBar: ContentLoadingProgressBar, - downloadImage: ImageView, - textView: TextView?, - textViewProgress: TextView?, - clickableView: View, - isTextPercentage: Boolean, - data: IMinimumData, - clickCallback: (DownloadClickEvent) -> Unit, - ) { - setUpDownloadButton( - setupCurrentBytes, - setupTotalBytes, - progressBar, - textViewProgress, - data, - clickableView, - { (image, text) -> - downloadImage.isVisible = textViewProgress?.isGone ?: true - downloadImage.setImageResource(image) - textView?.text = text - }, - clickCallback, isTextPercentage - ) - } - - fun setUpButton( - setupCurrentBytes: Long?, - setupTotalBytes: Long?, - progressBar: ContentLoadingProgressBar, - downloadImage: ImageView, - textView: TextView?, - data: IMinimumData, - clickCallback: (DownloadClickEvent) -> Unit, - ) { - setUpDownloadButton( - setupCurrentBytes, - setupTotalBytes, - progressBar, - textView, - data, - downloadImage, - { - downloadImage.setImageResource(it.first) - }, - clickCallback - ) - } - - private fun setUpDownloadButton( - setupCurrentBytes: Long?, - setupTotalBytes: Long?, - progressBar: ContentLoadingProgressBar, - textView: TextView?, - data: IMinimumData, - downloadView: View, - downloadImageChangeCallback: (Pair) -> Unit, - clickCallback: (DownloadClickEvent) -> Unit, - isTextPercentage: Boolean = false - ) { - _clickCallback = clickCallback - _imageChangeCallback = downloadImageChangeCallback - var lastState: VideoDownloadManager.DownloadType? = null - var currentBytes = setupCurrentBytes ?: 0 - var totalBytes = setupTotalBytes ?: 0 - var needImageUpdate = true - - fun changeDownloadImage(state: VideoDownloadManager.DownloadType) { - lastState = state - if (currentBytes <= 0) needImageUpdate = true - val img = if (currentBytes > 0) { - when (state) { - VideoDownloadManager.DownloadType.IsPaused -> Pair( - R.drawable.ic_baseline_play_arrow_24, - R.string.download_paused - ) - VideoDownloadManager.DownloadType.IsDownloading -> Pair( - R.drawable.netflix_pause, - R.string.downloading - ) - else -> Pair(R.drawable.ic_baseline_delete_outline_24, R.string.downloaded) - } - } else { - Pair(R.drawable.netflix_download, R.string.download) - } - _imageChangeCallback?.invoke( - Pair( - img.first, - downloadView.context.getString(img.second) - ) - ) - } - - fun fixDownloadedBytes(setCurrentBytes: Long, setTotalBytes: Long, animate: Boolean) { - currentBytes = setCurrentBytes - totalBytes = setTotalBytes - - if (currentBytes == 0L) { - changeDownloadImage(VideoDownloadManager.DownloadType.IsStopped) - textView?.visibility = View.GONE - progressBar.visibility = View.GONE - } else { - if (lastState == VideoDownloadManager.DownloadType.IsStopped) { - changeDownloadImage(VideoDownloadManager.getDownloadState(data.id)) - } - textView?.visibility = View.VISIBLE - progressBar.visibility = View.VISIBLE - val currentMbString = formatShortFileSize(textView?.context, setCurrentBytes) - val totalMbString = formatShortFileSize(textView?.context, setTotalBytes) - - textView?.text = - if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else - textView?.context?.getString(R.string.download_size_format) - ?.format(currentMbString, totalMbString) - - progressBar.let { bar -> - bar.max = (setTotalBytes / 1000).toInt() - - if (animate) { - val animation: ObjectAnimator = ObjectAnimator.ofInt( - bar, - "progress", - bar.progress, - (setCurrentBytes / 1000).toInt() - ) - animation.duration = 500 - animation.setAutoCancel(true) - animation.interpolator = DecelerateInterpolator() - animation.start() - } else { - bar.progress = (setCurrentBytes / 1000).toInt() - } - } - } - } - - fixDownloadedBytes(currentBytes, totalBytes, false) - changeDownloadImage(VideoDownloadManager.getDownloadState(data.id)) - - downloadProgressEventListener = { downloadData: Triple -> - if (data.id == downloadData.first) { - if (downloadData.second != currentBytes || downloadData.third != totalBytes) { // TO PREVENT WASTING UI TIME - Coroutines.runOnMainThread { - fixDownloadedBytes(downloadData.second, downloadData.third, true) - changeDownloadImage(VideoDownloadManager.getDownloadState(data.id)) - } - } - } - } - - downloadStatusEventListener = - { downloadData: Pair -> - if (data.id == downloadData.first) { - if (lastState != downloadData.second || needImageUpdate) { // TO PREVENT WASTING UI TIME - Coroutines.runOnMainThread { - changeDownloadImage(downloadData.second) - } - } - } - } - - downloadProgressEventListener?.let { VideoDownloadManager.downloadProgressEvent += it } - downloadStatusEventListener?.let { VideoDownloadManager.downloadStatusEvent += it } - - downloadView.setOnClickListener { - if (currentBytes <= 0 || totalBytes <= 0) { - _clickCallback?.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) - } else { - val list = arrayListOf( - Pair(DOWNLOAD_ACTION_PLAY_FILE, R.string.popup_play_file), - Pair(DOWNLOAD_ACTION_DELETE_FILE, R.string.popup_delete_file), - ) - - // DON'T RESUME A DOWNLOADED FILE lastState != VideoDownloadManager.DownloadType.IsDone && - if ((currentBytes * 100 / totalBytes) < 98) { - list.add( - if (lastState == VideoDownloadManager.DownloadType.IsDownloading) - Pair(DOWNLOAD_ACTION_PAUSE_DOWNLOAD, R.string.popup_pause_download) - else - Pair(DOWNLOAD_ACTION_RESUME_DOWNLOAD, R.string.popup_resume_download) - ) - } - - it.popupMenuNoIcons( - list - ) { - _clickCallback?.invoke(DownloadClickEvent(itemId, data)) - } - } - } - - downloadView.setOnLongClickListener { - clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, data)) - return@setOnLongClickListener true - } - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt index 05f630a0..b43f1aac 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt @@ -22,7 +22,7 @@ data class DownloadMetadata( val progressPercentage: Long get() = if (downloadedLength < 1024) 0 else maxOf( 0, - minOf(100, (downloadedLength * 100L) / totalLength) + minOf(100, (downloadedLength * 100L) / (totalLength + 1)) ) } @@ -101,38 +101,41 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : open fun setProgress(downloadedBytes: Long, totalBytes: Long) { isZeroBytes = downloadedBytes == 0L - val steps = 10000L - progressBar.max = steps.toInt() - // div by zero error and 1 byte off is ok impo - val progress = (downloadedBytes * steps / (totalBytes + 1L)).toInt() + progressBar.post { + val steps = 10000L + progressBar.max = steps.toInt() + // div by zero error and 1 byte off is ok impo - val animation = ProgressBarAnimation( - progressBar, - progressBar.progress.toFloat(), - progress.toFloat() - ).apply { - fillAfter = true - duration = - if (progress > progressBar.progress) // we don't want to animate backward changes in progress - 100 - else - 0L - } + val progress = (downloadedBytes * steps / (totalBytes + 1L)).toInt() - if (isZeroBytes) { - progressText?.isVisible = false - } else { - progressText?.apply { - val currentMbString = Formatter.formatShortFileSize(context, downloadedBytes) - val totalMbString = Formatter.formatShortFileSize(context, totalBytes) - text = - //if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else - context?.getString(R.string.download_size_format) - ?.format(currentMbString, totalMbString) + val animation = ProgressBarAnimation( + progressBar, + progressBar.progress.toFloat(), + progress.toFloat() + ).apply { + fillAfter = true + duration = + if (progress > progressBar.progress) // we don't want to animate backward changes in progress + 100 + else + 0L } - } - progressBar.startAnimation(animation) + if (isZeroBytes) { + progressText?.isVisible = false + } else { + progressText?.apply { + val currentMbString = Formatter.formatShortFileSize(context, downloadedBytes) + val totalMbString = Formatter.formatShortFileSize(context, totalBytes) + text = + //if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else + context?.getString(R.string.download_size_format) + ?.format(currentMbString, totalMbString) + } + } + + progressBar.startAnimation(animation) + } } fun downloadStatusEvent(data: Pair) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt index bb2ba7b1..d97a4b88 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt @@ -21,14 +21,17 @@ class DownloadButton(context: Context, attributeSet: AttributeSet) : } override fun setStatus(status: DownloadStatusTell?) { - super.setStatus(status) - val txt = when (status) { - DownloadStatusTell.IsPaused -> R.string.download_paused - DownloadStatusTell.IsDownloading -> R.string.downloading - DownloadStatusTell.IsDone -> R.string.downloaded - else -> R.string.download + mainText?.post { + val txt = when (status) { + DownloadStatusTell.IsPaused -> R.string.download_paused + DownloadStatusTell.IsDownloading -> R.string.downloading + DownloadStatusTell.IsDone -> R.string.downloaded + else -> R.string.download + } + mainText?.setText(txt) } - mainText?.setText(txt) + super.setStatus(status) + } override fun setDefaultClickListener( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index 0b7a7fea..d20fcf93 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -174,7 +174,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : currentMetaData.apply { // DON'T RESUME A DOWNLOADED FILE lastState != VideoDownloadManager.DownloadType.IsDone && - if ((downloadedLength * 100 / totalLength) < 98) { + if (progressPercentage < 98) { list.add( if (status == VideoDownloadManager.DownloadType.IsDownloading) Pair(DOWNLOAD_ACTION_PAUSE_DOWNLOAD, R.string.popup_pause_download) @@ -248,33 +248,34 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : //progressBar.isVisible = // status != null && status != DownloadStatusTell.Complete && status != DownloadStatusTell.Error //progressBarBackground.isVisible = status != null && status != DownloadStatusTell.Complete - val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading + progressBarBackground.post { + val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading + if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) { + val animation = AnimationUtils.loadAnimation(context, waitingAnimation) + progressBarBackground.startAnimation(animation) + } else { + progressBarBackground.clearAnimation() + } - 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) + 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 } - - val progressDrawable = - if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline - - 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 } override fun resetView() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 6770e303..1fb3a72d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -186,6 +186,27 @@ object M3u8Helper2 { ) { val size get() = allTsLinks.size + suspend fun resolveLinkWhileSafe( + index: Int, + tries: Int = 3, + failDelay: Long = 3000, + condition : (() -> Boolean) + ): ByteArray? { + for (i in 0 until tries) { + if(!condition()) return null + + try { + val out = resolveLink(index) + return if(condition()) out else null + } catch (e: IllegalArgumentException) { + return null + } catch (t: Throwable) { + delay(failDelay) + } + } + return null + } + suspend fun resolveLinkSafe( index: Int, tries: Int = 3, @@ -240,8 +261,6 @@ object M3u8Helper2 { verify = false ).text - println("m3u8Response=$m3u8Response") - // encryption, this is because crunchy uses it var encryptionIv = byteArrayOf() var encryptionData = byteArrayOf() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt index a76cc115..d1614bc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt @@ -2,20 +2,18 @@ package com.lagradost.cloudstream3.utils import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.ui.download.EasyDownloadButton - object VideoDownloadHelper { data class DownloadEpisodeCached( @JsonProperty("name") val name: String?, @JsonProperty("poster") val poster: String?, @JsonProperty("episode") val episode: Int, @JsonProperty("season") val season: Int?, - @JsonProperty("id") override val id: Int, + @JsonProperty("id") val id: Int, @JsonProperty("parentId") val parentId: Int, @JsonProperty("rating") val rating: Int?, @JsonProperty("description") val description: String?, @JsonProperty("cacheTime") val cacheTime: Long, - ) : EasyDownloadButton.IMinimumData + ) data class DownloadHeaderCached( @JsonProperty("apiName") val apiName: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index f4eb37b7..0334103f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -15,7 +15,6 @@ import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.net.toUri -import androidx.lifecycle.MutableLiveData import androidx.preference.PreferenceManager import androidx.work.Data import androidx.work.ExistingWorkPolicy @@ -30,6 +29,8 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.amap +import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.services.VideoDownloadService @@ -42,13 +43,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly -import java.io.BufferedInputStream +import java.io.Closeable import java.io.File +import java.io.IOException import java.io.InputStream import java.io.OutputStream -import java.lang.Thread.sleep import java.net.URL -import java.net.URLConnection import java.util.* const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general" @@ -60,34 +60,31 @@ object VideoDownloadManager { private var currentDownloads = mutableListOf() private const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" - @DrawableRes - const val imgDone = R.drawable.rddone + @get:DrawableRes + val imgDone get() = R.drawable.rddone - @DrawableRes - const val imgDownloading = R.drawable.rdload + @get:DrawableRes + val imgDownloading get() = R.drawable.rdload - @DrawableRes - const val imgPaused = R.drawable.rdpause + @get:DrawableRes + val imgPaused get() = R.drawable.rdpause - @DrawableRes - const val imgStopped = R.drawable.rderror + @get:DrawableRes + val imgStopped get() = R.drawable.rderror - @DrawableRes - const val imgError = R.drawable.rderror + @get:DrawableRes + val imgError get() = R.drawable.rderror - @DrawableRes - const val pressToPauseIcon = R.drawable.ic_baseline_pause_24 + @get:DrawableRes + val pressToPauseIcon get() = R.drawable.ic_baseline_pause_24 - @DrawableRes - const val pressToResumeIcon = R.drawable.ic_baseline_play_arrow_24 + @get:DrawableRes + val pressToResumeIcon get() = R.drawable.ic_baseline_play_arrow_24 - @DrawableRes - const val pressToStopIcon = R.drawable.exo_icon_stop - - private var updateCount: Int = 0 - private val downloadDataUpdateCount = MutableLiveData() + @get:DrawableRes + val pressToStopIcon get() = R.drawable.baseline_stop_24 enum class DownloadType { IsPaused, @@ -251,9 +248,8 @@ object VideoDownloadManager { total: Long, notificationCallback: (Int, Notification) -> Unit, hlsProgress: Long? = null, - hlsTotal: Long? = null, - - ): Notification? { + hlsTotal: Long? = null + ): Notification? { try { if (total <= 0) return null// crash, invalid data @@ -336,14 +332,28 @@ object VideoDownloadManager { } val bigText = - if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { - (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix" - } else if (state == DownloadType.IsFailed) { - downloadFormat.format(context.getString(R.string.download_failed), rowTwo) - } else if (state == DownloadType.IsDone) { - downloadFormat.format(context.getString(R.string.download_done), rowTwo) - } else { - downloadFormat.format(context.getString(R.string.download_canceled), rowTwo) + when (state) { + DownloadType.IsDownloading, DownloadType.IsPaused -> { + (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix" + } + + DownloadType.IsFailed -> { + downloadFormat.format( + context.getString(R.string.download_failed), + rowTwo + ) + } + + DownloadType.IsDone -> { + downloadFormat.format(context.getString(R.string.download_done), rowTwo) + } + + else -> { + downloadFormat.format( + context.getString(R.string.download_canceled), + rowTwo + ) + } } val bodyStyle = NotificationCompat.BigTextStyle() @@ -351,14 +361,28 @@ object VideoDownloadManager { builder.setStyle(bodyStyle) } else { val txt = - if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { - rowTwo - } else if (state == DownloadType.IsFailed) { - downloadFormat.format(context.getString(R.string.download_failed), rowTwo) - } else if (state == DownloadType.IsDone) { - downloadFormat.format(context.getString(R.string.download_done), rowTwo) - } else { - downloadFormat.format(context.getString(R.string.download_canceled), rowTwo) + when (state) { + DownloadType.IsDownloading, DownloadType.IsPaused -> { + rowTwo + } + + DownloadType.IsFailed -> { + downloadFormat.format( + context.getString(R.string.download_failed), + rowTwo + ) + } + + DownloadType.IsDone -> { + downloadFormat.format(context.getString(R.string.download_done), rowTwo) + } + + else -> { + downloadFormat.format( + context.getString(R.string.download_canceled), + rowTwo + ) + } } builder.setContentText(txt) @@ -681,6 +705,171 @@ object VideoDownloadManager { return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream) } + /** This class handles the notifications, as well as the relevant key */ + data class DownloadMetaData( + private val id: Int?, + var bytesDownloaded: Long = 0, + var totalBytes: Long? = null, + + // notification metadata + private var lastUpdatedMs: Long = 0, + private val createNotificationCallback: (CreateNotificationMetadata) -> Unit, + + private var internalType: DownloadType = DownloadType.IsPending, + + // how many segments that we have downloaded + var hlsProgress: Int = 0, + // how many segments that exist + var hlsTotal: Int? = null, + // this is how many segments that has been written to the file + // will always be <= hlsProgress as we may keep some in a buffer + var hlsWrittenProgress: Long = 0, + + // this is used for copy with metadata on how much we have downloaded for setKey + private var downloadFileInfoTemplate: DownloadedFileInfo? = null + ) : Closeable { + val approxTotalBytes: Long + get() = totalBytes ?: hlsTotal?.let { total -> + (bytesDownloaded * (total / hlsProgress.toFloat())).toLong() + } ?: 0L + + private val isHLS get() = hlsTotal != null + + private val downloadEventListener = { event: Pair -> + if (event.first == id) { + when (event.second) { + DownloadActionType.Pause -> { + type = DownloadType.IsPaused + } + + DownloadActionType.Stop -> { + type = DownloadType.IsStopped + removeKey(KEY_RESUME_PACKAGES, event.first.toString()) + saveQueue() + } + + DownloadActionType.Resume -> { + type = DownloadType.IsDownloading + } + } + } + } + + private fun updateFileInfo() { + if (id == null) return + downloadFileInfoTemplate?.let { template -> + setKey( + KEY_DOWNLOAD_INFO, + id.toString(), + template.copy( + totalBytes = approxTotalBytes, + extraInfo = if (isHLS) hlsWrittenProgress.toString() else null + ) + ) + } + } + + fun setDownloadFileInfoTemplate(template: DownloadedFileInfo) { + downloadFileInfoTemplate = template + updateFileInfo() + } + + init { + if (id != null) { + downloadEvent += downloadEventListener + } + } + + override fun close() { + // as we may need to resume hls downloads, we save the current written index + if (isHLS) { + updateFileInfo() + } + if (id != null) { + downloadEvent -= downloadEventListener + downloadStatus -= id + } + } + + var type + get() = internalType + set(value) { + internalType = value + notify() + } + + companion object { + const val UPDATE_RATE_MS: Long = 1000L + } + + @JvmName("DownloadMetaDataNotify") + private fun notify() { + lastUpdatedMs = System.currentTimeMillis() + try { + val bytes = approxTotalBytes + + // notification creation + if (isHLS) { + createNotificationCallback( + CreateNotificationMetadata( + internalType, + bytesDownloaded, + bytes, + hlsTotal = hlsTotal?.toLong(), + hlsProgress = hlsProgress.toLong() + ) + ) + } else { + createNotificationCallback( + CreateNotificationMetadata( + internalType, + bytesDownloaded, + bytes, + ) + ) + } + + // as hls has an approx file size we want to update this metadata + if (isHLS) { + updateFileInfo() + } + + // push all events, this *should* not crash, TODO MUTEX? + if (id != null) { + downloadStatus[id] = type + downloadProgressEvent(Triple(id, bytesDownloaded, bytes)) + downloadStatusEvent(id to type) + } + } catch (t: Throwable) { + logError(t) + } + } + + private fun checkNotification() { + if (lastUpdatedMs + UPDATE_RATE_MS > System.currentTimeMillis()) return + notify() + } + + + /** adds the length and pushes a notification if necessary */ + fun addBytes(length: Long) { + bytesDownloaded += length + // we don't want to update the notification after it is paused, + // download progress may not stop directly when we "pause" it + if (type == DownloadType.IsDownloading) checkNotification() + } + + /** adds the length + hsl progress and pushes a notification if necessary */ + fun addSegment(length: Long) { + hlsProgress += 1 + addBytes(length) + } + + fun setWrittenSegment(segmentIndex: Int) { + hlsWrittenProgress = segmentIndex.toLong() + 1L + } + } + @Throws suspend fun downloadThing( context: Context, @@ -692,253 +881,225 @@ object VideoDownloadManager { parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, ): Int = withContext(Dispatchers.IO) { + // we cant download torrents with this implementation, aria2c might be used in the future if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) { return@withContext ERROR_UNKNOWN } - val basePath = context.getBasePath() - - val displayName = getDisplayName(name, extension) - val relativePath = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.first.isDownloadDir()) getRelativePath( - folder - ) else folder - - fun deleteFile(): Int { - return delete(context, name, relativePath, extension, parentId, basePath.first) - } - - val stream = setupStream(context, name, relativePath, extension, tryResume) - if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - - val resume = stream.resume!! - val fileStream = stream.fileStream!! - val fileLength = stream.fileLength!! - - // CONNECT - val connection: URLConnection = - URL(link.url.replace(" ", "%20")).openConnection() // IDK OLD PHONES BE WACK - - // SET CONNECTION SETTINGS - connection.connectTimeout = 10000 - connection.setRequestProperty("Accept-Encoding", "identity") - connection.setRequestProperty("user-agent", USER_AGENT) - if (link.referer.isNotEmpty()) connection.setRequestProperty("referer", link.referer) - - // extra stuff - connection.setRequestProperty( - "sec-ch-ua", - "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"" + var fileStream: OutputStream? = null + var requestStream: InputStream? = null + val metadata = DownloadMetaData( + totalBytes = 0, + bytesDownloaded = 0, + createNotificationCallback = createNotificationCallback, + id = parentId, ) + try { + // get the file path + val (baseFile, basePath) = context.getBasePath() + val displayName = getDisplayName(name, extension) + val relativePath = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.isDownloadDir()) getRelativePath( + folder + ) else folder - connection.setRequestProperty("sec-ch-ua-mobile", "?0") - connection.setRequestProperty("accept", "*/*") - // dataSource.setRequestProperty("Sec-Fetch-Site", "none") //same-site - connection.setRequestProperty("sec-fetch-user", "?1") - connection.setRequestProperty("sec-fetch-mode", "navigate") - connection.setRequestProperty("sec-fetch-dest", "video") - link.headers.entries.forEach { - connection.setRequestProperty(it.key, it.value) - } + // set up the download file + val stream = setupStream(context, name, relativePath, extension, tryResume) + if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode + fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN + val resume = stream.resume ?: return@withContext ERROR_UNKNOWN + val fileLength = stream.fileLength ?: return@withContext ERROR_UNKNOWN + val resumeAt = (if (resume) fileLength else 0) + metadata.bytesDownloaded = resumeAt + metadata.type = DownloadType.IsPending - if (resume) - connection.setRequestProperty("Range", "bytes=${fileLength}-") - val resumeLength = (if (resume) fileLength else 0) + // set up a connection + val request = app.get( + link.url.replace(" ", "%20"), + headers = link.headers + mapOf( + "Accept-Encoding" to "identity", + "accept" to "*/*", + "user-agent" to USER_AGENT, + "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", + "sec-fetch-mode" to "navigate", + "sec-fetch-dest" to "video", + "sec-fetch-user" to "?1", + "sec-ch-ua-mobile" to "?0", + ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap(), + referer = link.referer, + verify = false + ) - // ON CONNECTION - connection.connect() + // init variables + val contentLength = request.size ?: 0 + metadata.totalBytes = contentLength + resumeAt - val contentLength = try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // fuck android - connection.contentLengthLong - } else { - connection.getHeaderField("content-length").toLongOrNull() - ?: connection.contentLength.toLong() - } - } catch (e: Exception) { - logError(e) - 0L - } - val bytesTotal = contentLength + resumeLength - - if (extension == "mp4" && bytesTotal < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG - - parentId?.let { - setKey( - KEY_DOWNLOAD_INFO, - it.toString(), + // save + metadata.setDownloadFileInfoTemplate( DownloadedFileInfo( - bytesTotal, - relativePath ?: "", - displayName, - basePath = basePath.second + totalBytes = metadata.approxTotalBytes, + relativePath = relativePath ?: "", + displayName = displayName, + basePath = basePath ) ) + + // total length is less than 5mb, that is too short and something has gone wrong + if (extension == "mp4" && metadata.approxTotalBytes < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION + + // read the buffer into the filestream, this is equivalent of transferTo + requestStream = request.body.byteStream() + metadata.type = DownloadType.IsDownloading + + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var read: Int + while (requestStream.read(buffer, 0, DEFAULT_BUFFER_SIZE).also { read = it } >= 0) { + fileStream.write(buffer, 0, read) + + // wait until not paused + while (metadata.type == DownloadType.IsPaused) delay(100) + // if stopped then break to delete + if (metadata.type == DownloadType.IsStopped) break + metadata.addBytes(read.toLong()) + } + + if (metadata.type == DownloadType.IsStopped) { + return@withContext delete( + context, + name, + relativePath, + extension, + parentId, + baseFile + ) + } + + metadata.type = DownloadType.IsDone + return@withContext SUCCESS_DOWNLOAD_DONE + } catch (e: IOException) { + // some sort of IO error, this should not happened + // we just rethrow it + logError(e) + throw e + } catch (t: Throwable) { + // some sort of network error, will error + + // note that when failing we don't want to delete the file, + // only user interaction has that power + metadata.type = DownloadType.IsFailed + return@withContext ERROR_CONNECTION_ERROR + } finally { + fileStream?.closeQuietly() + requestStream?.closeQuietly() + metadata.close() } + } - // Could use connection.contentType for mime types when creating the file, - // however file is already created and players don't go of file type - // https://stackoverflow.com/questions/23714383/what-are-all-the-possible-values-for-http-content-type-header - // might receive application/octet-stream - /*if (!connection.contentType.isNullOrEmpty() && !connection.contentType.startsWith("video")) { - return ERROR_WRONG_CONTENT // CONTENT IS NOT VIDEO, SHOULD NEVER HAPPENED, BUT JUST IN CASE - }*/ + @Throws + private suspend fun downloadHLS( + context: Context, + link: ExtractorLink, + name: String, + folder: String?, + parentId: Int?, + startIndex: Int?, + createNotificationCallback: (CreateNotificationMetadata) -> Unit, + parallelConnections: Int = 3 + ): Int = withContext(Dispatchers.IO) { + require(parallelConnections >= 1) - // READ DATA FROM CONNECTION - val connectionInputStream: InputStream = BufferedInputStream(connection.inputStream) - val buffer = ByteArray(1024) - var count: Int - var bytesDownloaded = resumeLength + val metadata = DownloadMetaData( + createNotificationCallback = createNotificationCallback, + id = parentId + ) + val extension = "mp4" - var isPaused = false - var isStopped = false - var isDone = false - var isFailed = false + var fileStream: OutputStream? = null + try { + // the start .ts index + var startAt = startIndex ?: 0 - // TO NOT REUSE CODE - fun updateNotification() { - val type = when { - isDone -> DownloadType.IsDone - isStopped -> DownloadType.IsStopped - isFailed -> DownloadType.IsFailed - isPaused -> DownloadType.IsPaused - else -> DownloadType.IsDownloading - } + // set up the file data + val (baseFile, basePath) = context.getBasePath() + val relativePath = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.isDownloadDir()) getRelativePath( + folder + ) else folder + val displayName = getDisplayName(name, extension) + val stream = setupStream(context, name, relativePath, extension, startAt > 0) + if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode + if (stream.resume != true) startAt = 0 + fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN - parentId?.let { id -> - try { - downloadStatus[id] = type - downloadStatusEvent.invoke(Pair(id, type)) - downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal)) - } catch (e: Exception) { - // IDK MIGHT ERROR - } - } - - createNotificationCallback.invoke( - CreateNotificationMetadata( - type, - bytesDownloaded, - bytesTotal + // push the metadata + metadata.bytesDownloaded = stream.fileLength ?: 0 + metadata.hlsProgress = startAt + metadata.type = DownloadType.IsPending + metadata.setDownloadFileInfoTemplate( + DownloadedFileInfo( + totalBytes = 0, + relativePath = relativePath ?: "", + displayName = displayName, + basePath = basePath ) ) - /*createNotification( - context, - source, - link.name, - ep, - type, - bytesDownloaded, - bytesTotal - )*/ - } - val downloadEventListener = { event: Pair -> - if (event.first == parentId) { - when (event.second) { - DownloadActionType.Pause -> { - isPaused = true; updateNotification() + // do the initial get request to fetch the segments + val m3u8 = M3u8Helper.M3u8Stream( + link.url, link.quality, mapOf( + "Accept-Encoding" to "identity", + "accept" to "*/*", + "user-agent" to USER_AGENT, + ) + if (link.referer.isNotBlank()) mapOf("referer" to link.referer) else emptyMap() + ) + val items = M3u8Helper2.hslLazy(listOf(m3u8)) + + metadata.hlsTotal = items.size + metadata.type = DownloadType.IsDownloading + + // does several connections in parallel instead of a regular for loop to improve + // download speed + (startAt until items.size).chunked(parallelConnections).forEach { subset -> + // wait until not paused + while (metadata.type == DownloadType.IsPaused) delay(100) + // if stopped then break to delete + if (metadata.type == DownloadType.IsStopped) return@forEach + + subset.amap { idx -> + idx to items.resolveLinkSafe(idx)?.also { bytes -> + metadata.addSegment(bytes.size.toLong()) } - - DownloadActionType.Stop -> { - isStopped = true; updateNotification() - removeKey(KEY_RESUME_PACKAGES, event.first.toString()) - saveQueue() - } - - DownloadActionType.Resume -> { - isPaused = false; updateNotification() + }.forEach { (idx, bytes) -> + if (bytes == null) { + metadata.type = DownloadType.IsFailed + return@withContext ERROR_CONNECTION_ERROR } + fileStream.write(bytes) + metadata.setWrittenSegment(idx) } } - } - if (parentId != null) - downloadEvent += downloadEventListener - - // UPDATE DOWNLOAD NOTIFICATION - val notificationCoroutine = main { - while (true) { - if (!isPaused) { - updateNotification() - } - for (i in 1..10) { - delay(100) - } - } - } - - // THE REAL READ - try { - while (true) { - count = connectionInputStream.read(buffer) - if (count < 0) break - bytesDownloaded += count - // downloadProgressEvent.invoke(Pair(id, bytesDownloaded)) // Updates too much for any UI to keep up with - while (isPaused) { - sleep(100) - if (isStopped) { - break - } - } - if (isStopped) { - break - } - fileStream.write(buffer, 0, count) - } - } catch (e: Exception) { - logError(e) - isFailed = true - updateNotification() - } - - // REMOVE AND EXIT ALL - fileStream.close() - connectionInputStream.close() - notificationCoroutine.cancel() - - try { - if (parentId != null) - downloadEvent -= downloadEventListener - } catch (e: Exception) { - logError(e) - } - - try { - parentId?.let { - downloadStatus.remove(it) - } - } catch (e: Exception) { - // IDK MIGHT ERROR - } - - // RETURN MESSAGE - return@withContext when { - isFailed -> { - parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } - ERROR_CONNECTION_ERROR + if (metadata.type == DownloadType.IsStopped) { + return@withContext delete( + context, + name, + relativePath, + extension, + parentId, + baseFile + ) } - isStopped -> { - parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } - deleteFile() - } - - else -> { - parentId?.let { id -> - downloadProgressEvent.invoke( - Triple( - id, - bytesDownloaded, - bytesTotal - ) - ) - } - isDone = true - updateNotification() - SUCCESS_DOWNLOAD_DONE - } + metadata.type = DownloadType.IsDone + return@withContext SUCCESS_DOWNLOAD_DONE + } catch (t: Throwable) { + logError(t) + metadata.type = DownloadType.IsFailed + return@withContext ERROR_UNKNOWN + } finally { + fileStream?.closeQuietly() + metadata.close() } } @@ -1107,192 +1268,6 @@ object VideoDownloadManager { return SUCCESS_STOPPED } - @Throws - private suspend fun downloadHLS( - context: Context, - link: ExtractorLink, - name: String, - folder: String?, - parentId: Int?, - startIndex: Int?, - createNotificationCallback: (CreateNotificationMetadata) -> Unit - ): Int = withContext(Dispatchers.IO) { - val extension = "mp4" - - val m3u8 = M3u8Helper.M3u8Stream( - link.url, link.quality, mapOf("referer" to link.referer) - ) - - var realIndex = startIndex ?: 0 - val basePath = context.getBasePath() - - val relativePath = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.first.isDownloadDir()) getRelativePath( - folder - ) else folder - - val stream = setupStream(context, name, relativePath, extension, realIndex > 0) - if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - - if (stream.resume != true) realIndex = 0 - val fileLengthAdd = stream.fileLength ?: 0 - val items = M3u8Helper2.hslLazy(listOf(m3u8)) - - val displayName = getDisplayName(name, extension) - - val fileStream = stream.fileStream!! - - var isDone = false - var isFailed = false - var isPaused = false - var bytesDownloaded = fileLengthAdd - var tsProgress: Long = realIndex.toLong() + 1 // we don't want div by zero - val totalTs: Long = items.size.toLong() - - fun deleteFile(): Int { - return delete(context, name, relativePath, extension, parentId, basePath.first) - } - - fun updateInfo() { - setKey( - KEY_DOWNLOAD_INFO, - (parentId ?: return).toString(), - DownloadedFileInfo( - // approx bytes - totalBytes = (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), - relativePath = relativePath ?: "", - displayName = displayName, - extraInfo = tsProgress.toString(), - basePath = basePath.second - ) - ) - } - - updateInfo() - - fun updateNotification() { - val type = when { - isDone -> DownloadType.IsDone - isFailed -> DownloadType.IsFailed - isPaused -> DownloadType.IsPaused - else -> DownloadType.IsDownloading - } - - parentId?.let { id -> - try { - downloadStatus[id] = type - downloadStatusEvent.invoke(Pair(id, type)) - downloadProgressEvent.invoke( - Triple( - id, - bytesDownloaded, - (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), - ) - ) - } catch (_: Throwable) {} - } - - createNotificationCallback.invoke( - CreateNotificationMetadata( - type, - bytesDownloaded, - (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), - tsProgress, - totalTs - ) - ) - } - - val notificationCoroutine = main { - while (true) { - if (!isDone) { - updateNotification() - } - for (i in 1..10) { - delay(100) - } - } - } - - val downloadEventListener = { event: Pair -> - if (event.first == parentId) { - when (event.second) { - DownloadActionType.Stop -> { - isFailed = true - } - - DownloadActionType.Pause -> { - isPaused = true - } - - DownloadActionType.Resume -> { - isPaused = false - } - } - updateNotification() - } - } - - fun closeAll() { - try { - if (parentId != null) - downloadEvent -= downloadEventListener - } catch (t: Throwable) { - logError(t) - } - try { - parentId?.let { - downloadStatus.remove(it) - } - } catch (t: Throwable) { - logError(t) - } - notificationCoroutine.cancel() - } - - if (parentId != null) - downloadEvent += downloadEventListener - - fun onFailed() { - fileStream.close() - deleteFile() - updateNotification() - closeAll() - } - - for (idx in realIndex until items.size) { - while (isPaused) { - if (isFailed) { - onFailed() - return@withContext SUCCESS_STOPPED - } - delay(100) - } - - if (isFailed) { - onFailed() - return@withContext SUCCESS_STOPPED - } - - val bytes = items.resolveLinkSafe(idx) ?: run { - isFailed = true - onFailed() - return@withContext ERROR_CONNECTION_ERROR - } - - fileStream.write(bytes) - tsProgress = idx.toLong() + 1 - bytesDownloaded += bytes.size.toLong() - updateInfo() - } - isDone = true - fileStream.close() - updateNotification() - - closeAll() - updateInfo() - return@withContext SUCCESS_DOWNLOAD_DONE - } fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { return getFileName(context, metadata.name, metadata.episode, metadata.season) @@ -1353,22 +1328,30 @@ object VideoDownloadManager { )?.extraInfo?.toIntOrNull() } else null return suspendSafeApiCall { - downloadHLS(context, link, name, folder, ep.id, startIndex) { meta -> - main { - createNotification( - context, - source, - link.name, - ep, - meta.type, - meta.bytesDownloaded, - meta.bytesTotal, - notificationCallback, - meta.hlsProgress, - meta.hlsTotal - ) + downloadHLS( + context, + link, + name, + folder, + ep.id, + startIndex, + createNotificationCallback = { meta -> + main { + createNotification( + context, + source, + link.name, + ep, + meta.type, + meta.bytesDownloaded, + meta.bytesTotal, + notificationCallback, + meta.hlsProgress, + meta.hlsTotal + ) + } } - } + ) }.also { extractorJob.cancel() } ?: ERROR_UNKNOWN @@ -1392,7 +1375,7 @@ object VideoDownloadManager { }.also { extractorJob.cancel() } ?: ERROR_UNKNOWN } - fun downloadCheck( + suspend fun downloadCheck( context: Context, notificationCallback: (Int, Notification) -> Unit, ): Int? { if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) { @@ -1407,42 +1390,55 @@ object VideoDownloadManager { currentDownloads.add(id) - main { - try { - for (index in (pkg.linkIndex ?: 0) until item.links.size) { - val link = item.links[index] - val resume = pkg.linkIndex == index + try { + for (index in (pkg.linkIndex ?: 0) until item.links.size) { + val link = item.links[index] + val resume = pkg.linkIndex == index - setKey( - KEY_RESUME_PACKAGES, - id.toString(), - DownloadResumePackage(item, index) + setKey( + KEY_RESUME_PACKAGES, + id.toString(), + DownloadResumePackage(item, index) + ) + + var connectionResult = + downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + resume + ) + //.also { println("Single episode finished with return code: $it") } + + // retry every link at least once + if (connectionResult <= 0) { + connectionResult = downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + true ) - val connectionResult = withContext(Dispatchers.IO) { - downloadSingleEpisode( - context, - item.source, - item.folder, - item.ep, - link, - notificationCallback, - resume - ).also { println("Single episode finished with return code: $it") } - } - if (connectionResult != null && connectionResult > 0) { // SUCCESS - removeKey(KEY_RESUME_PACKAGES, id.toString()) - break - } else if (index == item.links.lastIndex) { - downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) - } } - } catch (e: Exception) { - logError(e) - } finally { - currentDownloads.remove(id) - // Because otherwise notifications will not get caught by the workmanager - downloadCheckUsingWorker(context) + + if (connectionResult > 0) { // SUCCESS + removeKey(KEY_RESUME_PACKAGES, id.toString()) + break + } else if (index == item.links.lastIndex) { + downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) + } } + } catch (e: Exception) { + logError(e) + } finally { + currentDownloads.remove(id) + // Because otherwise notifications will not get caught by the work manager + downloadCheckUsingWorker(context) } } return null @@ -1538,26 +1534,13 @@ object VideoDownloadManager { return context.getKey(KEY_RESUME_PACKAGES, id.toString()) } - fun downloadFromResume( + suspend fun downloadFromResume( context: Context, pkg: DownloadResumePackage, notificationCallback: (Int, Notification) -> Unit, setKey: Boolean = true ) { if (!currentDownloads.any { it == pkg.item.ep.id }) { -// if (currentDownloads.size == maxConcurrentDownloads) { -// main { -//// showToast( // can be replaced with regular Toast -//// context, -//// "${pkg.item.ep.mainName}${pkg.item.ep.episode?.let { " ${context.getString(R.string.episode)} $it " } ?: " "}${ -//// context.getString( -//// R.string.queued -//// ) -//// }", -//// Toast.LENGTH_SHORT -//// ) -// } -// } downloadQueue.addLast(pkg) downloadCheck(context, notificationCallback) if (setKey) saveQueue() @@ -1590,7 +1573,7 @@ object VideoDownloadManager { return false }*/ - fun downloadEpisode( + suspend fun downloadEpisode( context: Context?, source: String?, folder: String?, @@ -1599,13 +1582,12 @@ object VideoDownloadManager { notificationCallback: (Int, Notification) -> Unit, ) { if (context == null) return - if (links.isNotEmpty()) { - downloadFromResume( - context, - DownloadResumePackage(DownloadItem(source, folder, ep, links), null), - notificationCallback - ) - } + if (links.isEmpty()) return + downloadFromResume( + context, + DownloadResumePackage(DownloadItem(source, folder, ep, links), null), + notificationCallback + ) } /** Worker stuff */ diff --git a/app/src/main/res/drawable/baseline_stop_24.xml b/app/src/main/res/drawable/baseline_stop_24.xml new file mode 100644 index 00000000..100cb1fc --- /dev/null +++ b/app/src/main/res/drawable/baseline_stop_24.xml @@ -0,0 +1,10 @@ + + + From a05616e3e8c6c0373f88a7c6dcc022d42ca7188d Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 01:37:48 +0200 Subject: [PATCH 091/156] fix --- .../cloudstream3/extractors/Mp4Upload.kt | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt index 93a280ed..e746b286 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt @@ -10,24 +10,39 @@ open class Mp4Upload : ExtractorApi() { override var name = "Mp4Upload" override var mainUrl = "https://www.mp4upload.com" private val srcRegex = Regex("""player\.src\("(.*?)"""") - override val requiresReferer = true + private val srcRegex2 = Regex("""player\.src\([\w\W]*src: "(.*?)"""") + override val requiresReferer = true + private val idMatch = Regex("""mp4upload\.com/(embed-|)([A-Za-z0-9]*)""") override suspend fun getUrl(url: String, referer: String?): List? { - with(app.get(url)) { - getAndUnpack(this.text).let { unpackedText -> - val quality = unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull() - srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link -> - return listOf( - ExtractorLink( - name, - name, - link, - url, - quality ?: Qualities.Unknown.value, - ) - ) - } - } + val realUrl = idMatch.find(url)?.groupValues?.get(2)?.let { id -> + "$mainUrl/embed-$id.html" + } ?: url + val response = app.get(realUrl) + val unpackedText = getAndUnpack(response.text) + val quality = + unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull() + srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link -> + return listOf( + ExtractorLink( + name, + name, + link, + url, + quality ?: Qualities.Unknown.value, + ) + ) + } + srcRegex2.find(unpackedText)?.groupValues?.get(1)?.let { link -> + return listOf( + ExtractorLink( + name, + name, + link, + url, + quality ?: Qualities.Unknown.value, + ) + ) } return null } From 35e1b8b4dcbe5172afc8f4cf23a673a84f99c194 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 01:38:40 +0200 Subject: [PATCH 092/156] bump --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f72ec0b0..71015e31 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -52,7 +52,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.1.3" + versionName = "4.1.4" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") From e20e3dcfd3ce450b0efe61d64db4a34413652c72 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 04:46:47 +0200 Subject: [PATCH 093/156] fixed some bugs caused by new download update --- app/build.gradle.kts | 2 +- .../utils/DownloadFileWorkManager.kt | 17 +- .../utils/VideoDownloadManager.kt | 271 ++++++++++-------- 3 files changed, 165 insertions(+), 125 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 71015e31..708a2083 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -52,7 +52,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.1.4" + versionName = "4.1.5" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt index c1eb649b..aa424c08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt @@ -7,6 +7,7 @@ import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.VideoDownloadManager.WORK_KEY_INFO @@ -25,15 +26,16 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo override suspend fun doWork(): Result { val key = workerParams.inputData.getString("key") try { - println("KEY $key") if (key == DOWNLOAD_CHECK) { - downloadCheck(applicationContext, ::handleNotification)?.let { - awaitDownload(it) - } + downloadCheck(applicationContext, ::handleNotification) } else if (key != null) { - val info = applicationContext.getKey(WORK_KEY_INFO, key) + val info = + applicationContext.getKey(WORK_KEY_INFO, key) val pkg = - applicationContext.getKey(WORK_KEY_PACKAGE, key) + applicationContext.getKey( + WORK_KEY_PACKAGE, + key + ) if (info != null) { downloadEpisode( applicationContext, @@ -43,10 +45,8 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo info.links, ::handleNotification ) - awaitDownload(info.ep.id) } else if (pkg != null) { downloadFromResume(applicationContext, pkg, ::handleNotification) - awaitDownload(pkg.item.ep.id) } removeKeys(key) } @@ -73,6 +73,7 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo VideoDownloadManager.DownloadType.IsDone, VideoDownloadManager.DownloadType.IsFailed, VideoDownloadManager.DownloadType.IsStopped -> { isDone = true } + else -> Unit } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 0334103f..ef0d9d8a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -26,6 +26,7 @@ import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType @@ -723,7 +724,7 @@ object VideoDownloadManager { var hlsTotal: Int? = null, // this is how many segments that has been written to the file // will always be <= hlsProgress as we may keep some in a buffer - var hlsWrittenProgress: Long = 0, + var hlsWrittenProgress: Int = 0, // this is used for copy with metadata on how much we have downloaded for setKey private var downloadFileInfoTemplate: DownloadedFileInfo? = null @@ -798,6 +799,15 @@ object VideoDownloadManager { notify() } + fun onDelete() { + bytesDownloaded = 0 + hlsWrittenProgress = 0 + hlsProgress = 0 + + //internalType = DownloadType.IsStopped + notify() + } + companion object { const val UPDATE_RATE_MS: Long = 1000L } @@ -842,6 +852,9 @@ object VideoDownloadManager { } } catch (t: Throwable) { logError(t) + if (BuildConfig.DEBUG) { + throw t + } } } @@ -866,7 +879,7 @@ object VideoDownloadManager { } fun setWrittenSegment(segmentIndex: Int) { - hlsWrittenProgress = segmentIndex.toLong() + 1L + hlsWrittenProgress = segmentIndex + 1 } } @@ -916,16 +929,18 @@ object VideoDownloadManager { // set up a connection val request = app.get( link.url.replace(" ", "%20"), - headers = link.headers + mapOf( - "Accept-Encoding" to "identity", - "accept" to "*/*", - "user-agent" to USER_AGENT, - "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", - "sec-fetch-mode" to "navigate", - "sec-fetch-dest" to "video", - "sec-fetch-user" to "?1", - "sec-ch-ua-mobile" to "?0", - ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap(), + headers = link.headers.appendAndDontOverride( + mapOf( + "Accept-Encoding" to "identity", + "accept" to "*/*", + "user-agent" to USER_AGENT, + "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", + "sec-fetch-mode" to "navigate", + "sec-fetch-dest" to "video", + "sec-fetch-user" to "?1", + "sec-ch-ua-mobile" to "?0", + ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap() + ), referer = link.referer, verify = false ) @@ -964,14 +979,14 @@ object VideoDownloadManager { } if (metadata.type == DownloadType.IsStopped) { - return@withContext delete( - context, - name, - relativePath, - extension, - parentId, - baseFile - ) + // we need to close before delete + fileStream.closeQuietly() + metadata.onDelete() + if (deleteFile(context, baseFile, relativePath ?: "", displayName)) { + return@withContext SUCCESS_STOPPED + } else { + return@withContext ERROR_DELETING_FILE + } } metadata.type = DownloadType.IsDone @@ -995,6 +1010,18 @@ object VideoDownloadManager { } } + /** Helper function to make sure duplicate attributes don't get overriden or inserted without lowercase cmp + * example: map("a" to 1) appendAndDontOverride map("A" to 2, "a" to 3, "c" to 4) = map("a" to 1, "c" to 4) + * */ + private fun Map.appendAndDontOverride(rhs: Map): Map { + val out = this.toMutableMap() + val current = this.keys.map { it.lowercase() } + for ((key, value) in rhs) { + if (current.contains(key.lowercase())) continue + out[key] = value + } + return out + } @Throws private suspend fun downloadHLS( @@ -1047,11 +1074,13 @@ object VideoDownloadManager { // do the initial get request to fetch the segments val m3u8 = M3u8Helper.M3u8Stream( - link.url, link.quality, mapOf( - "Accept-Encoding" to "identity", - "accept" to "*/*", - "user-agent" to USER_AGENT, - ) + if (link.referer.isNotBlank()) mapOf("referer" to link.referer) else emptyMap() + link.url, link.quality, link.headers.appendAndDontOverride( + mapOf( + "Accept-Encoding" to "identity", + "accept" to "*/*", + "user-agent" to USER_AGENT, + ) + if (link.referer.isNotBlank()) mapOf("referer" to link.referer) else emptyMap() + ) ) val items = M3u8Helper2.hslLazy(listOf(m3u8)) @@ -1081,14 +1110,14 @@ object VideoDownloadManager { } if (metadata.type == DownloadType.IsStopped) { - return@withContext delete( - context, - name, - relativePath, - extension, - parentId, - baseFile - ) + // we need to close before delete + fileStream.closeQuietly() + metadata.onDelete() + if (deleteFile(context, baseFile, relativePath ?: "", displayName)) { + return@withContext SUCCESS_STOPPED + } else { + return@withContext ERROR_DELETING_FILE + } } metadata.type = DownloadType.IsDone @@ -1194,7 +1223,7 @@ object VideoDownloadManager { return (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace( '/', File.separatorChar - ) + ).replace("${File.separatorChar}${File.separatorChar}", File.separatorChar.toString()) } /** @@ -1224,7 +1253,7 @@ object VideoDownloadManager { return this != null && this.filePath == getDownloadDir()?.filePath } - private fun delete( + /*private fun delete( context: Context, name: String, folder: String?, @@ -1235,7 +1264,7 @@ object VideoDownloadManager { val displayName = getDisplayName(name, extension) // delete all subtitle files - if (extension == "mp4") { + if (extension != "vtt" && extension != "srt") { try { delete(context, name, folder, "vtt", parentId, basePath) delete(context, name, folder, "srt", parentId, basePath) @@ -1248,9 +1277,9 @@ object VideoDownloadManager { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.isDownloadDir()) { val relativePath = getRelativePath(folder) val lastContent = - context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName) - if (lastContent != null) { - context.contentResolver.delete(lastContent, null, null) + context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName) ?: return ERROR_DELETING_FILE + if(context.contentResolver.delete(lastContent, null, null) <= 0) { + return ERROR_DELETING_FILE } } else { val dir = basePath?.gotoDir(folder) @@ -1260,13 +1289,12 @@ object VideoDownloadManager { // Cleans up empty directory if (dir.listFiles()?.isEmpty() == true) dir.delete() } -// } parentId?.let { downloadDeleteEvent.invoke(parentId) } } return SUCCESS_STOPPED - } + }*/ fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { @@ -1377,71 +1405,71 @@ object VideoDownloadManager { suspend fun downloadCheck( context: Context, notificationCallback: (Int, Notification) -> Unit, - ): Int? { - if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) { - val pkg = downloadQueue.removeFirst() - val item = pkg.item - val id = item.ep.id - if (currentDownloads.contains(id)) { // IF IT IS ALREADY DOWNLOADING, RESUME IT - downloadEvent.invoke(Pair(id, DownloadActionType.Resume)) - /** ID needs to be returned to the work-manager to properly await notification */ - return id - } + ) { + if (!(currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0)) return - currentDownloads.add(id) - - try { - for (index in (pkg.linkIndex ?: 0) until item.links.size) { - val link = item.links[index] - val resume = pkg.linkIndex == index - - setKey( - KEY_RESUME_PACKAGES, - id.toString(), - DownloadResumePackage(item, index) - ) - - var connectionResult = - downloadSingleEpisode( - context, - item.source, - item.folder, - item.ep, - link, - notificationCallback, - resume - ) - //.also { println("Single episode finished with return code: $it") } - - // retry every link at least once - if (connectionResult <= 0) { - connectionResult = downloadSingleEpisode( - context, - item.source, - item.folder, - item.ep, - link, - notificationCallback, - true - ) - } - - if (connectionResult > 0) { // SUCCESS - removeKey(KEY_RESUME_PACKAGES, id.toString()) - break - } else if (index == item.links.lastIndex) { - downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) - } - } - } catch (e: Exception) { - logError(e) - } finally { - currentDownloads.remove(id) - // Because otherwise notifications will not get caught by the work manager - downloadCheckUsingWorker(context) - } + val pkg = downloadQueue.removeFirst() + val item = pkg.item + val id = item.ep.id + if (currentDownloads.contains(id)) { // IF IT IS ALREADY DOWNLOADING, RESUME IT + downloadEvent.invoke(Pair(id, DownloadActionType.Resume)) + /** ID needs to be returned to the work-manager to properly await notification */ + // return id } - return null + + currentDownloads.add(id) + try { + for (index in (pkg.linkIndex ?: 0) until item.links.size) { + val link = item.links[index] + val resume = pkg.linkIndex == index + + setKey( + KEY_RESUME_PACKAGES, + id.toString(), + DownloadResumePackage(item, index) + ) + + var connectionResult = + downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + resume + ) + //.also { println("Single episode finished with return code: $it") } + + // retry every link at least once + if (connectionResult <= 0) { + connectionResult = downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + true + ) + } + + if (connectionResult > 0) { // SUCCESS + removeKey(KEY_RESUME_PACKAGES, id.toString()) + break + } else if (index == item.links.lastIndex) { + downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) + } + } + } catch (e: Exception) { + logError(e) + } finally { + currentDownloads.remove(id) + // Because otherwise notifications will not get caught by the work manager + downloadCheckUsingWorker(context) + } + + // return id } fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? { @@ -1500,23 +1528,21 @@ object VideoDownloadManager { return success } - private fun deleteFile(context: Context, id: Int): Boolean { - val info = - context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return false - downloadEvent.invoke(Pair(id, DownloadActionType.Stop)) - downloadProgressEvent.invoke(Triple(id, 0, 0)) - downloadStatusEvent.invoke(Pair(id, DownloadType.IsStopped)) - downloadDeleteEvent.invoke(id) - val base = basePathToFile(context, info.basePath) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) { + private fun deleteFile( + context: Context, + folder: UniFile?, + relativePath: String, + displayName: String + ): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && folder.isDownloadDir()) { val cr = context.contentResolver ?: return false val fileUri = - cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName) + cr.getExistingDownloadUriOrNullQ(relativePath, displayName) ?: return true // FILE NOT FOUND, ALREADY DELETED return cr.delete(fileUri, null, null) > 0 // IF DELETED ROWS IS OVER 0 } else { - val file = base?.gotoDir(info.relativePath)?.findFile(info.displayName) + val file = folder?.gotoDir(relativePath)?.findFile(displayName) // val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName) // val dFile = File(normalPath) if (file?.exists() != true) return true @@ -1530,6 +1556,17 @@ object VideoDownloadManager { } } + private fun deleteFile(context: Context, id: Int): Boolean { + val info = + context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return false + downloadEvent.invoke(Pair(id, DownloadActionType.Stop)) + downloadProgressEvent.invoke(Triple(id, 0, 0)) + downloadStatusEvent.invoke(id to DownloadType.IsStopped) + downloadDeleteEvent.invoke(id) + val base = basePathToFile(context, info.basePath) + return deleteFile(context, base, info.relativePath, info.displayName) + } + fun getDownloadResumePackage(context: Context, id: Int): DownloadResumePackage? { return context.getKey(KEY_RESUME_PACKAGES, id.toString()) } @@ -1544,10 +1581,12 @@ object VideoDownloadManager { downloadQueue.addLast(pkg) downloadCheck(context, notificationCallback) if (setKey) saveQueue() + //ret } else { - downloadEvent.invoke( + downloadEvent( Pair(pkg.item.ep.id, DownloadActionType.Resume) ) + //null } } From f571596bbc69d1fca24fb99dcef86227f03a4a80 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Sat, 19 Aug 2023 19:34:21 +0530 Subject: [PATCH 094/156] fix: expand resume watching sheet and ft: ripple on profile drawable when pressed (#566) * add ripple to profile icon on home * Update HomeParentItemAdapterPreview.kt --- .../cloudstream3/ui/home/HomeParentItemAdapterPreview.kt | 4 ++-- app/src/main/res/layout/fragment_home_head.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 943f784a..13497a99 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -539,7 +539,7 @@ class HomeParentItemAdapterPreview( resumeAdapter.updateList(resumeWatching) if (binding is FragmentHomeHeadBinding) { - binding.homeBookmarkParentItemTitle.setOnClickListener { + binding.homeWatchParentItemTitle.setOnClickListener { viewModel.popup( HomeViewModel.ExpandableHomepageList( HomePageList( @@ -578,4 +578,4 @@ class HomeParentItemAdapterPreview( } } } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/fragment_home_head.xml b/app/src/main/res/layout/fragment_home_head.xml index e13b96a8..90386ccf 100644 --- a/app/src/main/res/layout/fragment_home_head.xml +++ b/app/src/main/res/layout/fragment_home_head.xml @@ -61,7 +61,7 @@ android:layout_height="match_parent" android:layout_gravity="center" android:layout_marginStart="-50dp" - android:background="?android:attr/selectableItemBackgroundBorderless" + android:foreground="?android:attr/selectableItemBackgroundBorderless" android:contentDescription="@string/account" android:nextFocusLeft="@id/home_search" android:padding="10dp" @@ -288,4 +288,4 @@ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:listitem="@layout/home_result_grid" /> - \ No newline at end of file + From b3abf1e45fbf2a532b551f9d69dbf7c674765ba7 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 17:03:27 +0200 Subject: [PATCH 095/156] fixed decryption --- .../cloudstream3/utils/M3u8Helper.kt | 24 ++++++++----------- .../utils/VideoDownloadManager.kt | 2 ++ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 1fb3a72d..5c0b45de 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -88,21 +88,17 @@ object M3u8Helper2 { } } - private val defaultIvGen = sequence { - var initial = 1 + private fun defaultIv(index: Int) : ByteArray { + return toBytes16Big(index+1) + } - while (true) { - yield(toBytes16Big(initial)) - ++initial - } - }.iterator() - - fun getDecrypter( + fun getDecrypted( secretKey: ByteArray, data: ByteArray, - iv: ByteArray = "".toByteArray() + iv: ByteArray = byteArrayOf(), + index : Int, ): ByteArray { - val ivKey = if (iv.isEmpty()) defaultIvGen.next() else iv + val ivKey = if (iv.isEmpty()) defaultIv(index) else iv val c = Cipher.getInstance("AES/CBC/PKCS5Padding") val skSpec = SecretKeySpec(secretKey, "AES") val ivSpec = IvParameterSpec(ivKey) @@ -234,7 +230,7 @@ object M3u8Helper2 { if (tsData.isEmpty()) throw ErrorLoadingException("no data") return if (isEncrypted) { - getDecrypter(encryptionData, tsData, encryptionIv) + getDecrypted(encryptionData, tsData, encryptionIv, index) } else { tsData } @@ -272,13 +268,13 @@ object M3u8Helper2 { val match = ENCRYPTION_URL_IV_REGEX.find(m3u8Response)!!.groupValues - var encryptionUri = match[1] + var encryptionUri = match[2] if (isNotCompleteUrl(encryptionUri)) { encryptionUri = "${getParentLink(secondSelection.streamUrl)}/$encryptionUri" } - encryptionIv = match[2].toByteArray() + encryptionIv = match[3].toByteArray() val encryptionKeyResponse = app.get(encryptionUri, headers = headers, verify = false) encryptionData = encryptionKeyResponse.body.bytes() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index ef0d9d8a..dc3eaa25 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -803,6 +803,8 @@ object VideoDownloadManager { bytesDownloaded = 0 hlsWrittenProgress = 0 hlsProgress = 0 + if (id != null) + downloadDeleteEvent(id) //internalType = DownloadType.IsStopped notify() From 98b641714073e6bb84f74bd9ea3e19a03ed857aa Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 21:37:14 +0200 Subject: [PATCH 096/156] made downloader faster with parallel downloads --- .../ui/result/ResultViewModel2.kt | 6 +- .../utils/VideoDownloadManager.kt | 438 ++++++++++++++++-- 2 files changed, 395 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 2fe3b012..bdd27091 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -593,10 +593,8 @@ class ResultViewModel2 : ViewModel() { folder, if (link.url.contains(".srt")) ".srt" else "vtt", false, - null - ) { - // no notification - } + null, createNotificationCallback = {} + ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index dc3eaa25..d8ef7e85 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -40,14 +40,20 @@ import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly import java.io.Closeable import java.io.File import java.io.IOException -import java.io.InputStream import java.io.OutputStream import java.net.URL import java.util.* @@ -710,6 +716,8 @@ object VideoDownloadManager { data class DownloadMetaData( private val id: Int?, var bytesDownloaded: Long = 0, + var bytesWritten: Long = 0, + var totalBytes: Long? = null, // notification metadata @@ -732,10 +740,21 @@ object VideoDownloadManager { val approxTotalBytes: Long get() = totalBytes ?: hlsTotal?.let { total -> (bytesDownloaded * (total / hlsProgress.toFloat())).toLong() - } ?: 0L + } ?: bytesDownloaded private val isHLS get() = hlsTotal != null + private var stopListener: (() -> Unit)? = null + + /** on cancel button pressed or failed invoke this once and only once */ + fun setOnStop(callback: (() -> Unit)) { + stopListener = callback + } + + fun removeStopListener() { + stopListener = null + } + private val downloadEventListener = { event: Pair -> if (event.first == id) { when (event.second) { @@ -747,6 +766,8 @@ object VideoDownloadManager { type = DownloadType.IsStopped removeKey(KEY_RESUME_PACKAGES, event.first.toString()) saveQueue() + stopListener?.invoke() + stopListener = null } DownloadActionType.Resume -> { @@ -783,13 +804,14 @@ object VideoDownloadManager { override fun close() { // as we may need to resume hls downloads, we save the current written index - if (isHLS) { + if (isHLS || totalBytes == null) { updateFileInfo() } if (id != null) { downloadEvent -= downloadEventListener downloadStatus -= id } + stopListener = null } var type @@ -846,6 +868,11 @@ object VideoDownloadManager { updateFileInfo() } + if (internalType == DownloadType.IsStopped || internalType == DownloadType.IsFailed) { + stopListener?.invoke() + stopListener = null + } + // push all events, this *should* not crash, TODO MUTEX? if (id != null) { downloadStatus[id] = type @@ -874,6 +901,10 @@ object VideoDownloadManager { if (type == DownloadType.IsDownloading) checkNotification() } + fun addBytesWritten(length: Long) { + bytesWritten += length + } + /** adds the length + hsl progress and pushes a notification if necessary */ fun addSegment(length: Long) { hlsProgress += 1 @@ -885,6 +916,173 @@ object VideoDownloadManager { } } + /** bytes have the size end-start where the byte range is [start,end) + * note that ByteArray is a pointer and therefore cant be stored without cloning it */ + data class LazyStreamDownloadResponse( + val bytes: ByteArray, + val startByte: Long, + val endByte: Long, + ) { + val size get() = endByte - startByte + + override fun toString(): String { + return "$startByte->$endByte" + } + + override fun equals(other: Any?): Boolean { + if (other !is LazyStreamDownloadResponse) return false + return other.startByte == startByte && other.endByte == endByte + } + + override fun hashCode(): Int { + return Objects.hash(startByte, endByte) + } + } + + data class LazyStreamDownloadData( + private val url: String, + private val headers: Map, + private val referer: String, + /** This specifies where chunck i starts and ends, + * bytes=${chuckStartByte[ i ]}-${chuckStartByte[ i+1 ] -1} + * where out of bounds => bytes=${chuckStartByte[ i ]}- */ + private val chuckStartByte: LongArray, + val totalLength: Long?, + val downloadLength: Long?, + val chuckSize: Long, + val bufferSize: Int, + ) { + val size get() = chuckStartByte.size + + /** returns what byte it has downloaded, + * so start at 10 and download 4 bytes = return 14 + * + * the range is [startByte, endByte) to be able to do [a, b) [b, c) ect + * + * [a, null) will return inclusive to eof = [a, eof] + * + * throws an error if initial get request fails, can be specified as return startByte + * */ + @Throws + private suspend fun resolve( + startByte: Long, + endByte: Long?, + callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) + ): Long = withContext(Dispatchers.IO) { + var currentByte: Long = startByte + val stopAt = endByte ?: Long.MAX_VALUE + if (currentByte >= stopAt) return@withContext currentByte + + val request = app.get( + url, + headers = headers + mapOf( + // range header is inclusive so [startByte, endByte-1] = [startByte, endByte) + // if nothing at end the server will continue until eof + "Range" to "bytes=$startByte-" // ${endByte?.minus(1)?.toString() ?: "" } + ), + referer = referer, + verify = false + ) + val requestStream = request.body.byteStream() + + val buffer = ByteArray(bufferSize) + var read: Int + + try { + while (requestStream.read(buffer, 0, bufferSize).also { read = it } >= 0) { + val start = currentByte + currentByte += read.toLong() + + // this stops overflow + if (currentByte >= stopAt) { + callback(LazyStreamDownloadResponse(buffer, start, stopAt)) + break + } else { + callback(LazyStreamDownloadResponse(buffer, start, currentByte)) + } + } + } catch (e: CancellationException) { + throw e + } catch (t: Throwable) { + logError(t) + } finally { + requestStream.closeQuietly() + } + + return@withContext currentByte + } + + /** retries the resolve n times and returns true if successful */ + suspend fun resolveSafe( + index: Int, + retries: Int = 3, + callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) + ): Boolean { + var start = chuckStartByte.getOrNull(index) ?: return false + val end = chuckStartByte.getOrNull(index + 1) + + for (i in 0 until retries) { + try { + // in case + start = resolve(start, end, callback) + // no end defined, so we don't care exactly where it ended + if (end == null) return true + // we have download more or exactly what we needed + if (start >= end) return true + } catch (e: IllegalStateException) { + return false + } catch (e: CancellationException) { + return false + } catch (t: Throwable) { + continue + } + } + return false + } + + } + + @Throws + suspend fun streamLazy( + url: String, + headers: Map, + referer: String, + startByte: Long, + /** how many bytes every connection should be, by default it is 10 MiB */ + chuckSize: Long = (1 shl 20) * 10, + /** maximum bytes in the buffer that responds */ + bufferSize: Int = DEFAULT_BUFFER_SIZE + ): LazyStreamDownloadData { + // we don't want to make a separate connection for every 1kb + require(chuckSize > 1000) + + val contentLength = + app.head(url = url, headers = headers, referer = referer, verify = false).size + + var downloadLength: Long? = null + var totalLength: Long? = null + + val ranges = if (contentLength == null) { + LongArray(1) { startByte } + } else { + downloadLength = contentLength - startByte + totalLength = contentLength + LongArray((downloadLength / chuckSize).toInt()) { idx -> + startByte + idx * chuckSize + } + } + return LazyStreamDownloadData( + url = url, + headers = headers, + referer = referer, + chuckStartByte = ranges, + downloadLength = downloadLength, + totalLength = totalLength, + chuckSize = chuckSize, + bufferSize = bufferSize + ) + } + @Throws suspend fun downloadThing( context: Context, @@ -895,6 +1093,7 @@ object VideoDownloadManager { tryResume: Boolean, parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, + parallelConnections: Int = 3 ): Int = withContext(Dispatchers.IO) { // we cant download torrents with this implementation, aria2c might be used in the future if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) { @@ -902,7 +1101,7 @@ object VideoDownloadManager { } var fileStream: OutputStream? = null - var requestStream: InputStream? = null + //var requestStream: InputStream? = null val metadata = DownloadMetaData( totalBytes = 0, bytesDownloaded = 0, @@ -926,11 +1125,13 @@ object VideoDownloadManager { val fileLength = stream.fileLength ?: return@withContext ERROR_UNKNOWN val resumeAt = (if (resume) fileLength else 0) metadata.bytesDownloaded = resumeAt + metadata.bytesWritten = resumeAt metadata.type = DownloadType.IsPending - // set up a connection - val request = app.get( - link.url.replace(" ", "%20"), + val items = streamLazy( + url = link.url.replace(" ", "%20"), + referer = link.referer, + startByte = resumeAt, headers = link.headers.appendAndDontOverride( mapOf( "Accept-Encoding" to "identity", @@ -941,17 +1142,12 @@ object VideoDownloadManager { "sec-fetch-dest" to "video", "sec-fetch-user" to "?1", "sec-ch-ua-mobile" to "?0", - ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap() - ), - referer = link.referer, - verify = false + ) + ) ) - // init variables - val contentLength = request.size ?: 0 - metadata.totalBytes = contentLength + resumeAt - - // save + metadata.totalBytes = items.totalLength + metadata.type = DownloadType.IsDownloading metadata.setDownloadFileInfoTemplate( DownloadedFileInfo( totalBytes = metadata.approxTotalBytes, @@ -961,23 +1157,166 @@ object VideoDownloadManager { ) ) - // total length is less than 5mb, that is too short and something has gone wrong - if (extension == "mp4" && metadata.approxTotalBytes < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION + val currentMutex = Mutex() + val current = (0 until items.size).iterator() - // read the buffer into the filestream, this is equivalent of transferTo - requestStream = request.body.byteStream() - metadata.type = DownloadType.IsDownloading + val fileMutex = Mutex() + // start to data + val pendingData: HashMap = + hashMapOf() - val buffer = ByteArray(DEFAULT_BUFFER_SIZE) - var read: Int - while (requestStream.read(buffer, 0, DEFAULT_BUFFER_SIZE).also { read = it } >= 0) { - fileStream.write(buffer, 0, read) + val jobs = (0 until parallelConnections).map { + launch { - // wait until not paused - while (metadata.type == DownloadType.IsPaused) delay(100) - // if stopped then break to delete - if (metadata.type == DownloadType.IsStopped) break - metadata.addBytes(read.toLong()) + // this may seem a bit complex but it more or less acts as a queue system + // imagine we do the downloading [0,3] and it response in the order 0,2,3,1 + // file: [_,_,_,_] queue: [_,_,_,_] Initial condition + // file: [X,_,_,_] queue: [_,_,_,_] + added 0 directly to file + // file: [X,_,_,_] queue: [_,_,X,_] + added 2 to queue + // file: [X,_,_,_] queue: [_,_,X,X] + added 3 to queue + // file: [X,X,_,_] queue: [_,_,X,X] + added 1 directly to file + // file: [X,X,X,X] queue: [_,_,_,_] write the queue and remove from it + + val callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) = + callback@{ response -> + if (!isActive) return@callback + fileMutex.withLock { + // wait until not paused + while (metadata.type == DownloadType.IsPaused) delay(100) + // if stopped then throw + if (metadata.type == DownloadType.IsStopped || metadata.type == DownloadType.IsFailed) { + this.cancel() + return@callback + } + + val responseSize = response.size + metadata.addBytes(response.size) + + if (response.startByte == metadata.bytesWritten) { + // if we are first in the queue then write it directly + fileStream.write( + response.bytes, + 0, + responseSize.toInt() + ) + metadata.addBytesWritten(responseSize) + } else { + // otherwise append to queue, we need to clone the bytes as they will be overridden otherwise + pendingData[response.startByte] = + response.copy(bytes = response.bytes.clone()) + } + + while (true) { + // remove the current queue start, so no possibility of + // while(true) { continue } in case size = 0, and removed extra + // garbage + val pending = pendingData.remove(metadata.bytesWritten) ?: break + + val size = pending.size + + fileStream.write( + pending.bytes, + 0, + size.toInt() + ) + metadata.addBytesWritten(size) + } + } + } + + // this will take up the first available job and resolve + while (true) { + if (!isActive) return@launch + fileMutex.withLock { + if (metadata.type == DownloadType.IsStopped) return@launch + } + + // just in case, we never want this to fail due to multithreading + val index = currentMutex.withLock { + if (!current.hasNext()) return@launch + current.nextInt() + } + + // in case something has gone wrong set to failed if the fail is not caused by + // user cancellation + if (!items.resolveSafe(index, callback = callback)) { + fileMutex.withLock { + if (metadata.type != DownloadType.IsStopped) { + metadata.type = DownloadType.IsFailed + } + } + return@launch + } + } + } + } + + // fast stop as the jobs may be in a slow request + metadata.setOnStop { + jobs.forEach { job -> + try { + job.cancel() + } catch (t: Throwable) { + logError(t) + } + } + } + + jobs.forEach { it.join() } + + // jobs are finished so we don't want to stop them anymore + metadata.removeStopListener() + + // set up a connection + //val request = app.get( + // link.url.replace(" ", "%20"), + // headers = link.headers.appendAndDontOverride( + // mapOf( + // "Accept-Encoding" to "identity", + // "accept" to "*/*", + // "user-agent" to USER_AGENT, + // "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", + // "sec-fetch-mode" to "navigate", + // "sec-fetch-dest" to "video", + // "sec-fetch-user" to "?1", + // "sec-ch-ua-mobile" to "?0", + // ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap() + // ), + // referer = link.referer, + // verify = false + //) + + // init variables + //val contentLength = request.size ?: 0 + //metadata.totalBytes = contentLength + resumeAt + //// save + //metadata.setDownloadFileInfoTemplate( + // DownloadedFileInfo( + // totalBytes = metadata.approxTotalBytes, + // relativePath = relativePath ?: "", + // displayName = displayName, + // basePath = basePath + // ) + //) + //// total length is less than 5mb, that is too short and something has gone wrong + //if (extension == "mp4" && metadata.approxTotalBytes < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION + //// read the buffer into the filestream, this is equivalent of transferTo + //requestStream = request.body.byteStream() + //metadata.type = DownloadType.IsDownloading + //val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + //var read: Int + //while (requestStream.read(buffer, 0, DEFAULT_BUFFER_SIZE).also { read = it } >= 0) { + // fileStream.write(buffer, 0, read) + // // wait until not paused + // while (metadata.type == DownloadType.IsPaused) delay(100) + // // if stopped then break to delete + // if (metadata.type == DownloadType.IsStopped) break + // metadata.addBytes(read.toLong()) + //} + + + if (metadata.type == DownloadType.IsFailed) { + return@withContext ERROR_CONNECTION_ERROR } if (metadata.type == DownloadType.IsStopped) { @@ -1003,11 +1342,12 @@ object VideoDownloadManager { // note that when failing we don't want to delete the file, // only user interaction has that power + metadata.removeStopListener() metadata.type = DownloadType.IsFailed return@withContext ERROR_CONNECTION_ERROR } finally { fileStream?.closeQuietly() - requestStream?.closeQuietly() + //requestStream?.closeQuietly() metadata.close() } } @@ -1388,20 +1728,28 @@ object VideoDownloadManager { } return suspendSafeApiCall { - downloadThing(context, link, name, folder, "mp4", tryResume, ep.id) { meta -> - main { - createNotification( - context, - source, - link.name, - ep, - meta.type, - meta.bytesDownloaded, - meta.bytesTotal, - notificationCallback - ) - } - } + downloadThing( + context, + link, + name, + folder, + "mp4", + tryResume, + ep.id, + createNotificationCallback = { meta -> + main { + createNotification( + context, + source, + link.name, + ep, + meta.type, + meta.bytesDownloaded, + meta.bytesTotal, + notificationCallback + ) + } + }) }.also { extractorJob.cancel() } ?: ERROR_UNKNOWN } From 61ca0a56bea81ef7bd18f943006df56ff4b8e707 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sat, 19 Aug 2023 21:38:29 +0200 Subject: [PATCH 097/156] Translations update from Hosted Weblate (#546) Co-authored-by: Alexandru Co-authored-by: Alexthegib Co-authored-by: Alexthegib Co-authored-by: Amir Co-authored-by: Astrid Co-authored-by: Carlos Luiz Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com> Co-authored-by: Danilo Co-authored-by: Eryk Michalak Co-authored-by: Ettore Atalan Co-authored-by: Fjuro Co-authored-by: Htet Oo Hlaing Co-authored-by: Imprevisible Co-authored-by: Jan Haider Co-authored-by: Jimuel Mallari Co-authored-by: Massimo Pissarello Co-authored-by: Milo Ivir Co-authored-by: PiterDev Co-authored-by: Rex_sa Co-authored-by: Reza Almanda Co-authored-by: Rudy Tantono Co-authored-by: Skrripy Co-authored-by: dabao1955 Co-authored-by: gallegonovato Co-authored-by: george kitsoukakis Co-authored-by: infoekcz Co-authored-by: tuan041 --- app/src/main/res/values-ar/strings.xml | 3 +- app/src/main/res/values-bp/strings.xml | 79 ++- app/src/main/res/values-cs/strings.xml | 3 +- app/src/main/res/values-es/strings.xml | 3 +- app/src/main/res/values-fil/strings.xml | 2 + app/src/main/res/values-fr/strings.xml | 24 +- app/src/main/res/values-in/strings.xml | 4 +- app/src/main/res/values-it/strings.xml | 3 +- app/src/main/res/values-my/strings.xml | 556 ++++++++++++++++++ app/src/main/res/values-nl/strings.xml | 5 +- app/src/main/res/values-pl/strings.xml | 5 +- app/src/main/res/values-pt/strings.xml | 3 +- app/src/main/res/values-qt/strings.xml | 3 +- app/src/main/res/values-uk/strings.xml | 37 +- app/src/main/res/values-vi/strings.xml | 15 +- fastlane/metadata/android/pt/changelogs/2.txt | 1 + .../metadata/android/pt/full_description.txt | 10 + .../metadata/android/pt/short_description.txt | 1 + fastlane/metadata/android/pt/title.txt | 1 + fastlane/metadata/android/vi/changelogs/2.txt | 1 + .../metadata/android/vi/full_description.txt | 10 + .../metadata/android/vi/short_description.txt | 1 + fastlane/metadata/android/vi/title.txt | 1 + 23 files changed, 734 insertions(+), 37 deletions(-) create mode 100644 app/src/main/res/values-fil/strings.xml create mode 100644 app/src/main/res/values-my/strings.xml create mode 100644 fastlane/metadata/android/pt/changelogs/2.txt create mode 100644 fastlane/metadata/android/pt/full_description.txt create mode 100644 fastlane/metadata/android/pt/short_description.txt create mode 100644 fastlane/metadata/android/pt/title.txt create mode 100644 fastlane/metadata/android/vi/changelogs/2.txt create mode 100644 fastlane/metadata/android/vi/full_description.txt create mode 100644 fastlane/metadata/android/vi/short_description.txt create mode 100644 fastlane/metadata/android/vi/title.txt diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 9b440e6f..987211a5 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -584,4 +584,5 @@ @string/default_subtitles لا توجد اضافة في المستودع المستودع لم يتم العثور عليه، تحقق من العنوان اوجرب شبكة افتراضية خاصة(vpn) - + لقد صوتت بالفعل + \ No newline at end of file diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 38424e56..2a3bdb27 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -10,7 +10,7 @@ %dm Poster - @string/result_poster_img_des + Pôster Episode Poster Main Poster Next Random @@ -66,7 +66,7 @@ Erro Carregando Links Armazenamento Interno Dub - Leg + Sub Deletar Arquivo Assistir Arquivo Retomar Download @@ -257,7 +257,7 @@ Não mostrar de novo Pular essa Atualização Atualizar - Qualidade preferida + Qualidade preferida de reprodução (Wi-fi) Máximo de caracteres do título de vídeos Resolução do player de vídeo Tamanho do buffer do vídeo @@ -428,4 +428,75 @@ Começa o próximo episódio quando o atual termina Ativar NSFW em fornecedores compatíveis Fornecedores - + Reverter + Ações + votou com sucesso + Baixando atualização do aplicativo… + Referencias + Atualizações do App + Tocar com CloudStream + Automaticamente instale todos os plugins não instalados dos repositórios adicionados. + Reproduzir Trailer + Navegador + Copia de Segurança + A Barra de Progresso pode ser usada quando o player estiver oculto + Inscrever + Essa lista está vazia. Tente mudar para outra. + Reproduzir Livestream + Log do Teste + Baixa plugins automaticamente + Selecione o modo para filtrar os plugins baixados + Teste falhou + A Barra de Progresso pode ser usada quando o player estiver visível + Organizar + Sim + Você tem certeza que deseja sair\? + Instalando atualização do aplicativo… + Editar + Perfis + Exibindo Player - procure na Barra de Progresso + remover dos assitidos + Extensões + Alfabética(A => Z) + Abrir com + Selecionar Biblioteca + Passou + Sua biblioteca está vazia :0 +\nEntre numa conta de biblioteca ou adicione Midias para sua biblioteca local. + Qualidade preferida de reprodução (Dados Móveis) + Legado + Biblioteca + Não + Trilhas Sonoras + Votação(Baixa para Alta) + Atualização iniciada + Conteúdo +18 + Ajuda + Processo de configuração de Redo + Não pudemos instalar a nova versão do App + instalador de pacotes + Organizar por + Votação(Alta para Baixa) + Alfabética(Z => A) + Qualidade + Perfil de plano de fundo + Aqui você pode alterar como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais alta, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte e da prioridade da qualidade é a prioridade do vídeo. +\n +\nFonte A: 3 +\nQualidade B: 7 +\nTerá uma prioridade de vídeo combinada de 10. +\n +\nNOTA: Se a soma for 10 ou mais, o Player pulará automaticamente o carregamento quando o link for carregado! + Arquivo de modo de segurança encontrado! +\nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido. + Inscrevel em %d + Episódio %d Lançado + Selecionar padrão + Disinscrevel em %d + Alguns aparelhos não possuem suporte para este pacote de instalação. Tente a opção legada se a atualização não instalar. + Dados móveis + Perfil %d + Atualizando shows inscritos + Player oculto - Procure na barra de progresso + Conteúdo +18 + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index f304199e..165dbbb4 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -576,4 +576,5 @@ V repozitáři nebyly nalezeny žádné doplňky Repozitář nenalezen, zkontrolujte adresu URL a zkuste použít VPN @string/default_subtitles - + Již jste hlasovali + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 42e07c90..6326211e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -552,4 +552,5 @@ @string/default_subtitles No se encontraron complementos en el repositorio Repositorio no encontrado, comprueba la URL y prueba la VPN - + Ya has votado + \ No newline at end of file diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/app/src/main/res/values-fil/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 36c1cf1f..4cc56207 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -530,4 +530,26 @@ Joueur représenté - Montant de la recherche Joueur caché - Montant de la recherche Impossible d\'accéder à GitHub. Activation du proxy jsDelivr… - + Vous avez déjà voté + Désactivé + Ici, vous pouvez modifier la façon dont les sources sont ordonnées. Si une vidéo a une priorité plus élevée, elle apparaîtra plus haut dans la sélection de la source. La somme de la priorité source et de la priorité qualité est la priorité vidéo. +\n +\nSource A : 3 +\nQualité B : 7 +\nLa priorité vidéo combinée sera de 10. +\n +\nREMARQUE : Si la somme est de 10 ou plus, le joueur sautera automatiquement le chargement lorsque ce lien est chargé ! + Aucun plugin trouvé dans ce dossier + Dossier non trouvé, vérifiez l\'url et essayé un VPN + Données mobiles + Définir par défaut + Utiliser + Modifier + Profils + Aide + Profil %d + Wi-Fi + Qualités + L\'interface utilisateur n\'a pas pu être créée correctement. Il s\'agit d\'un bogue majeur qui doit être signalé immédiatement %s + Sélectionnez le mode pour filtrer le téléchargement des plugins + \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 2bd86090..87a01ff9 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -574,4 +574,6 @@ Pilih mode untuk memfilter unduhan plugin Tidak ada plugin yang ditemukan di repositori Repositori tidak ditemukan, periksa URL dan coba VPN - + Kamu sudah voting + \@string/default_subtitles + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index dddc57c4..7cca78ca 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -574,4 +574,5 @@ Seleziona la modalità per filtrare il download dei plugin @string/default_subtitles Disabilita - + Hai già votato + \ No newline at end of file diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml new file mode 100644 index 00000000..cde13ba5 --- /dev/null +++ b/app/src/main/res/values-my/strings.xml @@ -0,0 +1,556 @@ + + + သရုပ်ဆောင်များ: %s + %dရက် %ddနာရီ %ddမိနစ် + %ddနာရီ %ddမိနစ် + %ddမိနစ် + ပိုစတာ + အပိုင်း ပိုစတာ + မိန်း ပိုစတာ + နောက် ကျပန်း + နောက်သို့ + နောက်ခံပုံရိပ်ကို အကြိုကြည့်ရန် + အဆင့်: %.1f + အပ်ဒိတ်အသစ်! +\n%s -> %s + စစ်ထုတ်မှု + %d မိနစ် + CloudStream + CloudStream ဖြင့်ကြည့်ရန် + ပင်မ + ရှာရန် + ရှာရန်… + ရှာရန် %s… + အချက်အလက်မရှိပါ + အခြားရွေးစရာများ + နောက်အပိုင်း + ကဏ္ဍများ + မျှဝေမည် + ဘရောက်ဇာတွင်ဖွင့်ရန် + ဘရောက်ဇာ + မစောင့်တော့ပါ + ကြည့်နေသည် + ကြည့်ပြီး + ကြည့်ခြင်းရပ်ထားသော + ဘာမျှ + လင့်များချိတ်ဆက်ရာတွင်အချို့အယွင်း + ဖုန်း သိုလှောင်ရုံ + စာမှတ်များ စစ်ထုတ်မှု + စာမှတ်များ + ဖယ်ရှားရန် + ကြည့်ရှုမှုအခြေအနေသတ်မှတ်ခြင်း + မိတ္တူကူးရန် + ပိတ်ရန် + ရှင်းလင်းရန် + သိမ်းဆည်းရန် + ကြည့်ရှုမှုအရှိန် + နောက်ခံ အရောင် + ဝင်းဒိုး အရောင် + အစွန်းနားပုံစံ + စာတန်းထိုး အမြင့် + ဖောင့် + ဖောင့် အရွယ်အစား + အမျိုးအစားများအသုံးပြု၍ရှာရန် + %d အက်ပ်ဖန်တီးသူတွေကိုကျေးဇူးတင်ကြောင်းပို့မည် + အလိုအလျောက် ဘာသာစကားရွေးချယ်ခြင်း + ဒေါင်းလုဒ် လုပ်ထားသော ဘာသာစကားများ + မူရင်းပုံစံအတိုင်းပြန်ထားရန်ဖိထားပါ + ဆက်လက်ကြည့်ရှုမည် + ဖယ်ရှားရန် + ပိုမို၍ + \@string/home_play + ဒီဟာကတောရပ်တစ်ခုပါ ဗီပီအန်တစ်ခုသုံးဖို့အကြံပြုပါတယ် + ဖော်ပြချက် + ဇာတ်လမ်းသွား မတွေ့ပါ + ဖော်ပြချက် မရိှပါ + Logcat ပြရန် 🐈 + Log + ရုပ်ပုံထပ် + ကြည့်ရှုမှု စခရင်အရွယ်အစားချိန်ညိှမှု + စာတန်းထိုးများ + ကြည့်ရှုမှုစာတန်းထိုးပြုပြင်စရာများ + Eigengravy လုပ်ဆောင်မှု + ရစ်ရန်ဘယ်ညာဆွဲပါ + သင်ရောက်နေတဲ့နေရာပြောင်းရန်ဘယ်ညာဆွဲပါ + ပြုပြင်စရာရိှပါက ပွတ်ဆွဲပါ + နောက်အပိုင်းကို အလိုအလျောက် ဖွင့်ပါ + ကျော်ရန်နှစ်ချက်နှိပ်ပါ + ရပ်ရန်နှစ်ချက်နှိပ်ပါ + ကျော်လိုသောပမာဏ (စက္ကန့်များ) + ရှေ့သို့ကျော်ရန် သို့ နောက်သို့ရစ်ရန် ဘယ် သို့ ညာ ပေါ်မှာနှစ်ချက်နှိပ်ပါ + ရပ်ရန် အလယ်တွင်နှစ်ချက်နှိပ်ပါ + ဖုန်းအလင်းအမှောင်အတိုင်းသုံးမည် + အက်ပ်ကြည့်ရှုမှုထဲမှာ ဖုန်းအလင်းအမှောင်အတိုင်းသုံးမည် + ကြည့်ရှုမှုတိုးတက်ခြင်းကိုအပ်ဒိတ်လုပ်ပါ + အရန်သိမ်းဖိုင်မှပြန်သိုလှောင်မည် + အရန်သိမ်းမည် + အရန်သိမ်းဖိုင်များရယူပြီး + အရန်သိမ်းဖိုင်မှပြန်သိုလှောငးခြင်မအောင်မြင်ပါ %s + သိုလှောင်ပြီး + သိုလှောင်ရုံခွင့်ပြုချက်မရိှပါ။ပြန်ကြိုးစားပါ။ + အရန်သိမ်းနေစဥ်အချို့အယွင်း %s + လိုက်ဘရီ + အပ်ဒိတ်များနှင့်အရန်သိမ်းဆည်းမှု + နက်နက်ရှိုင်းရှိုင်းရှာခြင်း + သင့်ကိုဝန်ဆောင်မှုပေးသူအလိုက်ရှာဖွေမှုရလဒ်များပေးမည် + ချို့ယွင်းမှုအကြီးစားဖြစ်မှသာဒေတာများပေးပို့ပါ + anime များအတွက်ဖြည့်စွက်အပိုင်းကိုပြရန် + ထွေလာများကိုပြရန် + Kitsu မှ ပိုစတာများကိုပြရန် + အလိုအလျောက် ဖြည့်စွက်လုပ်ဆောင်ချက်များကိုအပ်ဒိတ်တင်ခြင်း + အပိုလုပ်ဆောင်ချက်များကိုစစ်ထုတ်ရန်မုဒ်ရွေးပါ + အက်ပ်အပ်ဒိတ်များပြရန် + အစီအစဥ်ချခြင်းကိုပြန်စမည် + အက်ပ်ထည့်သွင်းခြင်း + အချို့ဖုန်းတွေက အက်ပ်ထည့်သွင်းခြင်းလုပ်ဆောင်ချက်အသစ်ကို မပံ့ပိုးပါဘူး။အကယ်၍အလုပ်မဖြစ်ပါကသမားရိုးကျနည်းလမ်းကိုအသုံးပြုပါ။ + Github + ဤဝန်ဆောင်မှုပေးသူသည် Chromecast ကိုမပံ့ပိုးပါ + လင့်များမတွေ့ပါ + ကလစ်ဘုတ်သို့မိတ္တူကူးပြီး + အပိုင်းကြည့်မည် + အတွဲ + အတွဲမရှိပါ + အပိုင်း + အပိုင်းများ + %d-%d + ဒါကအပြီးဖျက်ခြင်းဖြစ်ပါသည် %s +\nသင်သေချာပါသလား။ + %dမိနစ် +\nကျန်ရိှသည် + ထုတ်လွှင့်နေဆဲ + ထုတ်လွှင့်မှုပြီးဆုံး + အခြေအနေ + ခုနစ် + အဆင့်သတ်မှတ်ချက် + ကြာချိန် + ဆိုဒ် + အကျဥ်းချုပ် + နောက်အစီအစဥ် + စာတန်းထိုးမထည့် + ပုံသေ + \@string/default_subtitles + ကျန်ရှိသော + အက်ပ် + ရုပ်ရှင်များ + ဇာတ်လမ်းတွဲများ + ကာတွန်းများ + Anime + ေတာရပ်များ + မှတ်တမ်းရုပ်ရှင်များ + OVA + အာရှ ဒရာမာများ + တိုက်ရိုက်ထုတ်လွှင်မှုများ + အပြာဗီဒီယိုများ + အခြား + ရုပ်ရှင် + ဇာတ်လမ်းတွဲ + OVA + တောရပ် + မှတ်တမ်းရုပ်ရှင် + အာရှ ဒရာမာ + တိုက်ရိုက်ထုတ်လွှင့်မှု + အပြာဗီဒီယို + ဗီဒီယို + ရင်းမြစ်အချို့အယွင်း + အဝေးထိန်းချုပ်မှုအချို့အယွင်း + တင်ဆက်သူ အချို့အယွင်း + မျှော်လင့်မထားသော အချို့အယွင်း + Chromecast အပိုင်း + Chromecast ဖန်သားပြင် + အက်ပ်တွင်းဖွင့် + ဖွင့်ရန် %s + ဘရောက်ဇာထဲမှာ ဖွင့်ရန် + လင့်ကူးယူရန် + အလိုအလျောက်ဒေါင်းလုဒ် + လင့်များကို ပြန်စစ်ရန် + အရည်အသွေး အမှတ်အသား + နောက်ခံအသံ အမှတ်အသား + စာတန်း အမှတ်အသား + ခေါင်းစဥ် + အပ်ဒိတ်မရှိပါ + အပ်ဒိတ်စစ်ရန် + လော့ခ်ခတ်ရန် + ရင်းမြစ် + OPကိုကျော်ရန် + ဒီအပ်ဒိတ်ကိုကျော်ပါ + အပ်ဒိတ် + ခေါင်းစဥ်အတွက်စာလုံးရေပြည့်ခြင်း + ကြည့်ရှုမှု အရည်အသွေး + ဗီဒီယိုရှေ့ပြေးသိမ်းဆည်းမှုပမာဏ + ကြည့်ရှုမှုဘားမြင်တွေ့ရချိန်တွင်ပြသသောပမာဏ + ဝှက်ထားသောကြည့်ရှုပြီးသောပမာဏ + ဝှက်ထားသည့်အခါ အသုံးပြုသည့် ရှာဖွေမှုပမာဏ + Android TV ကဲ့သို့သော မမ်မိုရီနည်းသော စက်ပစ္စည်းများတွင် သတ်မှတ်နှုန်း အလွန်မြင့်မားပါက ပျက်စီးမှုများ ဖြစ်စေသည်။ + DNS over HTTPS + raw.githubusercontent.com ပရောက်စီ + GitHub သို့ မရောက်ရှိနိုင်ပါ။ jsDelivr ပရောက်စီကို ဖွင့်နေသည်… + ကလုန်း ဆိုဒ် + ဆိုဒ်ကိုဖယ်ရှားရန် + မတူညီသော URL တစ်ခုဖြင့် ရှိပြီးသား ဝဘ်ဆိုက်တစ်ခု၏ ပုံတူတစ်ခုကို ထည့်ပါ + ဒေါင်းလုဒ်လမ်းကြောင်း + NGINX ဆာဗာ URL + Dubbed/Subbed Anime ကိုပြသပါ + မျက်နှာပြင်နှင့် အံကိုက် + ဆန့်သည် + ချဲ့သည် + ရှင်းလင်းချက် + ISP ရှောင်လွှဲမှုများ + လင့်များ + အက်ပ်အပ်ဒိတ်များ + Extensions + ဆောင်ရွက်ချက်များ + Cache + Android တီဗွီ + စာတန်းထိုးများ + ပုံသေများ + ပုံပန်းသဏ္ဌာန် + အထွေထွေ + ပင်မစာမျက်နှာမှာကျပန်းခလုတ်ကိုပြပါ + %s အပိုင်း %d + အပိုင်း %d ထုတ်လွှင့်ပြသမည် + ပိုစတာ + ပံ့ပိုးပေးသောဝန်ဆောင်မှုပြောင်းရန် + အရှိန် (%.2fx) + ဒေါင်းလုဒ်များ + ပြင်ဆင်ရန် + ခဏစောင့်ပါ… + ကြည့်ဆဲ + ကြည့်ရန် + ပြန်ကြည့်နေသည် + စာတန်းထိုး + ရုပ်ရှင်ကြည့်မည် + ထွေလာ ကြည့်မည် + လိုက်ခ် ကြည့်မည် + တောရပ် ကြည့်မည် + ရင်းမြစ်များ + ချိတ်ဆက်မှုပြန်ကြိုးစား… + နောက်သို့ + အပိုင်း ကြည့်မည် + ဒေါင်းလုဒ် + ဒေါင်းလုဒ် လုပ်ပြီး + ဒေါင်းလုဒ် လုပ်နေသည် + ဒေါင်းလုဒ် ရပ်ထား + ဒေါင်းလုဒ်စတင် + ဒေါင်းလုဒ် မအောင်မြင် + ဒေါင်းလုဒ် ပယ်ဖျက်ပြီး + ဒေါင်းလုဒ်ပြီးစီး + အပ်ဒိတ်စတင် + တိုက်ရိုက်ကြည့်မည် + နောက်ခံအသံ + ရှာဖွေမှုရလဒ်များတွင်ရွေးချယ်ထားသောဗီဒီယိုအရည်အသွေးကိုဝှက်ထားရန် + စာတန်းထိုး + ဖိုင်ဖျက်ရန် + ဖိုင်ကို ဖွင့်ရန် + ဒေါင်းလုဒ် ဆက်လုပ်ရန် + ဒေါင်းလုဒ် ရပ်ရန် + အလိုအလျောက်အက်ပ်ချို့ယွင်းချက်ပေးပို့ခြင်းကိုပိတ်မည် + ပိုမို၍ + ပ့ံပိုးပေးသောဝန်ဆောင်မှုများအသုံးပြု၍ရှာရန် + ဝုက်ရန် + ကျေးဇူးတင်ကြောင်းမပို့ရသေး + ကြည့်မည် + အချက်အလက် + စာတန်းထိုး ဘာသာစကား + ဒီမှာနေရာချခြင်းဖြင့်ဖောင့်များကိုသွင်းပါ %s + အတည်ပြု + ပယ်ဖျက်ရန် + စာတန်းထိုး ပြုပြင်ခြင်း + စာသား အရောင် + အနားကွပ် အရောင် + ဒီပံ့ပိုးမှုကောင်းမွန်စွာအလုပ်လုပ်ရန်ဗီပီအန်တစ်ခုလိုနိုင်ပါတယ် + အသေးစိတ်အချက်အလက်များပြမထားပါ။ဝဘ်ဆိုဒ်ပေါ်မှာမရှိလျှင်ကြည့်ရှု၍မရနိုင်ပါ။ + ဖြည့်စွက်လုပ်ဆောင်ချက်များကို အလိုအလျောက်ဒေါင်းလုဒ်လုပ်ခြင်း + ရီပိုစစ်ထရီများမှမထည့်သွင်းရသေးသောဖြည့်စွက်လုပ်ဆောင်ချက်များအားလုံးကိုထည့်သွင်းပါ။ + ပြန်ကြည့်ခြင်းကိုအသေးစား ကြည့်ရှုမှုတွင်ဆက်ပြပါ + အနက်ရောင်ဘောင်များကို ဖယ်ရှားရန် + အက်ပ်ထဲဝင်လိုက်သည့်နှင့်အက်ပ်အပ်ဒိတ်ကိုစစ်ဆေးပါ။ + Chromecast စာတန်းထိုးများ + Chromecast စာတန်းထိုး ပြုပြင်ရန် + ကြည့်ရှုမှုပုံစံထဲမှာအရိှန်ရွေးစရာတစ်ခုထည့်ရန် + အသံအတိုးအကျယ်နှင့်အလင်းအမှောင်များကိုချိန်ညိှရန် ဘယ် သို့ ညာ ဘက်တွင် အပေါ်အောက်ဆွဲပါ + ယခုကြည့်နေသောအပိုင်းပြီးပါကနောက်အပိုင်းကိုဖွင့်ပါ + သင့်၏အပိုင်းကြည်ရှုမှုရောက်ရှိနေရာကိုအလိုအလျောက်သိမ်းဆည်းပါ + ရှာရန် + အကောင့်များ + အချက်အလက် + ဒေတာများမပို့ရန် + ကြည့်ရှုပြီးသောအချိန်ပမာဏ + Android TV ကဲ့သို့သော သိုလှောင်မှုနေရာနည်းပါးသော စက်ပစ္စည်းများတွင် အလွန်မြင့်မားစွာ သတ်မှတ်ပါက ပြဿနာများ ဖြစ်လာနိုင်သည်။ + ISP ပိတ်ဆို့ခြင်းကို ကျော်လွှားရန်အတွက် အသုံးဝင်သည် + jsDelivr ကို အသုံးပြု၍ GitHub ပိတ်ဆို့ခြင်းကို ကျော်ဖြတ်သည်။ အပ်ဒိတ်များကို ရက်အနည်းငယ်ကြာအောင် နှောင့်နှေးစေနိုင်သည်။ + အရန်သိမ်းထားသော + လက်ဟန်များ + ကြည့်ရှုမှုလုပ်ဆောင်ချက်များ + အပြင်အဆင် + လုပ်ဆောင်ချက်များ + ကျပန်းခလုတ် + ရှေ့ပြေးအပ်ဒိတ်များကိုထည့်သွင်းပါ + ပုံမှန်အပ်ဒိတ်များအစား ရှေ့ပြေးအက်ဒိတ်များကိုရှာပါ + တူညီသောအက်ပ်ရေးသားသူများ၏ ဝတ္ထုရှည်များဖတ်နိုင်သည့် အက်ပ် + တူညီသောအက်ပ်ရေးသားသူများ၏ Anime အက်ပ် + Discord ကိုဝင်ရန် + အက်ပ်ရေးသားသူများထံ ကျေးဇူးတင်စာပို့မည် + ပေးခဲ့သောစာအရေအတွက် + အက်ပ်ဘာသာစကား + မူလအခြေအနေများကိုပြန်ထားပါ + စိတ်မကောင်းပါ။အက်ပ်ရပ်တန့်သွားပါတယ်။အမည်မဖော်ထားတဲ့တင်ပြချက်ကို အက်ပ်ရေးသားသူများထံ ပို့မှာဖြစ်ပါတယ် + %s %d%s + အတွဲ + %d %s + အပိုင်း + အပိုင်းများမတွေ့ပါ + ဖိုင်ကိုဖျက်ရန် + ဖျက်ရန် + ရပ်ရန် + စရန် + မအောင်မြင်ပါ + ကျော်ဖြတ်ပြီး + ကြည့်လက်စ + -30 + +30 + အသုံးပြုပြီးသော + ကာတွန်း + Anime + ဒေါင်းလုဒ် အချို့အယွင်း၊သိုလှောင်ရုံခွင့်ပြုချက်တွေကိုစစ်ဆေးပါ + ဒေါင်းလုဒ် ကြေးမုံ + စာတန်းထိုးများကို ဒေါင်းလုဒ်လုပ်ရန် + ပိုစတာပေါ်ရှိ UI အစိတ်အပိုင်းများကို ပြောင်းပါ + ပြန်ညိှ + နောက်ထပ်မပြရန် + ဝိုင်ဖိုင်ဖြင့်ကြည့်စဥ်ဗီဒီယိုအရည်အသွေး + မိုဘိုင်းဒေတာဖြင့်ကြည့်စဥ်ဗီဒီယိုအရည်အသွေး + ဗီဒီယိုရှေ့ပြေးသိမ်းဆည်းမှုအကွာအဝေး + ဗီဒီယိုcacheအများ + ဗီဒီယို cache နှင့် ရုပ်ပုံ cache များကိူရှင်းလင်းရန် + ပံ့ပိုးပေးထားသည့် ဝန်ဆောင်မှုများပေါ်တွင် အပြာဗီဒီယို ကို ဖွင့်ပါ + ဝန်ဆောင်မှုပံ့ပိုးသူဘာသာစကား + အက်ပ်အပြင်အဆင် + ဦးစားပေးမီဒီယာ + အက်ပ် အပြင်အဆင် + ပိုစတာခေါင်းစဉ်တည်နေရာ + ခေါင်းစဉ်ကို ပိုစတာအောက်မှာ ထားပါ + ဖုန်းအပြင်အဆင် + အင်မြူလိတ်တာ အပြင်အဆင် + အဓိကအရောင် + အကြံပြုသည် + ဒီဗွီဒီ + 4K + ထုတ်လွှင့်ရန် လင့်ခ် နှင့်ချိတ်ပါ + ရည်ညွှန်းသည် + ရှေ့သို့ + နောက်သို့ + အပ်ဒိတ်လုပ်ပြီး %d ဖြည့်စွက်များ + ဒေါင်းလုဒ်မလုပ်ရသေး: %d + ဝဘ်ဘရောက်ဇာ + အက်ပ်မတွေ့ပါ + ဘာသာစကားအားလုံး + ကျော်ရန် %s + အစမှပြန်စ + ရောထားသောအဆုံးပိုင်း + ရောထားသောအစပိုင်း + ခရက်ဒစ်များ + အစ + သေချာသည် + သမားရိုးကျ + ထည့်သွင်းသူ + ထွက်ချိန်တွင် အက်ပ်ကို အပ်ဒိတ်လုပ်ပါမည် + CloudStream တွင် မူရင်းအတိုင်း ထည့်သွင်းထားသည့်ဆိုက်များ မရှိပါ။ ရီပိုစစ်ထရီများ မှ ဆိုဒ် များကို ထည့်သွင်းရန်လိုအပ်သည်။ +\n +\nSky UK Limited မှ ဦးနှောက်မဲ့ DMCA ကို ဖယ်ရှားလိုက်ခြင်းကြောင့် 🤮 ကျွန်ုပ်တို့သည် ရီပိုစစ်ထရီဆိုဒ်ကို အက်ပ်တွင် ချိတ်ဆက်၍မရပါ။ +\n +\nကျွန်ုပ်တို့၏ Discord တွင်ပါဝင်ပါ သို့မဟုတ် အွန်လိုင်းတွင်ရှာဖွေပါ။ + အခြားသူများ၏ရီပိုစစ်ထရီများကိုရှာဖွေမည် + အသံများ + အသံဖိုင်များ + ဗီဒီယိုအသံဖိုင်များ + ပြန်စတင်ချိန်မှာအသုံးပြုပါ + ရပ်ရန် + လုံခြုံသောမုဖွင့်ရန် + ဖုန်းတွင်းကြည့်ရှုမှု + ဦးစားပေး ဗီဒီယိုဖွင့်စက် + ပြဿနာဖြစ်စေသည့်အရာကို သင်ရှာဖွေရာတွင် အထောက်အကူဖြစ်စေရန်အတွက် ပျက်စီးမှုတစ်ခုကြောင့် အဆက်များအားလုံးကို ပိတ်ထားသည်။ + အဆင့်သတ်မှတ်ချက်များ: %s + ပျက်စီးမှုအချက်အလက်ကို ကြည့်ပါ + ဖော်ပြချက် + ဗားရှင်း + အခြေအနေ + အရွယ်အစား + ရေးသားသူများ + HLS ဖွင့်စဥ် + ကြည့်ရှုခဲ့သည်များ + အက်ပ်၏ ဗားရှင်းအသစ်ကို ထည့်သွင်း၍မရပါ + အစပိုင်း/အဆုံးပိုင်းအတွက် ကျော်နိုင်သော ပေါ့ပ်အပ်များကို ပြပါ + စာသားအလွန်များသဖြင့်ကလစ်ဘုတ်တွင် သိမ်းဆည်း၍မရပါ။ + ပံ့ပိုးပေးသူများ + အပြင်အဆင် + အလိုအလျောက် + တီဗွီအပြင်အဆင် + သင့်စကားဝှက် + သင့်ယူဇာနိမ်း + သင့်အီးမေးလ် လိပ်စာ + 127.0.0.1 + သင့်ဆိုဒ် + example.com + အရိပ် + ထမြောက်မှု + စာတန်းထိုးများ ထပ်တူပြုရန် + စာတန်းထိုးများ အလွန်စောနေပါက %d ms ဒီဟာကိုသုံးပါ + The quick brown fox jumps over the lazy dog + တင်ပြီး %s + ဖိုင်မှတင်သွင်းပြီး + ရုံရိုက် + အကြည် + အကြည် + UHD + HDR + SDR + Web + WP + SD + ကြည့်ရှုမှု + ပိုစတာပုံရိပ် + စာတန်းထိုးများမှ bloat ကိုဖယ်ရှားပါ + နှစ်သက်ရာ မီဒီယာဘာသာစကားဖြင့် စစ်ထုတ်ပါ + အပိုများ + ဒေတာမမှန်ပါ + URL မမှန်ပါ + အချို့အယွင်း + စာတန်းထိုးများမှ ပိတ်ထားသော စာတန်းများကို ဖယ်ရှားပါ + ထွေလာ + ဖြည့်စွက်များ + ရီပိုစစ်ထရီ ဖြည့်စွက်များအားလုံးကိုဖျက်မည်ဖြစ်သည် + ရီပိုစစ်ထရီ ကိုဖျက်ရန် + ဤရီပိုစစ်ထရီမှ ဖြည့်စွက်များအားလုံးကို ဒေါင်းလုဒ်လုပ်မှာလား\? + %s (ပိတ်ပြီး) + ထောက်ပံ့ထားသော + ဘာသာစကား + အဆက်များကိုအရင်သွင်းပါ + VLC + MPV + ဝဘ်ထဲတွင်ဖွင့်ရန် + အစပိုင်း + အဆုံးပိုင်း + ကြည့်ရှုခဲ့သည်များကိုရှင်းရန် + ကြည့်ပြီးသည်မှဖယ်ရှားရန် + သင်ထွက်ရန်သေချာပြီလား + မသေချာပါ + အက်ပ်အပ်ဒိတ်အားဒေါင်းလုဒ်လုပ်နေသည်… + အက်ပ်အပ်ဒိတ်အားသွင်းနေသည်… + အပ်ဒိတ်ဖြစ်မှု (အသစ် မှ အဟောင်း) + သင့်လိုက်ဘရီသည် ဗလာဖြစ်နေသည် :( +\nအကောင့်ဝင်ပါ သို့မဟုတ် သင့်ဖုန်းလိုက်ဘရီတွင် ကြည့်စရာများထည့်ပါ။ + သုံးရန် + တည်းဖြတ်ရန် + အရည်အသွေးများ + စာတန်းထိုး ကုဒ်လုပ်ခြင်း + ပံ့ပိုးပေးသူ စစ်ဆေးမှု + %s %s + အကောင့်ဝင်မည် + အကောင့်ပြောင်းမည် + ချိန်ညိှခြင်း + /%d + အင်တာနက်မှ တင်သွင်းမည် + နောက်ခံ + အကောင့်မဝင်ရောက်နိုင်ပါ %s + ဘာမျှ + အနည်းဆုံး + အနားကွပ် + ကျပန်း + ချုံ့ပြီး + 1000 ms + စာတန်းထိုး ကြန့်ကြာမှု + စာတန်းထိုးများအလွန်နောက်ကျနေပါက %d ms ဒီဟာကိုသုံးပါ + စာတန်းထိုး ကြန့်ကြာမှု သတ်မှတ်ထားခြင်းမရှိ + ရုံရိုက် + ရုံရိုက် + TC + အရည်အသွေး + အစီအစဥ်ချခြင်းကိုကျော်မည် + ချို့ယွင်းမှုသတင်းပေးပို့ခြင်း + ဘာတွေကြည့်ချင်လဲ + ပြီးပြီ + အဆက်များ + မသွင်းနိုင်ပါ %s + သင့်စက်ပစ္စည်းနှင့် ကိုက်ညီစေရန် အက်ပ်၏အသွင်အပြင်ကို ပြောင်းလဲပါ + ရီပိုစစ်ထရီထည့်ရန် + အသက်ပြည့်ပြီးသူများသာ + ရီပိုစစ်ထရီအမည် + ရီပိုစစ်ထရီ URL + ဖြည့်စွက်များ ထည့်ပြီး + ဤဘာသာစကားများဖြင့် ဗီဒီယိုများကို ကြည့်ရှုပါ + ဖြည့်စွက်များ ဒေါင်းလုဒ်လုပ်ပြီး + ဖြည့်စွက်များဖျက်ပြီး + ဒေါင်းလုဒ်လုပ်ခြင်း စတင်သည် %d %s… + ဒေါင်းလုဒ်လုပ်ပြီး %d %s + အားလုံး %s ဒေါင်းလုဒ်လုပ်ပြီးသား + ရီပိုစစ်ထရီထဲတွင်ဖြည့်စွက်များမတွေ့ပါ + ရီပိုစစ်ထရီမတွေ့ပါ၊URLကိုပြန်စစ်ပြီးဗီပီအန်ဖြင့်ကြိုးစားကြည့်ပါ + အသုတ်လိုက် ဒေါင်းလုဒ် + ဖြည့်စွက် + သင်အသုံးပြုလိုသောဆိုက်များစာရင်းကို ဒေါင်းလုဒ်လုပ်ပါ + ဒေါင်းလုဒ်လုပ်ပြီး: %d + ပိတ်ပြီး: %d + ဘာသာစကားကုဒ် (en) + အကောင့် + အကောင့်ထွက်မည် + အကောင့်ထည့်မည် + အကောင့်ဖွင့်မည် + စောင့်ကြည့်ခြင်းထည့်မည် + ထည့်ပြီး %s + အဆင့်သတ်မှတ်ထားပြီး + %d / 10 + /\?\? + %s ချိတ်ဆက်ပြီး + ပိတ်ပါ + ပုံမှန် + အားလုံး + အပြည့် + ဖိုင်ဒေါင်းလုဒ်လုပ်ပြီး + အဓိက + ထောက်ပံ့သည် + ရင်းမြစ် + မကြာမီလာမည်… + TS + Blu-ray + အရည်အသွေးနှင့်ခေါင်းစဥ် + ခေါင်းစဥ် + အိုင်ဒီမမှန်ပါ + အများမြင်နိုင်သော + စာတန်းထိုးအားလုံးကို စာလုံးအကြီးပြောင်းပါ + ပြန်စတင်မည် + ကြည့်ပြီးအဖြစ်မှတ်ရန် + အစီအစဥ်ချမှု + အစီအစဥ် + အဆင့်သတ်မှတ်ချက် (အမြင့်ဆုံးမှအနိမ့်ဆုံးသို့) + အဆင့်သတ်မှတ်ချက် (အနိမ့်ဆုံး မှ အမြင့်ဆုံးသို့) + အပ်ဒိတ်ဖြစ်မှု (အဟောင် မှ အသစ်) + အက္ခရာစဥ်လိုက် (A မှ Z) + အက္ခရာစဥ်လိုက် (Z မှ A) + ပြောင်းပြန် + စာရင်းသွင်းထားသောရှိုးများကိုအပ်ဒိတ်လုပ်နေသည် + စာရင်းသွင်းပြီး + စာရင်းသွင်းပြီး %s + စာရင်းသွင်းမှုပယ်ဖျက်ပြီး %s + ဤစာရင်းသည် ဗလာဖြစ်နေသည်။ အခြားတစ်ခုသို့ ပြောင်းကြည့်ပါ။ + Safe mode ဖိုင်ကို တွေ့ရှိခဲ့သည်။ +\nဖိုင်ကိုမဖယ်ရှားမချင်း စတင်ဖွင့်စတွင် မည်သည့် extension များကိုမျှ မတင်ပါ။ + အပိုင်းသစ် %d ထွက်ပြီ + ပရိုဖိုင် %d + ဝိုင်ဖိုင် + မိုဘိုင်းဒေတာ + ပုံသေထားရန် + ပရိုဖိုင်များ + အကူအညီ + ဤနေရာတွင် သင်သည် အရင်းအမြစ်များကို မည်ကဲ့သို့ အစီအစဥ်ချမည်ကို ပြောင်းလဲနိုင်သည်။ ဗီဒီယိုတစ်ခုတွင် ပိုမိုဦးစားပေးပါက ရင်းမြစ်ရွေးချယ်မှုတွင် ပိုမိုမြင့်မားလာမည်ဖြစ်သည်။ အရင်းအမြစ် ဦးစားပေးနှင့် အရည်အသွေး ဦးစားပေး၏ ပေါင်းစုသည် ဗီဒီယို ဦးစားပေးဖြစ်သည်။ +\n +\nအရင်းအမြစ် A: 3 +\nအရည်အသွေး B: 7 +\nပေါင်းစပ်ဗီဒီယို ဦးစားပေး 10 ခု ရှိပါမည်။ +\n +\nမှတ်ချက်- ပေါင်းလဒ်သည် 10 သို့မဟုတ် ထို့ထက်ပိုပါက ထိုလင့်ခ်ကို တင်သည့်အခါ ဗီဒီယိုဖွင့်စက်သည် အလိုအလျောက် ဒေါင်းလုဒ်ကို ကျော်သွားမည်ဖြစ်သည် + ပရိုဖိုင်နောက်ခံ + UI ကို မှန်ကန်စွာ ဖန်တီး၍မရပါ၊ ၎င်းသည် အဓိက ချို့ယွင်းချက်တစ်ခုဖြစ်ပြီး ချက်ချင်းသတင်းပို့သင့်သည်။ %s + သင်နဂိုတည်းကသတ်မှတ်ပြီး + လိုက်ဘရီရွေးချယ်ရန် + ဖြင့်ဖွင့်မည် + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 5f60ac14..c33bf107 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -573,5 +573,6 @@ Selecteer een modus om het downloaden van plug-ins te filteren Uitzetten De gebruikersinterface kon niet correct worden gemaakt, dit is een ERNSTIG PROBLEEM en moet onmiddellijk gerapporteerd worden %s - @string/default_subtitles - + \@string/default_subtitles + Je hebt al gestemd + \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 6db36065..38c56f0d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -553,4 +553,7 @@ Wybierz tryb filtrowania pobieranych rozszerzeń Wyłączać @string/default_subtitles - + Nie znaleziono żadnych wtyczek w repozytorium + Już oddano głos + Nie znaleziono tego repozytorium, sprawdź adres URL lub spróbuj połączyć się przez VPN + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b2504e84..75d1ddbc 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -552,4 +552,5 @@ Desativar Não foram encontrados plugins no repositório Repositório não encontrado, verifique o URL e tente a VPN - + Você já votou + \ No newline at end of file diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index f763d795..cce4e7d3 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -248,4 +248,5 @@ aoaaaaaoooghhh oooooh uuaagh @string/home_play - + oouuhhh ahhooo-ahah + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 2c5d4197..14b2334f 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -80,14 +80,14 @@ Сюжет не знайдено Опис не знайдено Показати Logcat 🐈 - Продовження відтворення в мініатюрному плеєрі поверх інших застосунків + Продовжує відтворення в мініатюрному плеєрі поверх інших застосунків Прибирає чорні рамки Субтитри Субтитри Chromecast Налаштування субтитрів Chromecast Режим Eigengravy Проведіть пальцем, щоб змінити налаштування - Проведіть пальцем вгору або вниз, ліворуч або праворуч, щоб змінити яскравість чи гучність + Проведіть вгору або вниз з лівого або правого боку, щоб змінити яскравість чи гучність Відтворювати наступний епізод після закінчення поточного Головна CloudStream @@ -121,8 +121,8 @@ Колір тексту Колір контуру Автовідтворення наступного епізоду - Проведіть пальцем з боку в бік, щоб керувати своїм положенням у відео - %d Бананів для розробників + Проведіть з боку в бік, щоб керувати своїм положенням у відео + %d бананів для розробників Кнопка зміни розміру плеєра @string/home_play Для коректної роботи цього постачальника може знадобитися VPN @@ -133,7 +133,7 @@ Проведіть пальцем, щоб перемотати Двічі торкніться, щоб перемотати Двічі торкніться для паузи - Крок перемотки (Секунди) + Крок перемотки (секунди) Натисніть двічі посередині, щоб призупинити відтворення відео Використовувати яскравість системи Оновити прогрес перегляду @@ -150,8 +150,8 @@ Надає результати пошуку, розділені за постачальниками Надсилає дані лише про збої Не надсилає даних - Показати заповнюючий епізод для аніме - Показати трейлери + Показувати філери до аніме + Показувати трейлери Приховати вибрану якість відео в результатах пошуку Автоматичне завантаження плагінів Показувати оновлення застосунку @@ -214,12 +214,12 @@ Завантажити дзеркало Перевірити наявність оновлень Заблокувати - Пропустити OP + Пропускати OP Не показувати знову Оновити Бажана якість перегляду (WiFi) Заголовок - Перемикання елементів інтерфейсу на плакаті + Перемикання елементів інтерфейсу на постері Оновлення не знайдено Двічі торкніться праворуч або ліворуч, щоб перемотати відео вперед або назад Використовуйте системну яскравість у плеєрі замість темної накладки @@ -227,10 +227,10 @@ Торренти Автоматична синхронізація прогресу поточного епізоду Відсутні дозволи на зберігання. Будь ласка, спробуйте ще раз. - Показати постери від Kitsu + Показувати постери від Kitsu Автоматичне оновлення плагінів Автоматично встановлювати всі ще не встановлені плагіни з доданих репозиторіїв. - Автоматично шукати нові оновлення після запуску застосунку. + Автоматично шукає нові оновлення після запуску застосунку. Оновлення до бета-версій Посилання скопійовано в буфер обміну Деякі телефони не підтримують новий інсталятор пакетів. Спробуйте стару версію, якщо оновлення не встановлюються. @@ -354,7 +354,7 @@ DNS через HTTPS Шлях завантаження Додайте клон існуючого сайту, з іншою URL-адресою - Відображати мітку Дубляж/Субтитри в аніме + Відображати мітку Дубляж/Субтитри до аніме Застереження Розширення Дії @@ -382,7 +382,7 @@ Підтримка Фон Blu-ray - Видалити закриті титри з субтитрів + Видаляти закриті титри з субтитрів DVD Недійсні дані Фільтрувати за бажаною мовою медіа @@ -400,7 +400,7 @@ HD TS TC - Видалити роздуття субтитрів + Видаляти роздуття субтитрів Referer Далі Дивіться відео на цих мовах @@ -451,8 +451,8 @@ Вбудований плеєр VLC MPV - Відтворення веб-відео - Веб-браузер + Web Video Cast + Веббраузер Ендінґ Коротке повторення Пропустити %s @@ -462,7 +462,7 @@ Вступ Очистити історію Історія - Показувати спливаючі вікна для опенінґу/ендінґу + Показує спливаюче вікно для пропуску опенінґу/ендінґу Забагато тексту. Не вдалося зберегти в буфер обміну. Позначити як переглянуте Ви впевнені що хочете вийти\? @@ -552,4 +552,5 @@ @string/default_subtitles Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії - + Ви вже проголосували + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 4b394227..f7abd6db 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -267,7 +267,7 @@ Cập nhật Chất lượng xem ưu tiên (WiFi) Kí tự tối đa trên tiêu đề - Độ phân giải trình phát video + Nội dung trình phát video Kích thước bộ nhớ đệm video Thời lượng bộ nhớ đệm Lưu bộ nhớ đệm video trên ổ cứng @@ -380,8 +380,8 @@ Web Ảnh áp phích Trình phát - Độ phân giải và Tiêu đề - Tiêu đề + Độ phân giải và Tên nguồn + Tên nguồn Độ phân giải Id không hợp lệ Lỗi dữ liệu @@ -561,4 +561,11 @@ \n \nLƯU Ý: Nếu tổng là 10 hoặc nhiều hơn, trình phát sẽ tự động bỏ tải khi liên kết đó được tải! Các phẩm chất - + Bạn đã bình chọn + Vô hiệu hoá + Không tìm thấy tiện ích, hãy kiểm tra URL và thử VPN + Không tìm thấy plugin + Không thể khởi tạo UI, đây là một LỖI LỚN và cần được báo cáo ngay lập tức tới %s + Chọn chế độ để lọc plugin tải xuống + \@string/default_subtitles + \ No newline at end of file diff --git a/fastlane/metadata/android/pt/changelogs/2.txt b/fastlane/metadata/android/pt/changelogs/2.txt new file mode 100644 index 00000000..1153e632 --- /dev/null +++ b/fastlane/metadata/android/pt/changelogs/2.txt @@ -0,0 +1 @@ +- Adicionado o registo de alterações! diff --git a/fastlane/metadata/android/pt/full_description.txt b/fastlane/metadata/android/pt/full_description.txt new file mode 100644 index 00000000..48bf36ce --- /dev/null +++ b/fastlane/metadata/android/pt/full_description.txt @@ -0,0 +1,10 @@ +O CloudStream-3 permite-lhe transmitir e descarregar filmes, séries de TV e anime. + +A aplicação é fornecida sem quaisquer anúncios e análises e +suporta vários sites de trailers e filmes, e muito mais, por exemplo + +Marcadores + +Downloads de legendas + +Suporte para Chromecast diff --git a/fastlane/metadata/android/pt/short_description.txt b/fastlane/metadata/android/pt/short_description.txt new file mode 100644 index 00000000..d0392f34 --- /dev/null +++ b/fastlane/metadata/android/pt/short_description.txt @@ -0,0 +1 @@ +Transmita e transfira filmes, séries de TV e anime. diff --git a/fastlane/metadata/android/pt/title.txt b/fastlane/metadata/android/pt/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/pt/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/vi/changelogs/2.txt b/fastlane/metadata/android/vi/changelogs/2.txt new file mode 100644 index 00000000..e03e458e --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/2.txt @@ -0,0 +1 @@ +- Đã thêm Nhật ký thay đổi! diff --git a/fastlane/metadata/android/vi/full_description.txt b/fastlane/metadata/android/vi/full_description.txt new file mode 100644 index 00000000..90ea7ab7 --- /dev/null +++ b/fastlane/metadata/android/vi/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 cho phép bạn xem và tải xuống phim lẻ, phim bộ và anime. + +Ứng dụng không có quảng cáo hay và phân tích nào, +đồng thời hỗ trợ nhiều trang web xem phim, v.v. + +Đánh dấu + +Tải phụ đề + +Hỗ trợ Chromecast diff --git a/fastlane/metadata/android/vi/short_description.txt b/fastlane/metadata/android/vi/short_description.txt new file mode 100644 index 00000000..e4e20bd5 --- /dev/null +++ b/fastlane/metadata/android/vi/short_description.txt @@ -0,0 +1 @@ +Xem và tải xuống phim lẻ, phim bộ và anime. diff --git a/fastlane/metadata/android/vi/title.txt b/fastlane/metadata/android/vi/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/vi/title.txt @@ -0,0 +1 @@ +CloudStream From 6948bf807373d349844701c4e2eb7e3a438179e0 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Sat, 19 Aug 2023 19:38:45 +0000 Subject: [PATCH 098/156] chore(locales): fix locale issues --- .../lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 2 ++ app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-bp/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fil/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 4 ++-- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-my/strings.xml | 6 +++--- app/src/main/res/values-nl/strings.xml | 4 ++-- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-qt/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 4 ++-- 16 files changed, 22 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index d76eba1e..2c81ad1f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -69,6 +69,7 @@ val appLanguages = arrayListOf( Triple("", "Esperanto", "eo"), Triple("", "español", "es"), Triple("", "فارسی", "fa"), + Triple("", "fil", "fil"), Triple("", "français", "fr"), Triple("", "galego", "gl"), Triple("", "हिन्दी", "hi"), @@ -84,6 +85,7 @@ val appLanguages = arrayListOf( Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), Triple("", "bahasa Melayu", "ms"), + Triple("", "ဗမာစာ", "my"), Triple("", "Nederlands", "nl"), Triple("", "norsk nynorsk", "nn"), Triple("", "norsk bokmål", "no"), diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 987211a5..0c11f7e9 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -585,4 +585,4 @@ لا توجد اضافة في المستودع المستودع لم يتم العثور عليه، تحقق من العنوان اوجرب شبكة افتراضية خاصة(vpn) لقد صوتت بالفعل - \ No newline at end of file + diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 2a3bdb27..425293e4 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -499,4 +499,4 @@ Atualizando shows inscritos Player oculto - Procure na barra de progresso Conteúdo +18 - \ No newline at end of file + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 165dbbb4..46bd860d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -577,4 +577,4 @@ Repozitář nenalezen, zkontrolujte adresu URL a zkuste použít VPN @string/default_subtitles Již jste hlasovali - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 6326211e..8e9f9c2c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -553,4 +553,4 @@ No se encontraron complementos en el repositorio Repositorio no encontrado, comprueba la URL y prueba la VPN Ya has votado - \ No newline at end of file + diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index a6b3daec..42eba3cc 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4cc56207..208e6140 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -552,4 +552,4 @@ Qualités L\'interface utilisateur n\'a pas pu être créée correctement. Il s\'agit d\'un bogue majeur qui doit être signalé immédiatement %s Sélectionnez le mode pour filtrer le téléchargement des plugins - \ No newline at end of file + diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 87a01ff9..d514bcc4 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -575,5 +575,5 @@ Tidak ada plugin yang ditemukan di repositori Repositori tidak ditemukan, periksa URL dan coba VPN Kamu sudah voting - \@string/default_subtitles - \ No newline at end of file + @string/default_subtitles + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 7cca78ca..0c34e89a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -575,4 +575,4 @@ @string/default_subtitles Disabilita Hai già votato - \ No newline at end of file + diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index cde13ba5..0cb44373 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -58,7 +58,7 @@ ဆက်လက်ကြည့်ရှုမည် ဖယ်ရှားရန် ပိုမို၍ - \@string/home_play + @string/home_play ဒီဟာကတောရပ်တစ်ခုပါ ဗီပီအန်တစ်ခုသုံးဖို့အကြံပြုပါတယ် ဖော်ပြချက် ဇာတ်လမ်းသွား မတွေ့ပါ @@ -128,7 +128,7 @@ နောက်အစီအစဥ် စာတန်းထိုးမထည့် ပုံသေ - \@string/default_subtitles + @string/default_subtitles ကျန်ရှိသော အက်ပ် ရုပ်ရှင်များ @@ -553,4 +553,4 @@ သင်နဂိုတည်းကသတ်မှတ်ပြီး လိုက်ဘရီရွေးချယ်ရန် ဖြင့်ဖွင့်မည် - \ No newline at end of file + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index c33bf107..d19726fd 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -573,6 +573,6 @@ Selecteer een modus om het downloaden van plug-ins te filteren Uitzetten De gebruikersinterface kon niet correct worden gemaakt, dit is een ERNSTIG PROBLEEM en moet onmiddellijk gerapporteerd worden %s - \@string/default_subtitles + @string/default_subtitles Je hebt al gestemd - \ No newline at end of file + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 38c56f0d..a170d610 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -556,4 +556,4 @@ Nie znaleziono żadnych wtyczek w repozytorium Już oddano głos Nie znaleziono tego repozytorium, sprawdź adres URL lub spróbuj połączyć się przez VPN - \ No newline at end of file + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 75d1ddbc..908ddb0d 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -553,4 +553,4 @@ Não foram encontrados plugins no repositório Repositório não encontrado, verifique o URL e tente a VPN Você já votou - \ No newline at end of file + diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index cce4e7d3..9c68c008 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -249,4 +249,4 @@ oooooh uuaagh @string/home_play oouuhhh ahhooo-ahah - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 14b2334f..e0db1c0e 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -553,4 +553,4 @@ Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували - \ No newline at end of file + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index f7abd6db..217d2791 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -567,5 +567,5 @@ Không tìm thấy plugin Không thể khởi tạo UI, đây là một LỖI LỚN và cần được báo cáo ngay lập tức tới %s Chọn chế độ để lọc plugin tải xuống - \@string/default_subtitles - \ No newline at end of file + @string/default_subtitles + From a3009af4f585c010cd5018037123fefd644f8921 Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Sat, 19 Aug 2023 19:48:10 +0000 Subject: [PATCH 099/156] Add Native Crash Handler (#565) * Add NativeCrashHandler * Safer init --- app/CMakeLists.txt | 6 +++ app/build.gradle.kts | 6 +++ app/src/main/cpp/native-lib.cpp | 28 ++++++++++++ .../lagradost/cloudstream3/AcraApplication.kt | 1 + .../cloudstream3/NativeCrashHandler.kt | 44 +++++++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 app/CMakeLists.txt create mode 100644 app/src/main/cpp/native-lib.cpp create mode 100644 app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 00000000..7f7fd14c --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,6 @@ +# Set this to the minimum version your project supports. +cmake_minimum_required(VERSION 3.18) +project(CrashHandler) +find_library(log-lib log) +add_library(native-lib SHARED src/main/cpp/native-lib.cpp) +target_link_libraries(native-lib ${log-lib}) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 708a2083..d6515289 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,6 +32,12 @@ android { enable = true } + externalNativeBuild { + cmake { + path("CMakeLists.txt") + } + } + signingConfigs { create("prerelease") { if (prereleaseStoreFile != null) { diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp new file mode 100644 index 00000000..f4cb531f --- /dev/null +++ b/app/src/main/cpp/native-lib.cpp @@ -0,0 +1,28 @@ +#include +#include +#include + +#define TAG "CloudStream Crash Handler" +volatile sig_atomic_t gSignalStatus = 0; +void handleNativeCrash(int signal) { + gSignalStatus = signal; +} + +extern "C" JNIEXPORT void JNICALL +Java_com_lagradost_cloudstream3_NativeCrashHandler_initNativeCrashHandler(JNIEnv *env, jobject) { + #define REGISTER_SIGNAL(X) signal(X, handleNativeCrash); + REGISTER_SIGNAL(SIGSEGV) + #undef REGISTER_SIGNAL +} + +//extern "C" JNIEXPORT void JNICALL +//Java_com_lagradost_cloudstream3_NativeCrashHandler_triggerNativeCrash(JNIEnv *env, jobject thiz) { +// int *p = nullptr; +// *p = 0; +//} + +extern "C" JNIEXPORT int JNICALL +Java_com_lagradost_cloudstream3_NativeCrashHandler_getSignalStatus(JNIEnv *env, jobject) { + //__android_log_print(ANDROID_LOG_INFO, TAG, "Got signal status %d", gSignalStatus); + return gSignalStatus; +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 61d467c4..32702657 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -106,6 +106,7 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) : class AcraApplication : Application() { override fun onCreate() { super.onCreate() + NativeCrashHandler.initCrashHandler() Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) { val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) startActivity(Intent.makeRestartActivityTask(intent!!.component)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt new file mode 100644 index 00000000..e5cb2702 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt @@ -0,0 +1,44 @@ +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 { + 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 + + throw 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() + } +} \ No newline at end of file From c4852ce440736105d32a18f1774ee65dead98808 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 20 Aug 2023 01:29:50 +0200 Subject: [PATCH 100/156] made HSL downloader even faster --- .../cloudstream3/utils/M3u8Helper.kt | 5 + .../utils/VideoDownloadManager.kt | 215 +++++++++++------- 2 files changed, 132 insertions(+), 88 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 5c0b45de..11dfa441 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.utils import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.app +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec @@ -196,6 +197,8 @@ object M3u8Helper2 { return if(condition()) out else null } catch (e: IllegalArgumentException) { return null + } catch (e : CancellationException) { + return null } catch (t: Throwable) { delay(failDelay) } @@ -213,6 +216,8 @@ object M3u8Helper2 { return resolveLink(index) } catch (e: IllegalArgumentException) { return null + } catch (e : CancellationException) { + return null } catch (t: Throwable) { delay(failDelay) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index d8ef7e85..89094f3f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -43,6 +43,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -1083,6 +1084,39 @@ object VideoDownloadManager { ) } + /** Helper function to make sure duplicate attributes don't get overriden or inserted without lowercase cmp + * example: map("a" to 1) appendAndDontOverride map("A" to 2, "a" to 3, "c" to 4) = map("a" to 1, "c" to 4) + * */ + private fun Map.appendAndDontOverride(rhs: Map): Map { + val out = this.toMutableMap() + val current = this.keys.map { it.lowercase() } + for ((key, value) in rhs) { + if (current.contains(key.lowercase())) continue + out[key] = value + } + return out + } + + private fun List.cancel() { + forEach { job -> + try { + job.cancel() + } catch (t: Throwable) { + logError(t) + } + } + } + + private suspend fun List.join() { + forEach { job -> + try { + job.join() + } catch (t: Throwable) { + logError(t) + } + } + } + @Throws suspend fun downloadThing( context: Context, @@ -1166,8 +1200,9 @@ object VideoDownloadManager { hashMapOf() val jobs = (0 until parallelConnections).map { - launch { + launch(Dispatchers.IO) { + // @downloadexplanation // this may seem a bit complex but it more or less acts as a queue system // imagine we do the downloading [0,3] and it response in the order 0,2,3,1 // file: [_,_,_,_] queue: [_,_,_,_] Initial condition @@ -1177,6 +1212,10 @@ object VideoDownloadManager { // file: [X,X,_,_] queue: [_,_,X,X] + added 1 directly to file // file: [X,X,X,X] queue: [_,_,_,_] write the queue and remove from it + // note that this is a bit more complex compared to hsl as ever segment + // will return several bytearrays, and is therefore chained by the byte + // so every request has a front and back byte instead of an index + // this *requires* that no gap exist due because of resolve val callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) = callback@{ response -> if (!isActive) return@callback @@ -1228,10 +1267,11 @@ object VideoDownloadManager { while (true) { if (!isActive) return@launch fileMutex.withLock { - if (metadata.type == DownloadType.IsStopped) return@launch + if (metadata.type == DownloadType.IsStopped + || metadata.type == DownloadType.IsFailed) return@launch } - // just in case, we never want this to fail due to multithreading + // mutex just in case, we never want this to fail due to multithreading val index = currentMutex.withLock { if (!current.hasNext()) return@launch current.nextInt() @@ -1253,68 +1293,14 @@ object VideoDownloadManager { // fast stop as the jobs may be in a slow request metadata.setOnStop { - jobs.forEach { job -> - try { - job.cancel() - } catch (t: Throwable) { - logError(t) - } - } + jobs.cancel() } - jobs.forEach { it.join() } + jobs.join() // jobs are finished so we don't want to stop them anymore metadata.removeStopListener() - // set up a connection - //val request = app.get( - // link.url.replace(" ", "%20"), - // headers = link.headers.appendAndDontOverride( - // mapOf( - // "Accept-Encoding" to "identity", - // "accept" to "*/*", - // "user-agent" to USER_AGENT, - // "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", - // "sec-fetch-mode" to "navigate", - // "sec-fetch-dest" to "video", - // "sec-fetch-user" to "?1", - // "sec-ch-ua-mobile" to "?0", - // ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap() - // ), - // referer = link.referer, - // verify = false - //) - - // init variables - //val contentLength = request.size ?: 0 - //metadata.totalBytes = contentLength + resumeAt - //// save - //metadata.setDownloadFileInfoTemplate( - // DownloadedFileInfo( - // totalBytes = metadata.approxTotalBytes, - // relativePath = relativePath ?: "", - // displayName = displayName, - // basePath = basePath - // ) - //) - //// total length is less than 5mb, that is too short and something has gone wrong - //if (extension == "mp4" && metadata.approxTotalBytes < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION - //// read the buffer into the filestream, this is equivalent of transferTo - //requestStream = request.body.byteStream() - //metadata.type = DownloadType.IsDownloading - //val buffer = ByteArray(DEFAULT_BUFFER_SIZE) - //var read: Int - //while (requestStream.read(buffer, 0, DEFAULT_BUFFER_SIZE).also { read = it } >= 0) { - // fileStream.write(buffer, 0, read) - // // wait until not paused - // while (metadata.type == DownloadType.IsPaused) delay(100) - // // if stopped then break to delete - // if (metadata.type == DownloadType.IsStopped) break - // metadata.addBytes(read.toLong()) - //} - - if (metadata.type == DownloadType.IsFailed) { return@withContext ERROR_CONNECTION_ERROR } @@ -1342,7 +1328,6 @@ object VideoDownloadManager { // note that when failing we don't want to delete the file, // only user interaction has that power - metadata.removeStopListener() metadata.type = DownloadType.IsFailed return@withContext ERROR_CONNECTION_ERROR } finally { @@ -1352,19 +1337,6 @@ object VideoDownloadManager { } } - /** Helper function to make sure duplicate attributes don't get overriden or inserted without lowercase cmp - * example: map("a" to 1) appendAndDontOverride map("A" to 2, "a" to 3, "c" to 4) = map("a" to 1, "c" to 4) - * */ - private fun Map.appendAndDontOverride(rhs: Map): Map { - val out = this.toMutableMap() - val current = this.keys.map { it.lowercase() } - for ((key, value) in rhs) { - if (current.contains(key.lowercase())) continue - out[key] = value - } - return out - } - @Throws private suspend fun downloadHLS( context: Context, @@ -1429,28 +1401,95 @@ object VideoDownloadManager { metadata.hlsTotal = items.size metadata.type = DownloadType.IsDownloading + + val currentMutex = Mutex() + val current = (0 until items.size).iterator() + + val fileMutex = Mutex() + val pendingData: HashMap = hashMapOf() + + // see @downloadexplanation for explanation of this download strategy, + // this keeps all jobs working at all times, // does several connections in parallel instead of a regular for loop to improve // download speed - (startAt until items.size).chunked(parallelConnections).forEach { subset -> - // wait until not paused - while (metadata.type == DownloadType.IsPaused) delay(100) - // if stopped then break to delete - if (metadata.type == DownloadType.IsStopped) return@forEach + val jobs = (0 until parallelConnections).map { + launch(Dispatchers.IO) { + while (true) { + if (!isActive) return@launch + fileMutex.withLock { + if (metadata.type == DownloadType.IsStopped + || metadata.type == DownloadType.IsFailed + ) return@launch + } - subset.amap { idx -> - idx to items.resolveLinkSafe(idx)?.also { bytes -> - metadata.addSegment(bytes.size.toLong()) + // mutex just in case, we never want this to fail due to multithreading + val index = currentMutex.withLock { + if (!current.hasNext()) return@launch + current.nextInt() + } + + // in case something has gone wrong set to failed if the fail is not caused by + // user cancellation + val bytes = items.resolveLinkSafe(index) ?: run { + fileMutex.withLock { + if (metadata.type != DownloadType.IsStopped) { + metadata.type = DownloadType.IsFailed + } + } + return@launch + } + + try { + fileMutex.lock() + // user pause + while (metadata.type == DownloadType.IsPaused) delay(100) + // if stopped then break to delete + if (metadata.type == DownloadType.IsStopped || !isActive) return@launch + + // send notification, no matter the actual write order + metadata.addSegment(bytes.size.toLong()) + + // directly write the bytes if you are first + if (metadata.hlsWrittenProgress == index) { + fileStream.write(bytes) + metadata.setWrittenSegment(index) + } else { + // no need to clone as there will be no modification of this bytearray + pendingData[index] = bytes + } + + // write the cached bytes submitted by other threads + while (true) { + fileStream.write( + pendingData.remove(metadata.hlsWrittenProgress) ?: break + ) + metadata.setWrittenSegment(metadata.hlsWrittenProgress) + } + } catch (t : Throwable) { + // this is in case of write fail + if (metadata.type != DownloadType.IsStopped) { + metadata.type = DownloadType.IsFailed + } + } finally { + fileMutex.unlock() + } } - }.forEach { (idx, bytes) -> - if (bytes == null) { - metadata.type = DownloadType.IsFailed - return@withContext ERROR_CONNECTION_ERROR - } - fileStream.write(bytes) - metadata.setWrittenSegment(idx) } } + // fast stop as the jobs may be in a slow request + metadata.setOnStop { + jobs.cancel() + } + + jobs.join() + + metadata.removeStopListener() + + if (metadata.type == DownloadType.IsFailed) { + return@withContext ERROR_CONNECTION_ERROR + } + if (metadata.type == DownloadType.IsStopped) { // we need to close before delete fileStream.closeQuietly() From 4e28e5f8cc4d811184799cb3aa024665cc0aee3d Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 20 Aug 2023 03:58:31 +0200 Subject: [PATCH 101/156] fixed not downloading the last 20MiB on mp4 downloader + bump + mb/s notification --- app/build.gradle.kts | 2 +- .../utils/VideoDownloadManager.kt | 79 ++++++++++++++----- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d6515289..50125aa3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,7 +58,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.1.5" + versionName = "4.1.6" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 89094f3f..507abc34 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -30,7 +30,6 @@ import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall @@ -256,7 +255,8 @@ object VideoDownloadManager { total: Long, notificationCallback: (Int, Notification) -> Unit, hlsProgress: Long? = null, - hlsTotal: Long? = null + hlsTotal: Long? = null, + bytesPerSecond: Long ): Notification? { try { if (total <= 0) return null// crash, invalid data @@ -327,22 +327,29 @@ object VideoDownloadManager { val totalMbString: String val suffix: String + val mbFormat = "%.1f MB" + if (hlsProgress != null && hlsTotal != null) { progressPercentage = hlsProgress.toLong() * 100 / hlsTotal progressMbString = hlsProgress.toString() totalMbString = hlsTotal.toString() - suffix = " - %.1f MB".format(progress / 1000000f) + suffix = " - $mbFormat".format(progress / 1000000f) } else { progressPercentage = progress * 100 / total - progressMbString = "%.1f MB".format(progress / 1000000f) - totalMbString = "%.1f MB".format(total / 1000000f) + progressMbString = mbFormat.format(progress / 1000000f) + totalMbString = mbFormat.format(total / 1000000f) suffix = "" } + val mbPerSecondString = + if (state == DownloadType.IsDownloading) { + " ($mbFormat/s)".format(bytesPerSecond.toFloat() / 1000000f) + } else "" + val bigText = when (state) { DownloadType.IsDownloading, DownloadType.IsPaused -> { - (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix" + (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix$mbPerSecondString" } DownloadType.IsFailed -> { @@ -608,6 +615,7 @@ object VideoDownloadManager { val bytesTotal: Long, val hlsProgress: Long? = null, val hlsTotal: Long? = null, + val bytesPerSecond: Long ) data class StreamData( @@ -723,6 +731,7 @@ object VideoDownloadManager { // notification metadata private var lastUpdatedMs: Long = 0, + private var lastDownloadedBytes: Long = 0, private val createNotificationCallback: (CreateNotificationMetadata) -> Unit, private var internalType: DownloadType = DownloadType.IsPending, @@ -738,6 +747,12 @@ object VideoDownloadManager { // this is used for copy with metadata on how much we have downloaded for setKey private var downloadFileInfoTemplate: DownloadedFileInfo? = null ) : Closeable { + fun setResumeLength(length: Long) { + bytesDownloaded = length + bytesWritten = length + lastDownloadedBytes = length + } + val approxTotalBytes: Long get() = totalBytes ?: hlsTotal?.let { total -> (bytesDownloaded * (total / hlsProgress.toFloat())).toLong() @@ -839,6 +854,13 @@ object VideoDownloadManager { @JvmName("DownloadMetaDataNotify") private fun notify() { + // max 10 sec between notifications, min 0.1s, this is to stop div by zero + val dt = (System.currentTimeMillis() - lastUpdatedMs).coerceIn(100, 10000) + + val bytesPerSecond = + ((bytesDownloaded - lastDownloadedBytes) * 1000L) / dt + + lastDownloadedBytes = bytesDownloaded lastUpdatedMs = System.currentTimeMillis() try { val bytes = approxTotalBytes @@ -851,7 +873,8 @@ object VideoDownloadManager { bytesDownloaded, bytes, hlsTotal = hlsTotal?.toLong(), - hlsProgress = hlsProgress.toLong() + hlsProgress = hlsProgress.toLong(), + bytesPerSecond = bytesPerSecond ) ) } else { @@ -860,6 +883,7 @@ object VideoDownloadManager { internalType, bytesDownloaded, bytes, + bytesPerSecond = bytesPerSecond ) ) } @@ -1057,21 +1081,29 @@ object VideoDownloadManager { // we don't want to make a separate connection for every 1kb require(chuckSize > 1000) - val contentLength = + var contentLength = app.head(url = url, headers = headers, referer = referer, verify = false).size + if (contentLength != null && contentLength <= 0) contentLength = null var downloadLength: Long? = null var totalLength: Long? = null val ranges = if (contentLength == null) { + // is the equivalent of [startByte..EOF] as we don't know the size we can only do one + // connection LongArray(1) { startByte } } else { downloadLength = contentLength - startByte totalLength = contentLength - LongArray((downloadLength / chuckSize).toInt()) { idx -> + // div with ceiling as + // this makes the last part "unknown ending" and it will break at EOF + // so eg startByte = 0, downloadLength = 13, chuckSize = 10 + // = LongArray(2) { 0, 10 } = [0,10) + [10..EOF] + LongArray(((downloadLength + chuckSize - 1) / chuckSize).toInt()) { idx -> startByte + idx * chuckSize } } + return LazyStreamDownloadData( url = url, headers = headers, @@ -1158,8 +1190,7 @@ object VideoDownloadManager { val resume = stream.resume ?: return@withContext ERROR_UNKNOWN val fileLength = stream.fileLength ?: return@withContext ERROR_UNKNOWN val resumeAt = (if (resume) fileLength else 0) - metadata.bytesDownloaded = resumeAt - metadata.bytesWritten = resumeAt + metadata.setResumeLength(resumeAt) metadata.type = DownloadType.IsPending val items = streamLazy( @@ -1268,7 +1299,8 @@ object VideoDownloadManager { if (!isActive) return@launch fileMutex.withLock { if (metadata.type == DownloadType.IsStopped - || metadata.type == DownloadType.IsFailed) return@launch + || metadata.type == DownloadType.IsFailed + ) return@launch } // mutex just in case, we never want this to fail due to multithreading @@ -1374,7 +1406,7 @@ object VideoDownloadManager { fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN // push the metadata - metadata.bytesDownloaded = stream.fileLength ?: 0 + metadata.setResumeLength(stream.fileLength ?: 0) metadata.hlsProgress = startAt metadata.type = DownloadType.IsPending metadata.setDownloadFileInfoTemplate( @@ -1446,12 +1478,15 @@ object VideoDownloadManager { // if stopped then break to delete if (metadata.type == DownloadType.IsStopped || !isActive) return@launch + val segmentLength = bytes.size.toLong() // send notification, no matter the actual write order - metadata.addSegment(bytes.size.toLong()) + metadata.addSegment(segmentLength) // directly write the bytes if you are first if (metadata.hlsWrittenProgress == index) { fileStream.write(bytes) + + metadata.addBytesWritten(segmentLength) metadata.setWrittenSegment(index) } else { // no need to clone as there will be no modification of this bytearray @@ -1460,12 +1495,14 @@ object VideoDownloadManager { // write the cached bytes submitted by other threads while (true) { - fileStream.write( - pendingData.remove(metadata.hlsWrittenProgress) ?: break - ) + val cache = pendingData.remove(metadata.hlsWrittenProgress) ?: break + val cacheLength = cache.size.toLong() + + fileStream.write(cache) + metadata.addBytesWritten(cacheLength) metadata.setWrittenSegment(metadata.hlsWrittenProgress) } - } catch (t : Throwable) { + } catch (t: Throwable) { // this is in case of write fail if (metadata.type != DownloadType.IsStopped) { metadata.type = DownloadType.IsFailed @@ -1756,7 +1793,8 @@ object VideoDownloadManager { meta.bytesTotal, notificationCallback, meta.hlsProgress, - meta.hlsTotal + meta.hlsTotal, + meta.bytesPerSecond ) } } @@ -1785,7 +1823,8 @@ object VideoDownloadManager { meta.type, meta.bytesDownloaded, meta.bytesTotal, - notificationCallback + notificationCallback, + bytesPerSecond = meta.bytesPerSecond ) } }) From afcbdeecc86151081aa8a135b3c5900bf1ec8c7e Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Tue, 22 Aug 2023 04:00:05 +0200 Subject: [PATCH 102/156] changes to downloader for stable resume --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 6 +- .../ui/settings/SettingsUpdates.kt | 11 +- .../utils/DownloadFileWorkManager.kt | 22 +- .../utils/VideoDownloadManager.kt | 532 ++++++------------ 4 files changed, 191 insertions(+), 380 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index e0d50cc3..9e601fc7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -520,10 +520,10 @@ class GeneratorPlayer : FullScreenPlayer() { if (uri == null) return@normalSafeApiCall val ctx = context ?: AcraApplication.context ?: return@normalSafeApiCall // RW perms for the path - val flags = + ctx.contentResolver.takePersistableUriPermission( + uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - ctx.contentResolver.takePersistableUriPermission(uri, flags) + ) val file = UniFile.fromUri(ctx, uri) println("Loaded subtitle file. Selected URI path: $uri - Name: ${file.name}") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index c304629a..62e46c08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -116,13 +116,14 @@ class SettingsUpdates : PreferenceFragmentCompat() { null, "txt", false - ).fileStream - fileStream?.writer()?.write(text) - } catch (e: Exception) { - logError(e) + ).openNew() + fileStream.writer().write(text) + dialog.dismissSafe(activity) + } catch (t: Throwable) { + logError(t) + showToast(t.message) } finally { fileStream?.closeQuietly() - dialog.dismissSafe(activity) } } binding.closeBtt.setOnClickListener { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt index aa424c08..421e4420 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt @@ -16,6 +16,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadCheck import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadEpisode import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadFromResume import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadStatusEvent +import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadResumePackage import kotlinx.coroutines.delay const val DOWNLOAD_CHECK = "DownloadCheck" @@ -36,15 +37,20 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo WORK_KEY_PACKAGE, key ) + if (info != null) { - downloadEpisode( - applicationContext, - info.source, - info.folder, - info.ep, - info.links, - ::handleNotification - ) + getDownloadResumePackage(applicationContext, info.ep.id)?.let { dpkg -> + downloadFromResume(applicationContext, dpkg, ::handleNotification) + } ?: run { + downloadEpisode( + applicationContext, + info.source, + info.folder, + info.ep, + info.links, + ::handleNotification + ) + } } else if (pkg != null) { downloadFromResume(applicationContext, pkg, ::handleNotification) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 507abc34..a81e4b3a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -9,9 +9,7 @@ import android.graphics.Bitmap import android.net.Uri import android.os.Build import android.os.Environment -import android.provider.MediaStore import androidx.annotation.DrawableRes -import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.net.toUri @@ -32,6 +30,7 @@ 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.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -44,6 +43,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -301,6 +301,8 @@ object VideoDownloadManager { if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { builder.setProgress((total / 1000).toInt(), (progress / 1000).toInt(), false) + } else if (state == DownloadType.IsPending) { + builder.setProgress(0,0,true) } val rowTwoExtra = if (ep.name != null) " - ${ep.name}\n" else "" @@ -352,6 +354,10 @@ object VideoDownloadManager { (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix$mbPerSecondString" } + DownloadType.IsPending -> { + (if (linkName == null) "" else "$linkName\n") + rowTwo + } + DownloadType.IsFailed -> { downloadFormat.format( context.getString(R.string.download_failed), @@ -363,7 +369,7 @@ object VideoDownloadManager { downloadFormat.format(context.getString(R.string.download_done), rowTwo) } - else -> { + DownloadType.IsStopped -> { downloadFormat.format( context.getString(R.string.download_canceled), rowTwo @@ -377,7 +383,7 @@ object VideoDownloadManager { } else { val txt = when (state) { - DownloadType.IsDownloading, DownloadType.IsPaused -> { + DownloadType.IsDownloading, DownloadType.IsPaused, DownloadType.IsPending -> { rowTwo } @@ -392,7 +398,7 @@ object VideoDownloadManager { downloadFormat.format(context.getString(R.string.download_done), rowTwo) } - else -> { + DownloadType.IsStopped -> { downloadFormat.format( context.getString(R.string.download_canceled), rowTwo @@ -480,54 +486,6 @@ object VideoDownloadManager { return tempName.replace(" ", " ").trim(' ') } - @RequiresApi(Build.VERSION_CODES.Q) - private fun ContentResolver.getExistingFolderStartName(relativePath: String): List>? { - try { - val projection = arrayOf( - MediaStore.MediaColumns._ID, - MediaStore.MediaColumns.DISPLAY_NAME, // unused (for verification use only) - //MediaStore.MediaColumns.RELATIVE_PATH, // unused (for verification use only) - ) - - val selection = - "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath'" - - val result = this.query( - MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), - projection, selection, null, null - ) - val list = ArrayList>() - - result.use { c -> - if (c != null && c.count >= 1) { - c.moveToFirst() - while (true) { - val id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) - val name = - c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)) - val uri = ContentUris.withAppendedId( - MediaStore.Downloads.EXTERNAL_CONTENT_URI, id - ) - list.add(Pair(name, uri)) - if (c.isLast) { - break - } - c.moveToNext() - } - - /* - val cDisplayName = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)) - val cRelativePath = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH))*/ - - } - } - return list - } catch (e: Exception) { - logError(e) - return null - } - } - /** * Used for getting video player subs. * @return List of pairs for the files in this format: @@ -538,76 +496,12 @@ object VideoDownloadManager { basePath: String? ): List>? { val base = basePathToFile(context, basePath) - val folder = base?.gotoDir(relativePath, false) + val folder = base?.gotoDir(relativePath, false) ?: return null + if (!folder.isDirectory) return null - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) { - return context.contentResolver?.getExistingFolderStartName(relativePath) - } else { -// val normalPath = -// "${Environment.getExternalStorageDirectory()}${File.separatorChar}${relativePath}".replace( -// '/', -// File.separatorChar -// ) -// val folder = File(normalPath) - if (folder?.isDirectory == true) { - return folder.listFiles()?.map { Pair(it.name ?: "", it.uri) } - } - } - return null -// } + return folder.listFiles()?.map { (it.name ?: "") to it.uri } } - @RequiresApi(Build.VERSION_CODES.Q) - private fun ContentResolver.getExistingDownloadUriOrNullQ( - relativePath: String, - displayName: String - ): Uri? { - try { - val projection = arrayOf( - MediaStore.MediaColumns._ID, - //MediaStore.MediaColumns.DISPLAY_NAME, // unused (for verification use only) - //MediaStore.MediaColumns.RELATIVE_PATH, // unused (for verification use only) - ) - - val selection = - "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath' AND " + "${MediaStore.MediaColumns.DISPLAY_NAME}='$displayName'" - - val result = this.query( - MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), - projection, selection, null, null - ) - - result.use { c -> - if (c != null && c.count >= 1) { - c.moveToFirst().let { - val id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) - /* - val cDisplayName = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)) - val cRelativePath = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH))*/ - - return ContentUris.withAppendedId( - MediaStore.Downloads.EXTERNAL_CONTENT_URI, id - ) - } - } - } - return null - } catch (e: Exception) { - logError(e) - return null - } - } - - @RequiresApi(Build.VERSION_CODES.Q) - fun ContentResolver.getFileLength(fileUri: Uri): Long? { - return try { - this.openFileDescriptor(fileUri, "r") - .use { it?.statSize ?: 0 } - } catch (e: Exception) { - logError(e) - null - } - } data class CreateNotificationMetadata( val type: DownloadType, @@ -619,16 +513,39 @@ object VideoDownloadManager { ) data class StreamData( - val errorCode: Int, - val resume: Boolean? = null, - val fileLength: Long? = null, - val fileStream: OutputStream? = null, - ) + private val fileLength: Long, + val file: UniFile, + //val fileStream: OutputStream, + ) { + fun open() : OutputStream { + return file.openOutputStream(resume) + } + + fun openNew() : OutputStream { + return file.openOutputStream(false) + } + + val resume: Boolean get() = fileLength > 0L + val startAt: Long get() = if (resume) fileLength else 0L + val exists: Boolean get() = file.exists() + } + + + //class ADownloadException(val id: Int) : RuntimeException(message = "Download error $id") + + fun UniFile.createFileOrThrow(displayName: String): UniFile { + return this.createFile(displayName) ?: throw IOException("Could not create file") + } + + fun UniFile.deleteOrThrow() { + if (!this.delete()) throw IOException("Could not delete file") + } /** * Sets up the appropriate file and creates a data stream from the file. * Used for initializing downloads. * */ + @Throws(IOException::class) fun setupStream( context: Context, name: String, @@ -637,88 +554,24 @@ object VideoDownloadManager { tryResume: Boolean, ): StreamData { val displayName = getDisplayName(name, extension) - val fileStream: OutputStream - val fileLength: Long - var resume = tryResume - val baseFile = context.getBasePath() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.first?.isDownloadDir() == true) { - val cr = context.contentResolver ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND) + val (baseFile, _) = context.getBasePath() - val currentExistingFile = - cr.getExistingDownloadUriOrNullQ( - folder ?: "", - displayName - ) // CURRENT FILE WITH THE SAME PATH + val subDir = baseFile?.gotoDir(folder) ?: throw IOException() + val foundFile = subDir.findFile(displayName) - fileLength = - if (currentExistingFile == null || !resume) 0 else (cr.getFileLength( - currentExistingFile - ) - ?: 0)// IF NOT RESUME THEN 0, OTHERWISE THE CURRENT FILE SIZE - - if (!resume && currentExistingFile != null) { // DELETE FILE IF FILE EXITS AND NOT RESUME - val rowsDeleted = context.contentResolver.delete(currentExistingFile, null, null) - if (rowsDeleted < 1) { - println("ERROR DELETING FILE!!!") - } - } - - var appendFile = false - val newFileUri = if (resume && currentExistingFile != null) { - appendFile = true - currentExistingFile - } else { - val contentUri = - MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI - //val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - val currentMimeType = when (extension) { - - // Absolutely ridiculous, if text/vtt is used as mimetype scoped storage prevents - // downloading to /Downloads yet it works with null - - "vtt" -> null // "text/vtt" - "mp4" -> "video/mp4" - "srt" -> null // "application/x-subrip"//"text/plain" - else -> null - } - val newFile = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) - put(MediaStore.MediaColumns.TITLE, name) - if (currentMimeType != null) - put(MediaStore.MediaColumns.MIME_TYPE, currentMimeType) - put(MediaStore.MediaColumns.RELATIVE_PATH, folder) - } - - cr.insert( - contentUri, - newFile - ) ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND) - } - - fileStream = cr.openOutputStream(newFileUri, "w" + (if (appendFile) "a" else "")) - ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND) + val (file, fileLength) = if (foundFile == null || !foundFile.exists()) { + subDir.createFileOrThrow(displayName) to 0L } else { - val subDir = baseFile.first?.gotoDir(folder) - val rFile = subDir?.findFile(displayName) - if (rFile?.exists() != true) { - fileLength = 0 - if (subDir?.createFile(displayName) == null) return StreamData(ERROR_CREATE_FILE) + if (tryResume) { + foundFile to foundFile.size() } else { - if (resume) { - fileLength = rFile.size() - } else { - fileLength = 0 - if (!rFile.delete()) return StreamData(ERROR_DELETING_FILE) - if (subDir.createFile(displayName) == null) return StreamData(ERROR_CREATE_FILE) - } + foundFile.deleteOrThrow() + subDir.createFileOrThrow(displayName) to 0L } - fileStream = (subDir.findFile(displayName) - ?: subDir.createFile(displayName))!!.openOutputStream() -// fileStream = FileOutputStream(rFile, false) - if (fileLength == 0L) resume = false } - return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream) + + return StreamData(fileLength, file) } /** This class handles the notifications, as well as the relevant key */ @@ -938,6 +791,8 @@ object VideoDownloadManager { fun setWrittenSegment(segmentIndex: Int) { hlsWrittenProgress = segmentIndex + 1 + // in case of abort we need to save every written progress + updateFileInfo() } } @@ -1185,18 +1040,16 @@ object VideoDownloadManager { // set up the download file val stream = setupStream(context, name, relativePath, extension, tryResume) - if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN - val resume = stream.resume ?: return@withContext ERROR_UNKNOWN - val fileLength = stream.fileLength ?: return@withContext ERROR_UNKNOWN - val resumeAt = (if (resume) fileLength else 0) - metadata.setResumeLength(resumeAt) + + fileStream = stream.open() + + metadata.setResumeLength(stream.startAt) metadata.type = DownloadType.IsPending val items = streamLazy( url = link.url.replace(" ", "%20"), referer = link.referer, - startByte = resumeAt, + startByte = stream.startAt, headers = link.headers.appendAndDontOverride( mapOf( "Accept-Encoding" to "identity", @@ -1230,6 +1083,19 @@ object VideoDownloadManager { val pendingData: HashMap = hashMapOf() + val fileChecker = launch(Dispatchers.IO) { + while (isActive) { + if (stream.exists) { + delay(5000) + continue + } + fileMutex.withLock { + metadata.type = DownloadType.IsStopped + } + break + } + } + val jobs = (0 until parallelConnections).map { launch(Dispatchers.IO) { @@ -1329,9 +1195,11 @@ object VideoDownloadManager { } jobs.join() + fileChecker.cancel() // jobs are finished so we don't want to stop them anymore metadata.removeStopListener() + if (!stream.exists) metadata.type = DownloadType.IsStopped if (metadata.type == DownloadType.IsFailed) { return@withContext ERROR_CONNECTION_ERROR @@ -1341,11 +1209,8 @@ object VideoDownloadManager { // we need to close before delete fileStream.closeQuietly() metadata.onDelete() - if (deleteFile(context, baseFile, relativePath ?: "", displayName)) { - return@withContext SUCCESS_STOPPED - } else { - return@withContext ERROR_DELETING_FILE - } + deleteFile(context, baseFile, relativePath ?: "", displayName) + return@withContext SUCCESS_STOPPED } metadata.type = DownloadType.IsDone @@ -1400,13 +1265,13 @@ object VideoDownloadManager { folder ) else folder val displayName = getDisplayName(name, extension) - val stream = setupStream(context, name, relativePath, extension, startAt > 0) - if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - if (stream.resume != true) startAt = 0 - fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN + val stream = + setupStream(context, name, relativePath, extension, startAt > 0) + if (!stream.resume) startAt = 0 + fileStream = stream.open() // push the metadata - metadata.setResumeLength(stream.fileLength ?: 0) + metadata.setResumeLength(stream.startAt) metadata.hlsProgress = startAt metadata.type = DownloadType.IsPending metadata.setDownloadFileInfoTemplate( @@ -1433,13 +1298,25 @@ object VideoDownloadManager { metadata.hlsTotal = items.size metadata.type = DownloadType.IsDownloading - val currentMutex = Mutex() - val current = (0 until items.size).iterator() + val current = (startAt until items.size).iterator() val fileMutex = Mutex() val pendingData: HashMap = hashMapOf() + val fileChecker = launch(Dispatchers.IO) { + while (isActive) { + if (stream.exists) { + delay(5000) + continue + } + fileMutex.withLock { + metadata.type = DownloadType.IsStopped + } + break + } + } + // see @downloadexplanation for explanation of this download strategy, // this keeps all jobs working at all times, // does several connections in parallel instead of a regular for loop to improve @@ -1476,7 +1353,7 @@ object VideoDownloadManager { // user pause while (metadata.type == DownloadType.IsPaused) delay(100) // if stopped then break to delete - if (metadata.type == DownloadType.IsStopped || !isActive) return@launch + if (metadata.type == DownloadType.IsStopped || metadata.type == DownloadType.IsFailed || !isActive) return@launch val segmentLength = bytes.size.toLong() // send notification, no matter the actual write order @@ -1499,11 +1376,13 @@ object VideoDownloadManager { val cacheLength = cache.size.toLong() fileStream.write(cache) + metadata.addBytesWritten(cacheLength) metadata.setWrittenSegment(metadata.hlsWrittenProgress) } } catch (t: Throwable) { // this is in case of write fail + logError(t) if (metadata.type != DownloadType.IsStopped) { metadata.type = DownloadType.IsFailed } @@ -1520,9 +1399,12 @@ object VideoDownloadManager { } jobs.join() + fileChecker.cancel() metadata.removeStopListener() + if (!stream.exists) metadata.type = DownloadType.IsStopped + if (metadata.type == DownloadType.IsFailed) { return@withContext ERROR_CONNECTION_ERROR } @@ -1531,11 +1413,8 @@ object VideoDownloadManager { // we need to close before delete fileStream.closeQuietly() metadata.onDelete() - if (deleteFile(context, baseFile, relativePath ?: "", displayName)) { - return@withContext SUCCESS_STOPPED - } else { - return@withContext ERROR_DELETING_FILE - } + deleteFile(context, baseFile, relativePath ?: "", displayName) + return@withContext SUCCESS_STOPPED } metadata.type = DownloadType.IsDone @@ -1564,6 +1443,11 @@ object VideoDownloadManager { directoryName: String?, createMissingDirectories: Boolean = true ): UniFile? { + if(directoryName == null) return this + + return directoryName.split(File.separatorChar).filter { it.isNotBlank() }.fold(this) { file: UniFile?, directory -> + file?.createDirectory(directory) + } // May give this error on scoped storage. // W/DocumentsContract: Failed to create document @@ -1571,7 +1455,7 @@ object VideoDownloadManager { // Not present in latest testing. -// println("Going to dir $directoryName from ${this.uri} ---- ${this.filePath}") + println("Going to dir $directoryName from ${this.uri} ---- ${this.filePath}") try { // Creates itself from parent if doesn't exist. @@ -1671,49 +1555,6 @@ object VideoDownloadManager { return this != null && this.filePath == getDownloadDir()?.filePath } - /*private fun delete( - context: Context, - name: String, - folder: String?, - extension: String, - parentId: Int?, - basePath: UniFile? - ): Int { - val displayName = getDisplayName(name, extension) - - // delete all subtitle files - if (extension != "vtt" && extension != "srt") { - try { - delete(context, name, folder, "vtt", parentId, basePath) - delete(context, name, folder, "srt", parentId, basePath) - } catch (e: Exception) { - logError(e) - } - } - - // If scoped storage and using download dir (not accessible with UniFile) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.isDownloadDir()) { - val relativePath = getRelativePath(folder) - val lastContent = - context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName) ?: return ERROR_DELETING_FILE - if(context.contentResolver.delete(lastContent, null, null) <= 0) { - return ERROR_DELETING_FILE - } - } else { - val dir = basePath?.gotoDir(folder) - val file = dir?.findFile(displayName) - val success = file?.delete() - if (success != true) return ERROR_DELETING_FILE else { - // Cleans up empty directory - if (dir.listFiles()?.isEmpty() == true) dir.delete() - } - parentId?.let { - downloadDeleteEvent.invoke(parentId) - } - } - return SUCCESS_STOPPED - }*/ - fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { return getFileName(context, metadata.name, metadata.episode, metadata.season) @@ -1765,70 +1606,60 @@ object VideoDownloadManager { } } - if (link.isM3u8 || URL(link.url).path.endsWith(".m3u8")) { - val startIndex = if (tryResume) { - context.getKey( - KEY_DOWNLOAD_INFO, - ep.id.toString(), - null - )?.extraInfo?.toIntOrNull() - } else null - return suspendSafeApiCall { - downloadHLS( + val callback: (CreateNotificationMetadata) -> Unit = { meta -> + main { + createNotification( + context, + source, + link.name, + ep, + meta.type, + meta.bytesDownloaded, + meta.bytesTotal, + notificationCallback, + meta.hlsProgress, + meta.hlsTotal, + meta.bytesPerSecond + ) + } + } + + try { + if (link.isM3u8 || normalSafeApiCall { URL(link.url).path.endsWith(".m3u8") } == true) { + val startIndex = if (tryResume) { + context.getKey( + KEY_DOWNLOAD_INFO, + ep.id.toString(), + null + )?.extraInfo?.toIntOrNull() + } else null + + return downloadHLS( context, link, name, folder, ep.id, startIndex, - createNotificationCallback = { meta -> - main { - createNotification( - context, - source, - link.name, - ep, - meta.type, - meta.bytesDownloaded, - meta.bytesTotal, - notificationCallback, - meta.hlsProgress, - meta.hlsTotal, - meta.bytesPerSecond - ) - } - } + callback ) - }.also { - extractorJob.cancel() - } ?: ERROR_UNKNOWN + } else { + return downloadThing( + context, + link, + name, + folder, + "mp4", + tryResume, + ep.id, + callback + ) + } + } catch (t: Throwable) { + return ERROR_UNKNOWN + } finally { + extractorJob.cancel() } - - return suspendSafeApiCall { - downloadThing( - context, - link, - name, - folder, - "mp4", - tryResume, - ep.id, - createNotificationCallback = { meta -> - main { - createNotification( - context, - source, - link.name, - ep, - meta.type, - meta.bytesDownloaded, - meta.bytesTotal, - notificationCallback, - bytesPerSecond = meta.bytesPerSecond - ) - } - }) - }.also { extractorJob.cancel() } ?: ERROR_UNKNOWN } suspend fun downloadCheck( @@ -1911,26 +1742,10 @@ object VideoDownloadManager { val info = context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return null val base = basePathToFile(context, info.basePath) + val file = base?.gotoDir(info.relativePath, false)?.findFile(info.displayName) + if (file?.exists() != true) return null - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) { - val cr = context.contentResolver ?: return null - val fileUri = - cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName) - ?: return null - val fileLength = cr.getFileLength(fileUri) ?: return null - if (fileLength == 0L) return null - return DownloadedFileInfoResult(fileLength, info.totalBytes, fileUri) - } else { - - val file = base?.gotoDir(info.relativePath, false)?.findFile(info.displayName) - -// val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName) -// val dFile = File(normalPath) - - if (file?.exists() != true) return null - - return DownloadedFileInfoResult(file.size(), info.totalBytes, file.uri) - } + return DownloadedFileInfoResult(file.size(), info.totalBytes, file.uri) } catch (e: Exception) { logError(e) return null @@ -1943,6 +1758,7 @@ object VideoDownloadManager { fun UniFile.size(): Long { val len = length() return if (len <= 1) { + println("LEN:::::::>>>>>>>>>>>>>>>>>>>>>>>$len") val inputStream = this.openInputStream() return inputStream.available().toLong().also { inputStream.closeQuietly() } } else { @@ -1962,32 +1778,20 @@ object VideoDownloadManager { relativePath: String, displayName: String ): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && folder.isDownloadDir()) { - val cr = context.contentResolver ?: return false - val fileUri = - cr.getExistingDownloadUriOrNullQ(relativePath, displayName) - ?: return true // FILE NOT FOUND, ALREADY DELETED - - return cr.delete(fileUri, null, null) > 0 // IF DELETED ROWS IS OVER 0 - } else { - val file = folder?.gotoDir(relativePath)?.findFile(displayName) -// val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName) -// val dFile = File(normalPath) - if (file?.exists() != true) return true - return try { - file.delete() - } catch (e: Exception) { - logError(e) - val cr = context.contentResolver - cr.delete(file.uri, null, null) > 0 - } + val file = folder?.gotoDir(relativePath)?.findFile(displayName) ?: return false + if (!file.exists()) return true + return try { + file.delete() + } catch (e: Exception) { + logError(e) + (context.contentResolver?.delete(file.uri, null, null) ?: return false) > 0 } } private fun deleteFile(context: Context, id: Int): Boolean { val info = context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return false - downloadEvent.invoke(Pair(id, DownloadActionType.Stop)) + downloadEvent.invoke(id to DownloadActionType.Stop) downloadProgressEvent.invoke(Triple(id, 0, 0)) downloadStatusEvent.invoke(id to DownloadType.IsStopped) downloadDeleteEvent.invoke(id) From 3ea6b1a8d507899ae5a9b295e9f29ded7e0e0448 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 23 Aug 2023 06:25:06 +0200 Subject: [PATCH 103/156] fixed resume download + migrated filesystem to SafeFile --- .../ui/player/DownloadedPlayerActivity.kt | 4 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 8 +- .../ui/settings/SettingsGeneral.kt | 42 +- .../cloudstream3/utils/BackupUtils.kt | 23 +- .../utils/VideoDownloadManager.kt | 378 +++++++----------- .../cloudstream3/utils/storage/MediaFile.kt | 369 +++++++++++++++++ .../cloudstream3/utils/storage/SafeFile.kt | 244 +++++++++++ .../utils/storage/UniFileWrapper.kt | 116 ++++++ 8 files changed, 924 insertions(+), 260 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index 6f40e145..03405faf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -6,11 +6,11 @@ import android.os.Bundle import android.util.Log import android.view.KeyEvent import androidx.appcompat.app.AppCompatActivity -import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.storage.SafeFile const val DTAG = "PlayerActivity" @@ -50,7 +50,7 @@ class DownloadedPlayerActivity : AppCompatActivity() { } private fun playUri(uri: Uri) { - val name = UniFile.fromUri(this, uri).name + val name = SafeFile.fromUri(this, uri)?.name() this.navigate( R.id.global_to_navigation_player, GeneratorPlayer.newInstance( DownloadFileGenerator( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 9e601fc7..341b4ad3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -52,6 +52,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.toPx +import com.lagradost.cloudstream3.utils.storage.SafeFile import kotlinx.coroutines.Job import java.util.* import kotlin.math.abs @@ -525,10 +526,11 @@ class GeneratorPlayer : FullScreenPlayer() { Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) - val file = UniFile.fromUri(ctx, uri) - println("Loaded subtitle file. Selected URI path: $uri - Name: ${file.name}") + val file = SafeFile.fromUri(ctx, uri) + val fileName = file?.name() + println("Loaded subtitle file. Selected URI path: $uri - Name: $fileName") // DO NOT REMOVE THE FILE EXTENSION FROM NAME, IT'S NEEDED FOR MIME TYPES - val name = file.name ?: uri.toString() + val name = fileName ?: uri.toString() val subtitleData = SubtitleData( name, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index 2c81ad1f..f46aac9b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -3,9 +3,7 @@ package com.lagradost.cloudstream3.ui.settings import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Build import android.os.Bundle -import android.os.Environment import android.view.View import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts @@ -13,7 +11,6 @@ import androidx.appcompat.app.AlertDialog import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty -import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.AcraApplication import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -41,7 +38,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath -import java.io.File +import com.lagradost.cloudstream3.utils.storage.SafeFile fun getCurrentLocale(context: Context): String { val res = context.resources @@ -139,8 +136,9 @@ class SettingsGeneral : PreferenceFragmentCompat() { context.contentResolver.takePersistableUriPermission(uri, flags) - val file = UniFile.fromUri(context, uri) - println("Selected URI path: $uri - Full path: ${file.filePath}") + val file = SafeFile.fromUri(context, uri) + val filePath = file?.filePath() + println("Selected URI path: $uri - Full path: $filePath") // Stores the real URI using download_path_key // Important that the URI is stored instead of filepath due to permissions. @@ -149,7 +147,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { // From URI -> File path // File path here is purely for cosmetic purposes in settings - (file.filePath ?: uri.toString()).let { + (filePath ?: uri.toString()).let { PreferenceManager.getDefaultSharedPreferences(context) .edit().putString(getString(R.string.download_path_pref), it).apply() } @@ -306,25 +304,23 @@ class SettingsGeneral : PreferenceFragmentCompat() { } return@setOnPreferenceClickListener true } + fun getDownloadDirs(): List { return normalSafeApiCall { - val defaultDir = VideoDownloadManager.getDownloadDir()?.filePath + context?.let { ctx -> + val defaultDir = VideoDownloadManager.getDefaultDir(ctx)?.filePath() - // app_name_download_path = Cloudstream and does not change depending on release. - // DOES NOT WORK ON SCOPED STORAGE. - val secondaryDir = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) null else Environment.getExternalStorageDirectory().absolutePath + - File.separator + resources.getString(R.string.app_name_download_path) - val first = listOf(defaultDir, secondaryDir) - (try { - val currentDir = context?.getBasePath()?.let { it.first?.filePath ?: it.second } + val first = listOf(defaultDir) + (try { + val currentDir = ctx.getBasePath().let { it.first?.filePath() ?: it.second } - (first + - requireContext().getExternalFilesDirs("").mapNotNull { it.path } + - currentDir) - } catch (e: Exception) { - first - }).filterNotNull().distinct() + (first + + ctx.getExternalFilesDirs("").mapNotNull { it.path } + + currentDir) + } catch (e: Exception) { + first + }).filterNotNull().distinct() + } } ?: emptyList() } @@ -339,7 +335,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { val currentDir = settingsManager.getString(getString(R.string.download_path_pref), null) - ?: VideoDownloadManager.getDownloadDir().toString() + ?: context?.let { ctx -> VideoDownloadManager.getDefaultDir(ctx).toString() } activity?.showBottomDialog( dirs + listOf("Custom"), diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index 5bd0cd15..2da54678 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -1,11 +1,8 @@ package com.lagradost.cloudstream3.utils import android.annotation.SuppressLint -import android.content.ContentValues import android.content.Context import android.net.Uri -import android.os.Build -import android.provider.MediaStore import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -36,9 +33,9 @@ import com.lagradost.cloudstream3.utils.DataStore.mapper import com.lagradost.cloudstream3.utils.DataStore.setKeyRaw import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.requestRW -import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath -import com.lagradost.cloudstream3.utils.VideoDownloadManager.isDownloadDir -import java.io.IOException +import com.lagradost.cloudstream3.utils.VideoDownloadManager.setupStream +import okhttp3.internal.closeQuietly +import java.io.OutputStream import java.io.PrintWriter import java.lang.System.currentTimeMillis import java.text.SimpleDateFormat @@ -147,6 +144,8 @@ object BackupUtils { @SuppressLint("SimpleDateFormat") fun FragmentActivity.backup() { + var fileStream: OutputStream? = null + var printStream: PrintWriter? = null try { if (!checkWrite()) { showToast(getString(R.string.backup_failed), Toast.LENGTH_LONG) @@ -154,13 +153,16 @@ object BackupUtils { return } - val subDir = getBasePath().first val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) val ext = "json" val displayName = "CS3_Backup_${date}" val backupFile = getBackup() + val stream = setupStream(this, displayName, null, ext, false) + fileStream = stream.openNew() + printStream = PrintWriter(fileStream) + printStream.print(mapper.writeValueAsString(backupFile)) - val steam = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + /*val steam = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && subDir?.isDownloadDir() == true ) { val cr = this.contentResolver @@ -198,7 +200,7 @@ object BackupUtils { val printStream = PrintWriter(steam) printStream.print(mapper.writeValueAsString(backupFile)) - printStream.close() + printStream.close()*/ showToast( R.string.backup_success, @@ -214,6 +216,9 @@ object BackupUtils { } catch (e: Exception) { logError(e) } + } finally { + printStream?.closeQuietly() + fileStream?.closeQuietly() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index a81e4b3a..37c02be4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -8,7 +8,6 @@ import android.content.* import android.graphics.Bitmap import android.net.Uri import android.os.Build -import android.os.Environment import androidx.annotation.DrawableRes import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -20,7 +19,6 @@ import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager import com.bumptech.glide.load.model.GlideUrl import com.fasterxml.jackson.annotation.JsonProperty -import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -31,19 +29,19 @@ import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall -import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute +import com.lagradost.cloudstream3.utils.storage.MediaFileContentType +import com.lagradost.cloudstream3.utils.storage.SafeFile import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel -import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -160,24 +158,33 @@ object VideoDownloadManager { @JsonProperty("pkg") val pkg: DownloadResumePackage, ) - private const val SUCCESS_DOWNLOAD_DONE = 1 - private const val SUCCESS_STREAM = 3 - private const val SUCCESS_STOPPED = 2 + data class DownloadStatus( + /** if you should retry with the same args and hope for a better result */ + val retrySame: Boolean, + /** if you should try the next mirror */ + val tryNext: Boolean, + /** if the result is what the user intended */ + val success: Boolean, + ) - // will not download the next one, but is still classified as an error - private const val ERROR_DELETING_FILE = 3 - private const val ERROR_CREATE_FILE = -2 - private const val ERROR_UNKNOWN = -10 + /** Invalid input, just skip to the next one as the same args will give the same error */ + private val DOWNLOAD_INVALID_INPUT = + DownloadStatus(retrySame = false, tryNext = true, success = false) - //private const val ERROR_OPEN_FILE = -3 - private const val ERROR_TOO_SMALL_CONNECTION = -4 + /** no need to try any other mirror as we have downloaded the file */ + private val DOWNLOAD_SUCCESS = + DownloadStatus(retrySame = false, tryNext = false, success = true) - //private const val ERROR_WRONG_CONTENT = -5 - private const val ERROR_CONNECTION_ERROR = -6 + /** the user pressed stop, so no need to download anything else */ + private val DOWNLOAD_STOPPED = + DownloadStatus(retrySame = false, tryNext = false, success = true) - //private const val ERROR_MEDIA_STORE_URI_CANT_BE_CREATED = -7 - //private const val ERROR_CONTENT_RESOLVER_CANT_OPEN_STREAM = -8 - private const val ERROR_CONTENT_RESOLVER_NOT_FOUND = -9 + /** the process failed due to some reason, so we retry and also try the next mirror */ + private val DOWNLOAD_FAILED = DownloadStatus(retrySame = true, tryNext = true, success = false) + + /** bad config, skip all mirrors as every call to download will have the same bad config */ + private val DOWNLOAD_BAD_CONFIG = + DownloadStatus(retrySame = false, tryNext = false, success = false) private const val KEY_RESUME_PACKAGES = "download_resume" const val KEY_DOWNLOAD_INFO = "download_info" @@ -209,15 +216,15 @@ object VideoDownloadManager { } } - /** Will return IsDone if not found or error */ - fun getDownloadState(id: Int): DownloadType { - return try { - downloadStatus[id] ?: DownloadType.IsDone - } catch (e: Exception) { - logError(e) - DownloadType.IsDone - } - } + ///** Will return IsDone if not found or error */ + //fun getDownloadState(id: Int): DownloadType { + // return try { + // downloadStatus[id] ?: DownloadType.IsDone + // } catch (e: Exception) { + // logError(e) + // DownloadType.IsDone + // } + //} private val cachedBitmaps = hashMapOf() fun Context.getImageBitmapFromUrl(url: String, headers: Map? = null): Bitmap? { @@ -302,7 +309,7 @@ object VideoDownloadManager { if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { builder.setProgress((total / 1000).toInt(), (progress / 1000).toInt(), false) } else if (state == DownloadType.IsPending) { - builder.setProgress(0,0,true) + builder.setProgress(0, 0, true) } val rowTwoExtra = if (ep.name != null) " - ${ep.name}\n" else "" @@ -496,10 +503,11 @@ object VideoDownloadManager { basePath: String? ): List>? { val base = basePathToFile(context, basePath) - val folder = base?.gotoDir(relativePath, false) ?: return null - if (!folder.isDirectory) return null + val folder = base?.gotoDirectory(relativePath, false) ?: return null + if (folder.isDirectory() != false) return null - return folder.listFiles()?.map { (it.name ?: "") to it.uri } + return folder.listFiles() + ?.mapNotNull { (it.name() ?: "") to (it.uri() ?: return@mapNotNull null) } } @@ -514,37 +522,29 @@ object VideoDownloadManager { data class StreamData( private val fileLength: Long, - val file: UniFile, + val file: SafeFile, //val fileStream: OutputStream, ) { - fun open() : OutputStream { - return file.openOutputStream(resume) + @Throws(IOException::class) + fun open(): OutputStream { + return file.openOutputStreamOrThrow(resume) } - fun openNew() : OutputStream { - return file.openOutputStream(false) + @Throws(IOException::class) + fun openNew(): OutputStream { + return file.openOutputStreamOrThrow(false) + } + + fun delete(): Boolean { + return file.delete() } val resume: Boolean get() = fileLength > 0L val startAt: Long get() = if (resume) fileLength else 0L - val exists: Boolean get() = file.exists() + val exists: Boolean get() = file.exists() == true } - //class ADownloadException(val id: Int) : RuntimeException(message = "Download error $id") - - fun UniFile.createFileOrThrow(displayName: String): UniFile { - return this.createFile(displayName) ?: throw IOException("Could not create file") - } - - fun UniFile.deleteOrThrow() { - if (!this.delete()) throw IOException("Could not delete file") - } - - /** - * Sets up the appropriate file and creates a data stream from the file. - * Used for initializing downloads. - * */ @Throws(IOException::class) fun setupStream( context: Context, @@ -552,19 +552,39 @@ object VideoDownloadManager { folder: String?, extension: String, tryResume: Boolean, + ): StreamData { + val (base, _) = context.getBasePath() + return setupStream( + base ?: throw IOException("Bad config"), + name, + folder, + extension, + tryResume + ) + } + + /** + * Sets up the appropriate file and creates a data stream from the file. + * Used for initializing downloads. + * */ + @Throws(IOException::class) + fun setupStream( + baseFile: SafeFile, + name: String, + folder: String?, + extension: String, + tryResume: Boolean, ): StreamData { val displayName = getDisplayName(name, extension) - val (baseFile, _) = context.getBasePath() - - val subDir = baseFile?.gotoDir(folder) ?: throw IOException() + val subDir = baseFile.gotoDirectoryOrThrow(folder) val foundFile = subDir.findFile(displayName) - val (file, fileLength) = if (foundFile == null || !foundFile.exists()) { + val (file, fileLength) = if (foundFile == null || foundFile.exists() != true) { subDir.createFileOrThrow(displayName) to 0L } else { if (tryResume) { - foundFile to foundFile.size() + foundFile to foundFile.lengthOrThrow() } else { foundFile.deleteOrThrow() subDir.createFileOrThrow(displayName) to 0L @@ -1004,21 +1024,20 @@ object VideoDownloadManager { } } - @Throws suspend fun downloadThing( context: Context, link: IDownloadableMinimum, name: String, - folder: String?, + folder: String, extension: String, tryResume: Boolean, parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, parallelConnections: Int = 3 - ): Int = withContext(Dispatchers.IO) { + ): DownloadStatus = withContext(Dispatchers.IO) { // we cant download torrents with this implementation, aria2c might be used in the future - if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) { - return@withContext ERROR_UNKNOWN + if (link.url.startsWith("magnet") || link.url.endsWith(".torrent") || parallelConnections < 1) { + return@withContext DOWNLOAD_INVALID_INPUT } var fileStream: OutputStream? = null @@ -1033,13 +1052,10 @@ object VideoDownloadManager { // get the file path val (baseFile, basePath) = context.getBasePath() val displayName = getDisplayName(name, extension) - val relativePath = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.isDownloadDir()) getRelativePath( - folder - ) else folder + if (baseFile == null) return@withContext DOWNLOAD_BAD_CONFIG // set up the download file - val stream = setupStream(context, name, relativePath, extension, tryResume) + val stream = setupStream(baseFile, name, folder, extension, tryResume) fileStream = stream.open() @@ -1069,7 +1085,7 @@ object VideoDownloadManager { metadata.setDownloadFileInfoTemplate( DownloadedFileInfo( totalBytes = metadata.approxTotalBytes, - relativePath = relativePath ?: "", + relativePath = folder, displayName = displayName, basePath = basePath ) @@ -1202,19 +1218,19 @@ object VideoDownloadManager { if (!stream.exists) metadata.type = DownloadType.IsStopped if (metadata.type == DownloadType.IsFailed) { - return@withContext ERROR_CONNECTION_ERROR + return@withContext DOWNLOAD_FAILED } if (metadata.type == DownloadType.IsStopped) { // we need to close before delete fileStream.closeQuietly() metadata.onDelete() - deleteFile(context, baseFile, relativePath ?: "", displayName) - return@withContext SUCCESS_STOPPED + stream.delete() + return@withContext DOWNLOAD_STOPPED } metadata.type = DownloadType.IsDone - return@withContext SUCCESS_DOWNLOAD_DONE + return@withContext DOWNLOAD_SUCCESS } catch (e: IOException) { // some sort of IO error, this should not happened // we just rethrow it @@ -1226,7 +1242,7 @@ object VideoDownloadManager { // note that when failing we don't want to delete the file, // only user interaction has that power metadata.type = DownloadType.IsFailed - return@withContext ERROR_CONNECTION_ERROR + return@withContext DOWNLOAD_FAILED } finally { fileStream?.closeQuietly() //requestStream?.closeQuietly() @@ -1234,39 +1250,36 @@ object VideoDownloadManager { } } - @Throws private suspend fun downloadHLS( context: Context, link: ExtractorLink, name: String, - folder: String?, + folder: String, parentId: Int?, startIndex: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, parallelConnections: Int = 3 - ): Int = withContext(Dispatchers.IO) { - require(parallelConnections >= 1) + ): DownloadStatus = withContext(Dispatchers.IO) { + if (parallelConnections < 1) return@withContext DOWNLOAD_INVALID_INPUT val metadata = DownloadMetaData( createNotificationCallback = createNotificationCallback, id = parentId ) - val extension = "mp4" - var fileStream: OutputStream? = null try { + val extension = "mp4" + // the start .ts index var startAt = startIndex ?: 0 // set up the file data val (baseFile, basePath) = context.getBasePath() - val relativePath = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.isDownloadDir()) getRelativePath( - folder - ) else folder + if (baseFile == null) return@withContext DOWNLOAD_BAD_CONFIG + val displayName = getDisplayName(name, extension) val stream = - setupStream(context, name, relativePath, extension, startAt > 0) + setupStream(baseFile, name, folder, extension, startAt > 0) if (!stream.resume) startAt = 0 fileStream = stream.open() @@ -1277,7 +1290,7 @@ object VideoDownloadManager { metadata.setDownloadFileInfoTemplate( DownloadedFileInfo( totalBytes = 0, - relativePath = relativePath ?: "", + relativePath = folder, displayName = displayName, basePath = basePath ) @@ -1406,99 +1419,29 @@ object VideoDownloadManager { if (!stream.exists) metadata.type = DownloadType.IsStopped if (metadata.type == DownloadType.IsFailed) { - return@withContext ERROR_CONNECTION_ERROR + return@withContext DOWNLOAD_FAILED } if (metadata.type == DownloadType.IsStopped) { // we need to close before delete fileStream.closeQuietly() metadata.onDelete() - deleteFile(context, baseFile, relativePath ?: "", displayName) - return@withContext SUCCESS_STOPPED + stream.delete() + return@withContext DOWNLOAD_STOPPED } metadata.type = DownloadType.IsDone - return@withContext SUCCESS_DOWNLOAD_DONE + return@withContext DOWNLOAD_SUCCESS } catch (t: Throwable) { logError(t) metadata.type = DownloadType.IsFailed - return@withContext ERROR_UNKNOWN + return@withContext DOWNLOAD_FAILED } finally { fileStream?.closeQuietly() metadata.close() } } - - /** - * Guarantees a directory is present with the dir name (if createMissingDirectories is true). - * Works recursively when '/' is present. - * Will remove any file with the dir name if present and add directory. - * Will not work if the parent directory does not exist. - * - * @param directoryName if null will use the current path. - * @return UniFile / null if createMissingDirectories = false and folder is not found. - * */ - private fun UniFile.gotoDir( - directoryName: String?, - createMissingDirectories: Boolean = true - ): UniFile? { - if(directoryName == null) return this - - return directoryName.split(File.separatorChar).filter { it.isNotBlank() }.fold(this) { file: UniFile?, directory -> - file?.createDirectory(directory) - } - - // May give this error on scoped storage. - // W/DocumentsContract: Failed to create document - // java.lang.IllegalArgumentException: Parent document isn't a directory - - // Not present in latest testing. - - println("Going to dir $directoryName from ${this.uri} ---- ${this.filePath}") - - try { - // Creates itself from parent if doesn't exist. - if (!this.exists() && createMissingDirectories && !this.name.isNullOrBlank()) { - if (this.parentFile != null) { - this.parentFile?.createDirectory(this.name) - } else if (this.filePath != null) { - UniFile.fromFile(File(this.filePath!!).parentFile)?.createDirectory(this.name) - } - } - - val allDirectories = directoryName?.split("/") - return if (allDirectories?.size == 1 || allDirectories == null) { - val found = this.findFile(directoryName) - when { - directoryName.isNullOrBlank() -> this - found?.isDirectory == true -> found - - !createMissingDirectories -> null - // Below creates directories - found?.isFile == true -> { - found.delete() - this.createDirectory(directoryName) - } - - this.isDirectory -> this.createDirectory(directoryName) - else -> this.parentFile?.createDirectory(directoryName) - } - } else { - var currentDirectory = this - allDirectories.forEach { - // If the next directory is not found it returns the deepest directory possible. - val nextDir = currentDirectory.gotoDir(it, createMissingDirectories) - currentDirectory = nextDir ?: return null - } - currentDirectory - } - } catch (e: Exception) { - logError(e) - return null - } - } - private fun getDisplayName(name: String, extension: String): String { return "$name.$extension" } @@ -1510,33 +1453,22 @@ object VideoDownloadManager { * As of writing UniFile is used for everything but download directory on scoped storage. * Special ContentResolver fuckery is needed for that as UniFile doesn't work. * */ - fun getDownloadDir(): UniFile? { + fun getDefaultDir(context: Context): SafeFile? { // See https://www.py4u.net/discuss/614761 - return UniFile.fromFile( - File( - Environment.getExternalStorageDirectory().absolutePath + File.separatorChar + - Environment.DIRECTORY_DOWNLOADS - ) + return SafeFile.fromMedia( + context, MediaFileContentType.Downloads ) } - @Deprecated("TODO fix UniFile to work with download directory.") - private fun getRelativePath(folder: String?): String { - return (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace( - '/', - File.separatorChar - ).replace("${File.separatorChar}${File.separatorChar}", File.separatorChar.toString()) - } - /** * Turns a string to an UniFile. Used for stored string paths such as settings. * Should only be used to get a download path. * */ - private fun basePathToFile(context: Context, path: String?): UniFile? { + private fun basePathToFile(context: Context, path: String?): SafeFile? { return when { - path.isNullOrBlank() -> getDownloadDir() - path.startsWith("content://") -> UniFile.fromUri(context, path.toUri()) - else -> UniFile.fromFile(File(path)) + path.isNullOrBlank() -> getDefaultDir(context) + path.startsWith("content://") -> SafeFile.fromUri(context, path.toUri()) + else -> SafeFile.fromFile(context, File(path)) } } @@ -1545,17 +1477,12 @@ object VideoDownloadManager { * Returns the file and a string to be stored for future file retrieval. * UniFile.filePath is not sufficient for storage. * */ - fun Context.getBasePath(): Pair { + fun Context.getBasePath(): Pair { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val basePathSetting = settingsManager.getString(getString(R.string.download_path_key), null) return basePathToFile(this, basePathSetting) to basePathSetting } - fun UniFile?.isDownloadDir(): Boolean { - return this != null && this.filePath == getDownloadDir()?.filePath - } - - fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { return getFileName(context, metadata.name, metadata.episode, metadata.season) } @@ -1596,7 +1523,7 @@ object VideoDownloadManager { link: ExtractorLink, notificationCallback: (Int, Notification) -> Unit, tryResume: Boolean = false, - ): Int { + ): DownloadStatus { val name = getFileName(context, ep) // Make sure this is cancelled when download is done or cancelled. @@ -1638,7 +1565,7 @@ object VideoDownloadManager { context, link, name, - folder, + folder ?: "", ep.id, startIndex, callback @@ -1648,7 +1575,7 @@ object VideoDownloadManager { context, link, name, - folder, + folder ?: "", "mp4", tryResume, ep.id, @@ -1656,7 +1583,7 @@ object VideoDownloadManager { ) } } catch (t: Throwable) { - return ERROR_UNKNOWN + return DOWNLOAD_FAILED } finally { extractorJob.cancel() } @@ -1698,10 +1625,8 @@ object VideoDownloadManager { notificationCallback, resume ) - //.also { println("Single episode finished with return code: $it") } - // retry every link at least once - if (connectionResult <= 0) { + if (connectionResult.retrySame) { connectionResult = downloadSingleEpisode( context, item.source, @@ -1713,11 +1638,12 @@ object VideoDownloadManager { ) } - if (connectionResult > 0) { // SUCCESS + if (connectionResult.success) { // SUCCESS removeKey(KEY_RESUME_PACKAGES, id.toString()) break - } else if (index == item.links.lastIndex) { + } else if (!connectionResult.tryNext || index >= item.links.lastIndex) { downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) + break } } } catch (e: Exception) { @@ -1731,62 +1657,69 @@ object VideoDownloadManager { // return id } - fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? { - val res = getDownloadFileInfo(context, id) - if (res == null) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) - return res + /* fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? { + val res = getDownloadFileInfo(context, id) + if (res == null) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) + return res + } + */ + fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? = + getDownloadFileInfo(context, id, removeKeys = true) + + private fun DownloadedFileInfo.toFile(context: Context): SafeFile? { + return basePathToFile(context, this.basePath)?.gotoDirectory(relativePath) + ?.findFile(displayName) } - private fun getDownloadFileInfo(context: Context, id: Int): DownloadedFileInfoResult? { + private fun getDownloadFileInfo( + context: Context, + id: Int, + removeKeys: Boolean = false + ): DownloadedFileInfoResult? { try { val info = context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return null - val base = basePathToFile(context, info.basePath) - val file = base?.gotoDir(info.relativePath, false)?.findFile(info.displayName) - if (file?.exists() != true) return null + val file = info.toFile(context) - return DownloadedFileInfoResult(file.size(), info.totalBytes, file.uri) + // only delete the key if the file is not found + if (file == null || !file.existsOrThrow()) { + if (removeKeys) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) + return null + } + + return DownloadedFileInfoResult( + file.lengthOrThrow(), + info.totalBytes, + file.uriOrThrow() + ) } catch (e: Exception) { logError(e) return null } } - /** - * Gets the true download size as Scoped Storage sometimes wrongly returns 0. - * */ - fun UniFile.size(): Long { - val len = length() - return if (len <= 1) { - println("LEN:::::::>>>>>>>>>>>>>>>>>>>>>>>$len") - val inputStream = this.openInputStream() - return inputStream.available().toLong().also { inputStream.closeQuietly() } - } else { - len - } - } - fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { val success = deleteFile(context, id) if (success) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) return success } - private fun deleteFile( + /*private fun deleteFile( context: Context, - folder: UniFile?, + folder: SafeFile?, relativePath: String, displayName: String ): Boolean { - val file = folder?.gotoDir(relativePath)?.findFile(displayName) ?: return false - if (!file.exists()) return true + val file = folder?.gotoDirectory(relativePath)?.findFile(displayName) ?: return false + if (file.exists() == false) return true return try { file.delete() } catch (e: Exception) { logError(e) - (context.contentResolver?.delete(file.uri, null, null) ?: return false) > 0 + (context.contentResolver?.delete(file.uri() ?: return true, null, null) + ?: return false) > 0 } - } + }*/ private fun deleteFile(context: Context, id: Int): Boolean { val info = @@ -1795,8 +1728,7 @@ object VideoDownloadManager { downloadProgressEvent.invoke(Triple(id, 0, 0)) downloadStatusEvent.invoke(id to DownloadType.IsStopped) downloadDeleteEvent.invoke(id) - val base = basePathToFile(context, info.basePath) - return deleteFile(context, base, info.relativePath, info.displayName) + return info.toFile(context)?.delete() ?: false } fun getDownloadResumePackage(context: Context, id: Int): DownloadResumePackage? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt new file mode 100644 index 00000000..83c66b8b --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt @@ -0,0 +1,369 @@ +package com.lagradost.cloudstream3.utils.storage + +import android.content.ContentResolver +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import androidx.annotation.RequiresApi +import com.hippo.unifile.UniRandomAccessFile +import com.lagradost.cloudstream3.mvvm.logError +import okhttp3.internal.closeQuietly +import java.io.File +import java.io.InputStream +import java.io.OutputStream + + +enum class MediaFileContentType { + Downloads, + Audio, + Video, + Images, +} + +// https://developer.android.com/training/data-storage/shared/media +fun MediaFileContentType.toPath(): String { + return when (this) { + MediaFileContentType.Downloads -> Environment.DIRECTORY_DOWNLOADS + MediaFileContentType.Audio -> Environment.DIRECTORY_MUSIC + MediaFileContentType.Video -> Environment.DIRECTORY_MOVIES + MediaFileContentType.Images -> Environment.DIRECTORY_DCIM + } +} + +fun MediaFileContentType.defaultPrefix(): String { + return Environment.getExternalStorageDirectory().absolutePath +} + +fun MediaFileContentType.toAbsolutePath(): String { + return defaultPrefix() + File.separator + + this.toPath() +} + +fun replaceDuplicateFileSeparators(path: String): String { + return path.replace(Regex("${File.separator}+"), File.separator) +} + +@RequiresApi(Build.VERSION_CODES.Q) +fun MediaFileContentType.toUri(external: Boolean): Uri { + val volume = if (external) MediaStore.VOLUME_EXTERNAL_PRIMARY else MediaStore.VOLUME_INTERNAL + return when (this) { + MediaFileContentType.Downloads -> MediaStore.Downloads.getContentUri(volume) + MediaFileContentType.Audio -> MediaStore.Audio.Media.getContentUri(volume) + MediaFileContentType.Video -> MediaStore.Video.Media.getContentUri(volume) + MediaFileContentType.Images -> MediaStore.Images.Media.getContentUri(volume) + } +} + +@RequiresApi(Build.VERSION_CODES.Q) +class MediaFile( + private val context: Context, + private val folderType: MediaFileContentType, + private val external: Boolean = true, + absolutePath: String, +) : SafeFile { + // this is the path relative to the download directory so "/hello/text.txt" = "hello/text.txt" is in fact "Download/hello/text.txt" + private val sanitizedAbsolutePath: String = + replaceDuplicateFileSeparators(absolutePath) + + // this is only a directory if the filepath ends with a / + private val isDir: Boolean = sanitizedAbsolutePath.endsWith(File.separator) + private val isFile: Boolean = !isDir + + // this is the relative path including the Download directory, so "/hello/text.txt" => "Download/hello" + private val relativePath: String = + replaceDuplicateFileSeparators(folderType.toPath() + File.separator + sanitizedAbsolutePath).substringBeforeLast( + File.separator + ) + + // "/hello/text.txt" => "text.txt" + private val namePath: String = sanitizedAbsolutePath.substringAfterLast(File.separator) + private val baseUri = folderType.toUri(external) + private val contentResolver: ContentResolver = context.contentResolver + + init { + // some standard asserts that should always be hold or else this class wont work + assert(!relativePath.endsWith(File.separator)) + assert(!(isDir && isFile)) + assert(!relativePath.contains(File.separator + File.separator)) + assert(!namePath.contains(File.separator)) + + if (isDir) { + assert(namePath.isBlank()) + } else { + assert(namePath.isNotBlank()) + } + } + + companion object { + private fun splitFilenameExt(name: String): Pair { + val split = name.indexOfLast { it == '.' } + if (split <= 0) return name to null + val ext = name.substring(split + 1 until name.length) + if (ext.isBlank()) return name to null + + return name.substring(0 until split) to ext + } + + private fun splitFilenameMime(name: String): Pair { + val (display, ext) = splitFilenameExt(name) + val mimeType = when (ext) { + + // Absolutely ridiculous, if text/vtt is used as mimetype scoped storage prevents + // downloading to /Downloads yet it works with null + + "vtt" -> null // "text/vtt" + "mp4" -> "video/mp4" + "srt" -> null // "application/x-subrip"//"text/plain" + else -> null + } + return display to mimeType + } + } + + private fun appendRelativePath(path: String, folder: Boolean): MediaFile? { + if (isFile) return null + + // VideoDownloadManager.sanitizeFilename(path.replace(File.separator, "")) + + val newPath = + sanitizedAbsolutePath + path + if (folder) File.separator else "" + + return MediaFile( + context = context, + folderType = folderType, + external = external, + absolutePath = newPath + ) + } + + private fun createUri(displayName: String? = namePath): Uri? { + if (displayName == null) return null + if (isFile) return null + val (name, mime) = splitFilenameMime(displayName) + + val newFile = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) + put(MediaStore.MediaColumns.TITLE, name) + if (mime != null) + put(MediaStore.MediaColumns.MIME_TYPE, mime) + put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath) + } + return contentResolver.insert(baseUri, newFile) + } + + override fun createFile(displayName: String?): SafeFile? { + if (isFile || displayName == null) return null + query(displayName)?.uri ?: createUri(displayName) ?: return null + return appendRelativePath(displayName, false) //SafeFile.fromUri(context, ?: return null) + } + + override fun createDirectory(directoryName: String?): SafeFile? { + if (directoryName == null) return null + // we don't create a dir here tbh, just fake create it + return appendRelativePath(directoryName, true) + } + + private data class QueryResult( + val uri: Uri, + val lastModified: Long, + val length: Long, + ) + + @RequiresApi(Build.VERSION_CODES.Q) + private fun query(displayName: String = namePath): QueryResult? { + try { + //val (name, mime) = splitFilenameMime(fullName) + + val projection = arrayOf( + MediaStore.MediaColumns._ID, + MediaStore.MediaColumns.DATE_MODIFIED, + MediaStore.MediaColumns.SIZE, + ) + + val selection = + "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}' AND ${MediaStore.MediaColumns.DISPLAY_NAME}='$displayName'" + + contentResolver.query( + baseUri, + projection, selection, null, null + )?.use { cursor -> + while (cursor.moveToNext()) { + val id = + cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) + + return QueryResult( + uri = ContentUris.withAppendedId( + baseUri, id + ), + lastModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)), + length = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)), + ) + } + } + } catch (t: Throwable) { + logError(t) + } + + return null + } + + override fun uri(): Uri? { + return query()?.uri + } + + override fun name(): String? { + if (isDir) return null + return namePath + } + + override fun type(): String? { + TODO("Not yet implemented") + } + + override fun filePath(): String { + return replaceDuplicateFileSeparators(relativePath + File.separator + namePath) + } + + override fun isDirectory(): Boolean { + return isDir + } + + override fun isFile(): Boolean { + return isFile + } + + override fun lastModified(): Long? { + if (isDir) return null + return query()?.lastModified + } + + override fun length(): Long? { + if (isDir) return null + val length = query()?.length ?: return null + if(length <= 0) { + val inputStream : InputStream = openInputStream() ?: return null + return try { + inputStream.available().toLong() + } catch (t : Throwable) { + null + } finally { + inputStream.closeQuietly() + } + } + return length + } + + override fun canRead(): Boolean { + TODO("Not yet implemented") + } + + override fun canWrite(): Boolean { + TODO("Not yet implemented") + } + + private fun delete(uri: Uri): Boolean { + return contentResolver.delete(uri, null, null) > 0 + } + + override fun delete(): Boolean { + return if (isDir) { + (listFiles() ?: return false).all { + it.delete() + } + } else { + delete(uri() ?: return false) + } + } + + override fun exists(): Boolean { + if (isDir) return true + return query() != null + } + + override fun listFiles(): List? { + if (isFile) return null + try { + val projection = arrayOf( + MediaStore.MediaColumns.DISPLAY_NAME + ) + + val selection = + "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}'" + contentResolver.query( + baseUri, + projection, selection, null, null + )?.use { cursor -> + val out = ArrayList(cursor.count) + while (cursor.moveToNext()) { + val nameIdx = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) + if (nameIdx == -1) continue + val name = cursor.getString(nameIdx) + + appendRelativePath(name, false)?.let { new -> + out.add(new) + } + } + + out + } + } catch (t: Throwable) { + logError(t) + } + return null + } + + override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? { + if (isFile || displayName == null) return null + + val new = appendRelativePath(displayName, false) ?: return null + if (new.exists()) { + return new + } + + return null//SafeFile.fromUri(context, query(displayName ?: return null)?.uri ?: return null) + } + + override fun renameTo(name: String?): Boolean { + TODO("Not yet implemented") + } + + override fun openOutputStream(append: Boolean): OutputStream? { + try { + // use current file + uri()?.let { + return contentResolver.openOutputStream( + it, + if (append) "wa" else "wt" + ) + } + + // create a new file if current is not found, + // as we know it is new only write access is needed + createUri()?.let { + return contentResolver.openOutputStream( + it, + "w" + ) + } + return null + } catch (t: Throwable) { + return null + } + } + + override fun openInputStream(): InputStream? { + try { + return contentResolver.openInputStream(uri() ?: return null) + } catch (t: Throwable) { + return null + } + } + + override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt new file mode 100644 index 00000000..9ba0ef88 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt @@ -0,0 +1,244 @@ +package com.lagradost.cloudstream3.utils.storage + +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Environment +import com.hippo.unifile.UniFile +import com.hippo.unifile.UniRandomAccessFile + +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +interface SafeFile { + companion object { + fun fromUri(context: Context, uri: Uri): SafeFile? { + return UniFileWrapper(UniFile.fromUri(context, uri) ?: return null) + } + + fun fromFile(context: Context, file: File?): SafeFile? { + if (file == null) return null + // because UniFile sucks balls on Media we have to do this + val absPath = file.absolutePath.removePrefix(File.separator) + for (value in MediaFileContentType.values()) { + val prefixes = listOf(value.toAbsolutePath(), value.toPath()) + for (prefix in prefixes) { + if (!absPath.startsWith(prefix)) continue + return fromMedia( + context, + value, + absPath.removePrefix(prefix).ifBlank { File.separator } + ) + } + } + + return UniFileWrapper(UniFile.fromFile(file) ?: return null) + } + + fun fromAsset( + context: Context, + filename: String? + ): SafeFile? { + return UniFileWrapper( + UniFile.fromAsset(context.assets, filename ?: return null) ?: return null + ) + } + + fun fromResource( + context: Context, + id: Int + ): SafeFile? { + return UniFileWrapper( + UniFile.fromResource(context, id) ?: return null + ) + } + + fun fromMedia( + context: Context, + folderType: MediaFileContentType, + path: String = File.separator, + external: Boolean = true, + ): SafeFile? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + //fromUri(context, folderType.toUri(external))?.findFile(folderType.toPath())?.gotoDirectory(path) + + return MediaFile( + context = context, + folderType = folderType, + external = external, + absolutePath = path + ) + } else { + fromFile( + context, + File( + (Environment.getExternalStorageDirectory().absolutePath + File.separator + + folderType.toPath() + File.separator + folderType).replace( + File.separator + File.separator, + File.separator + ) + ) + ) + } + + } + } + + /*val uri: Uri? get() = getUri() + val name: String? get() = getName() + val type: String? get() = getType() + val filePath: String? get() = getFilePath() + val isFile: Boolean? get() = isFile() + val isDirectory: Boolean? get() = isDirectory() + val length: Long? get() = length() + val canRead: Boolean get() = canRead() + val canWrite: Boolean get() = canWrite() + val lastModified: Long? get() = lastModified()*/ + + @Throws(IOException::class) + fun isFileOrThrow(): Boolean { + return isFile() ?: throw IOException("Unable to get if file is a file or directory") + } + + @Throws(IOException::class) + fun lengthOrThrow(): Long { + return length() ?: throw IOException("Unable to get file length") + } + + @Throws(IOException::class) + fun isDirectoryOrThrow(): Boolean { + return isDirectory() ?: throw IOException("Unable to get if file is a directory") + } + + @Throws(IOException::class) + fun filePathOrThrow(): String { + return filePath() ?: throw IOException("Unable to get file path") + } + + @Throws(IOException::class) + fun uriOrThrow(): Uri { + return uri() ?: throw IOException("Unable to get uri") + } + + @Throws(IOException::class) + fun renameOrThrow(name: String?) { + if (!renameTo(name)) { + throw IOException("Unable to rename to $name") + } + } + + @Throws(IOException::class) + fun openOutputStreamOrThrow(append: Boolean = false): OutputStream { + return openOutputStream(append) ?: throw IOException("Unable to open output stream") + } + + @Throws(IOException::class) + fun openInputStreamOrThrow(): InputStream { + return openInputStream() ?: throw IOException("Unable to open input stream") + } + + @Throws(IOException::class) + fun existsOrThrow(): Boolean { + return exists() ?: throw IOException("Unable get if file exists") + } + + @Throws(IOException::class) + fun findFileOrThrow(displayName: String?, ignoreCase: Boolean = false): SafeFile { + return findFile(displayName, ignoreCase) ?: throw IOException("Unable find file") + } + + @Throws(IOException::class) + fun gotoDirectoryOrThrow( + directoryName: String?, + createMissingDirectories: Boolean = true + ): SafeFile { + return gotoDirectory(directoryName, createMissingDirectories) + ?: throw IOException("Unable to go to directory $directoryName") + } + + @Throws(IOException::class) + fun listFilesOrThrow(): List { + return listFiles() ?: throw IOException("Unable to get files") + } + + + @Throws(IOException::class) + fun createFileOrThrow(displayName: String?): SafeFile { + return createFile(displayName) ?: throw IOException("Unable to create file $displayName") + } + + @Throws(IOException::class) + fun createDirectoryOrThrow(directoryName: String?): SafeFile { + return createDirectory( + directoryName ?: throw IOException("Unable to create file with invalid name") + ) + ?: throw IOException("Unable to create directory $directoryName") + } + + @Throws(IOException::class) + fun deleteOrThrow() { + if (!delete()) { + throw IOException("Unable to delete file") + } + } + + /** file.gotoDirectory("a/b/c") -> "file/a/b/c/" where a null or blank directoryName + * returns itself. createMissingDirectories specifies if the dirs should be created + * when travelling or break at a dir not found */ + fun gotoDirectory( + directoryName: String?, + createMissingDirectories: Boolean = true + ): SafeFile? { + if (directoryName == null) return this + + return directoryName.split(File.separatorChar).filter { it.isNotBlank() } + .fold(this) { file: SafeFile?, directory -> + // as MediaFile does not actually create a directory we can do this + if (createMissingDirectories || this is MediaFile) { + file?.createDirectory(directory) + } else { + val next = file?.findFile(directory) + + // we require the file to be a directory + if (next?.isDirectory() != true) { + null + } else { + next + } + } + } + } + + + fun createFile(displayName: String?): SafeFile? + fun createDirectory(directoryName: String?): SafeFile? + fun uri(): Uri? + fun name(): String? + fun type(): String? + fun filePath(): String? + fun isDirectory(): Boolean? + fun isFile(): Boolean? + fun lastModified(): Long? + fun length(): Long? + fun canRead(): Boolean + fun canWrite(): Boolean + fun delete(): Boolean + fun exists(): Boolean? + fun listFiles(): List? + + // fun listFiles(filter: FilenameFilter?): Array? + fun findFile(displayName: String?, ignoreCase: Boolean = false): SafeFile? + + fun renameTo(name: String?): Boolean + + /** Open a stream on to the content associated with the file */ + fun openOutputStream(append: Boolean = false): OutputStream? + + /** Open a stream on to the content associated with the file */ + fun openInputStream(): InputStream? + + /** Get a random access stuff of the UniFile, "r" or "rw" */ + fun createRandomAccessFile(mode: String?): UniRandomAccessFile? +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt new file mode 100644 index 00000000..f1592169 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt @@ -0,0 +1,116 @@ +package com.lagradost.cloudstream3.utils.storage + +import android.net.Uri +import com.hippo.unifile.UniFile +import com.hippo.unifile.UniRandomAccessFile +import com.lagradost.cloudstream3.mvvm.logError +import okhttp3.internal.closeQuietly +import java.io.InputStream +import java.io.OutputStream + +private fun UniFile.toFile(): SafeFile { + return UniFileWrapper(this) +} + +fun safe(apiCall: () -> T): T? { + return try { + apiCall.invoke() + } catch (throwable: Throwable) { + logError(throwable) + null + } +} + +class UniFileWrapper(val file: UniFile) : SafeFile { + override fun createFile(displayName: String?): SafeFile? { + return file.createFile(displayName)?.toFile() + } + + override fun createDirectory(directoryName: String?): SafeFile? { + return file.createDirectory(directoryName)?.toFile() + } + + override fun uri(): Uri? { + return safe { file.uri } + } + + override fun name(): String? { + return safe { file.name } + } + + override fun type(): String? { + return safe { file.type } + } + + override fun filePath(): String? { + return safe { file.filePath } + } + + override fun isDirectory(): Boolean? { + return safe { file.isDirectory } + } + + override fun isFile(): Boolean? { + return safe { file.isFile } + } + + override fun lastModified(): Long? { + return safe { file.lastModified() } + } + + override fun length(): Long? { + return safe { + val len = file.length() + if (len <= 1) { + val inputStream = this.openInputStream() ?: return@safe null + try { + inputStream.available().toLong() + } finally { + inputStream.closeQuietly() + } + } else { + len + } + } + } + + override fun canRead(): Boolean { + return safe { file.canRead() } ?: false + } + + override fun canWrite(): Boolean { + return safe { file.canWrite() } ?: false + } + + override fun delete(): Boolean { + return safe { file.delete() } ?: false + } + + override fun exists(): Boolean? { + return safe { file.exists() } + } + + override fun listFiles(): List? { + return safe { file.listFiles()?.mapNotNull { it?.toFile() } } + } + + override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? { + return safe { file.findFile(displayName, ignoreCase)?.toFile() } + } + + override fun renameTo(name: String?): Boolean { + return safe { file.renameTo(name) } ?: return false + } + + override fun openOutputStream(append: Boolean): OutputStream? { + return safe { file.openOutputStream(append) } + } + + override fun openInputStream(): InputStream? { + return safe { file.openInputStream() } + } + + override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? { + return safe { file.createRandomAccessFile(mode) } + } +} \ No newline at end of file From d436171a2f89f58107d5bc48d2a6be2fbb8845eb Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 23 Aug 2023 06:36:43 +0200 Subject: [PATCH 104/156] removed possible duplicate download queue --- .../lagradost/cloudstream3/utils/VideoDownloadManager.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 37c02be4..442fa32f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -1598,9 +1598,8 @@ object VideoDownloadManager { val item = pkg.item val id = item.ep.id if (currentDownloads.contains(id)) { // IF IT IS ALREADY DOWNLOADING, RESUME IT - downloadEvent.invoke(Pair(id, DownloadActionType.Resume)) - /** ID needs to be returned to the work-manager to properly await notification */ - // return id + downloadEvent.invoke(id to DownloadActionType.Resume) + return } currentDownloads.add(id) @@ -1741,14 +1740,14 @@ object VideoDownloadManager { notificationCallback: (Int, Notification) -> Unit, setKey: Boolean = true ) { - if (!currentDownloads.any { it == pkg.item.ep.id }) { + if (!currentDownloads.any { it == pkg.item.ep.id } && !downloadQueue.any { it.item.ep.id == pkg.item.ep.id }) { downloadQueue.addLast(pkg) downloadCheck(context, notificationCallback) if (setKey) saveQueue() //ret } else { downloadEvent( - Pair(pkg.item.ep.id, DownloadActionType.Resume) + pkg.item.ep.id to DownloadActionType.Resume ) //null } From bac2ee980551d194b70866fc1580ecf40455e024 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 23 Aug 2023 17:08:26 +0200 Subject: [PATCH 105/156] fixed div by zero --- .../com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index eb8cb9b3..91e97dfc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -44,6 +44,8 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed { private fun fixPlayerSize() { playerWidthHeight?.let { (w, h) -> + if(w <= 0 || h <= 0) return@let + val orientation = context?.resources?.configuration?.orientation ?: return val sw = if (orientation == Configuration.ORIENTATION_LANDSCAPE) { From e2502de02cdf226ab4c37cbc71743c68896f5745 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 23 Aug 2023 18:43:55 +0200 Subject: [PATCH 106/156] bump acra --- app/build.gradle.kts | 2 +- .../main/java/com/lagradost/cloudstream3/AcraApplication.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 50125aa3..178b49c2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,7 +58,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.1.6" + versionName = "4.1.7" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 32702657..c14780d8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -43,9 +43,9 @@ class CustomReportSender : ReportSender { override fun send(context: Context, errorContent: CrashReportData) { println("Sending report") val url = - "https://docs.google.com/forms/d/e/1FAIpQLSdOlbgCx7NeaxjvEGyEQlqdh2nCvwjm2vwpP1VwW7REj9Ri3Q/formResponse" + "https://docs.google.com/forms/d/e/1FAIpQLSfO4r353BJ79TTY_-t5KWSIJT2xfqcQWY81xjAA1-1N0U2eSg/formResponse" val data = mapOf( - "entry.753293084" to errorContent.toJSON() + "entry.1993829403" to errorContent.toJSON() ) thread { // to not run it on main thread From 5bad6aca352506c722749fa6c1b5123ae722a071 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 23 Aug 2023 23:57:54 +0200 Subject: [PATCH 107/156] fixed native crash handle --- .../com/lagradost/cloudstream3/AcraApplication.kt | 11 ++++++++--- .../com/lagradost/cloudstream3/NativeCrashHandler.kt | 11 ++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index c14780d8..4b4747ae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -104,13 +104,17 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) : } class AcraApplication : Application() { + override fun onCreate() { super.onCreate() NativeCrashHandler.initCrashHandler() - Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) { + ExceptionHandler(filesDir.resolve("last_error")) { val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) startActivity(Intent.makeRestartActivityTask(intent!!.component)) - }) + }.also { + exceptionHandler = it + Thread.setDefaultUncaughtExceptionHandler(it) + } } override fun attachBaseContext(base: Context?) { @@ -138,6 +142,8 @@ class AcraApplication : Application() { } companion object { + var exceptionHandler: ExceptionHandler? = null + /** Use to get activity from Context */ tailrec fun Context.getActivity(): Activity? = this as? Activity ?: (this as? ContextWrapper)?.baseContext?.getActivity() @@ -212,6 +218,5 @@ class AcraApplication : Application() { activity?.supportFragmentManager?.fragments?.lastOrNull() ) } - } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt index e5cb2702..1fe00748 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt @@ -14,6 +14,12 @@ object NativeCrashHandler { private external fun getSignalStatus(): Int private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch { + + //launch { + // delay(10000) + // triggerNativeCrash() + //} + while (true) { delay(10_000) val signal = getSignalStatus() @@ -24,7 +30,10 @@ object NativeCrashHandler { if (lastError != null) continue if (checkSafeModeFile()) continue - throw RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n") + AcraApplication.exceptionHandler?.uncaughtException( + Thread.currentThread(), + RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n") + ) } } From 2c0e40a233d816f55c58559dfb40308eee62fdac Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Thu, 24 Aug 2023 00:23:06 +0200 Subject: [PATCH 108/156] Lower targetSdk to get all installed packages --- app/build.gradle.kts | 2 +- app/src/main/AndroidManifest.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 178b49c2..dfd2c173 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -55,7 +55,7 @@ android { defaultConfig { applicationId = "com.lagradost.cloudstream3" minSdk = 21 - targetSdk = 33 + targetSdk = 29 versionCode = 59 versionName = "4.1.7" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 563c82f8..0e716034 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ - + Date: Thu, 24 Aug 2023 00:25:05 +0200 Subject: [PATCH 109/156] reverted low api crash handle crashing --- app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 4b4747ae..5f3162b4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -107,7 +107,7 @@ class AcraApplication : Application() { override fun onCreate() { super.onCreate() - NativeCrashHandler.initCrashHandler() + //NativeCrashHandler.initCrashHandler() ExceptionHandler(filesDir.resolve("last_error")) { val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) startActivity(Intent.makeRestartActivityTask(intent!!.component)) From 9a1358e295cf9761d3e8c9bba04ef214dcfc96ca Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Thu, 24 Aug 2023 14:16:33 +0000 Subject: [PATCH 110/156] Lower targetSdk to get all installed packages (#571) --- app/build.gradle.kts | 2 +- app/src/main/AndroidManifest.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 178b49c2..dfd2c173 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -55,7 +55,7 @@ android { defaultConfig { applicationId = "com.lagradost.cloudstream3" minSdk = 21 - targetSdk = 33 + targetSdk = 29 versionCode = 59 versionName = "4.1.7" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 563c82f8..0e716034 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ - + Date: Thu, 24 Aug 2023 16:39:50 +0200 Subject: [PATCH 111/156] fixed removal of predownloaded files --- .../java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt index 9ba0ef88..85a74963 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt @@ -23,7 +23,7 @@ interface SafeFile { // because UniFile sucks balls on Media we have to do this val absPath = file.absolutePath.removePrefix(File.separator) for (value in MediaFileContentType.values()) { - val prefixes = listOf(value.toAbsolutePath(), value.toPath()) + val prefixes = listOf(value.toAbsolutePath(), value.toPath()).map { it.removePrefix(File.separator) } for (prefix in prefixes) { if (!absPath.startsWith(prefix)) continue return fromMedia( From c92ac3e8b3502f26ed812647134dc0977218831e Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 24 Aug 2023 18:13:42 +0200 Subject: [PATCH 112/156] fixed removal of predownloaded files 2 + permission --- app/src/main/AndroidManifest.xml | 2 +- .../java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0e716034..15767d7b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ - + Download + if(relativePath == path) return this + val newPath = sanitizedAbsolutePath + path + if (folder) File.separator else "" From 9b4701fe91858c71fa32ecec98c3fb05451dfc6c Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 24 Aug 2023 18:14:54 +0200 Subject: [PATCH 113/156] dont remove keys while this is tested --- .../com/lagradost/cloudstream3/utils/VideoDownloadManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 442fa32f..948d7b8a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -1682,7 +1682,7 @@ object VideoDownloadManager { // only delete the key if the file is not found if (file == null || !file.existsOrThrow()) { - if (removeKeys) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) + //if (removeKeys) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) // TODO READD return null } From 1a4cbcaea048e9d057f9c70e37a7a46330a0203c Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 24 Aug 2023 21:17:42 +0200 Subject: [PATCH 114/156] small fix --- .../ui/result/ResultViewModel2.kt | 4 +-- .../cloudstream3/utils/storage/MediaFile.kt | 27 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index bdd27091..82d9a8fe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -591,7 +591,7 @@ class ResultViewModel2 : ViewModel() { link, "$fileName ${link.name}", folder, - if (link.url.contains(".srt")) ".srt" else "vtt", + if (link.url.contains(".srt")) "srt" else "vtt", false, null, createNotificationCallback = {} ) @@ -719,7 +719,7 @@ class ResultViewModel2 : ViewModel() { ) ) } - .map { ExtractorSubtitleLink(it.name, it.url, "") } + .map { ExtractorSubtitleLink(it.name, it.url, "") }.take(3) .forEach { link -> val fileName = VideoDownloadManager.getFileName(context, meta) downloadSubtitle(context, link, fileName, folder) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt index 526d31ca..51b8adfe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt @@ -13,6 +13,7 @@ import com.hippo.unifile.UniRandomAccessFile import com.lagradost.cloudstream3.mvvm.logError import okhttp3.internal.closeQuietly import java.io.File +import java.io.FileNotFoundException import java.io.InputStream import java.io.OutputStream @@ -65,6 +66,10 @@ class MediaFile( private val external: Boolean = true, absolutePath: String, ) : SafeFile { + override fun toString(): String { + return sanitizedAbsolutePath + } + // this is the path relative to the download directory so "/hello/text.txt" = "hello/text.txt" is in fact "Download/hello/text.txt" private val sanitizedAbsolutePath: String = replaceDuplicateFileSeparators(absolutePath) @@ -130,7 +135,7 @@ class MediaFile( // VideoDownloadManager.sanitizeFilename(path.replace(File.separator, "")) // in case of duplicate path, aka Download -> Download - if(relativePath == path) return this + if (relativePath == path) return this val newPath = sanitizedAbsolutePath + path + if (folder) File.separator else "" @@ -246,12 +251,24 @@ class MediaFile( override fun length(): Long? { if (isDir) return null - val length = query()?.length ?: return null - if(length <= 0) { - val inputStream : InputStream = openInputStream() ?: return null + val query = query() + val length = query?.length ?: return null + if (length <= 0) { + try { + contentResolver.openFileDescriptor(query.uri, "r") + .use { + it?.statSize + }?.let { + return it + } + } catch (e: FileNotFoundException) { + return null + } + + val inputStream: InputStream = openInputStream() ?: return null return try { inputStream.available().toLong() - } catch (t : Throwable) { + } catch (t: Throwable) { null } finally { inputStream.closeQuietly() From b38a9b1ff5d6e1c5de21851af089232fca7c985b Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 24 Aug 2023 21:39:05 +0200 Subject: [PATCH 115/156] fuck android --- .../com/lagradost/cloudstream3/utils/VideoDownloadManager.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 948d7b8a..7bd863ae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -62,6 +62,7 @@ const val DOWNLOAD_CHANNEL_DESCRIPT = "The download notification channel" object VideoDownloadManager { var maxConcurrentDownloads = 3 + var maxConcurrentConnections = 3 private var currentDownloads = mutableListOf() private const val USER_AGENT = @@ -1568,7 +1569,7 @@ object VideoDownloadManager { folder ?: "", ep.id, startIndex, - callback + callback, parallelConnections = maxConcurrentConnections ) } else { return downloadThing( @@ -1579,7 +1580,7 @@ object VideoDownloadManager { "mp4", tryResume, ep.id, - callback + callback, parallelConnections = maxConcurrentConnections ) } } catch (t: Throwable) { From 2d82480398c2dd5b0dfa5b0c0db7c626658aafd2 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Fri, 25 Aug 2023 15:58:58 +0700 Subject: [PATCH 116/156] fix Rabbitstream (#573) Co-authored-by: Sofie99 --- .../com/lagradost/cloudstream3/extractors/Rabbitstream.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt index b686f7d8..0154b4e8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt @@ -36,7 +36,6 @@ open class Rabbitstream : ExtractorApi() { override val requiresReferer = false open val embed = "ajax/embed-4" open val key = "https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt" - private var rawKey: String? = null override suspend fun getUrl( url: String, @@ -82,9 +81,10 @@ open class Rabbitstream : ExtractorApi() { ) } + } - private suspend fun getRawKey(): String = rawKey ?: app.get(key).text.also { rawKey = it } + private suspend fun getRawKey(): String = app.get(key).text private fun extractRealKey(originalString: String?, stops: String): Pair { val table = parseJson>>(stops) From d0c03321b90b13142dce2ab5931c13db48e4a90d Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Fri, 25 Aug 2023 10:59:18 +0200 Subject: [PATCH 117/156] Translations update from Hosted Weblate (#568) Co-authored-by: Carlos Luiz Co-authored-by: Joel Brink Co-authored-by: Julian Co-authored-by: Mubarek Seyd Juhar Co-authored-by: Sam Cooper Co-authored-by: Skrripy Co-authored-by: mbottari Co-authored-by: tabtomi8 --- app/src/main/res/values-ajp/strings.xml | 2 + app/src/main/res/values-am/strings.xml | 5 + app/src/main/res/values-ars/strings.xml | 203 +++++++++++++++++- app/src/main/res/values-bp/strings.xml | 69 +++++- app/src/main/res/values-de/strings.xml | 12 +- app/src/main/res/values-hu/strings.xml | 38 ++-- app/src/main/res/values-ti/strings.xml | 6 + app/src/main/res/values-uk/strings.xml | 4 +- .../metadata/android/ar-SA/changelogs/2.txt | 1 + .../android/ar-SA/full_description.txt | 10 +- .../android/ar-SA/short_description.txt | 2 +- fastlane/metadata/android/ar-SA/title.txt | 2 +- .../android/de-DE/full_description.txt | 6 +- 13 files changed, 321 insertions(+), 39 deletions(-) create mode 100644 app/src/main/res/values-ajp/strings.xml create mode 100644 app/src/main/res/values-am/strings.xml create mode 100644 app/src/main/res/values-ti/strings.xml create mode 100644 fastlane/metadata/android/ar-SA/changelogs/2.txt diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/app/src/main/res/values-ajp/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-am/strings.xml b/app/src/main/res/values-am/strings.xml new file mode 100644 index 00000000..98eb0e0d --- /dev/null +++ b/app/src/main/res/values-am/strings.xml @@ -0,0 +1,5 @@ + + + %s ክፍል %d + ተዋናዮች: %s + \ No newline at end of file diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index 42eba3cc..12d558ad 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -1,2 +1,203 @@ - + + لافتة + تغيير مزود + جارى التحميل + بث%s + ملء + تخطي التحميل + تحميل… + ترجمات + إعادة محاولة الاتصال … + %sييبي%d + الحلقة%dسيتم نشرها في + %dي%dس%dد + %dس%dد + %dد + لافتة الحلقة + اللافتة الاساسية + اذهب للخالف + معاينة الخلفية + سرعة(%.2fx) + فتح مع كلاودستريم + الصفحة الاساسية + ...%sابحث + لايوجد بيانات + المزيد من الخيارات + فتح في المتصفح + المتصفح + شاهد الفلم + دفق التورنت + بدأ التنزيل + عشوائي قادم + تشغيل المقطع الدعائي + الأنواع + توقف التنزيل + خطط للمشاهدة + لا يوجد + إعادة المشاهدة + !تم العثور على تحديث جديد +\n%s->%s + %.1f:قدر + %dاقل + كلاودستريم + بحث + التحميلات + اعدادات + ...بحث + الحلقة القادمة + شارك + مشاهدة + في التوقف + مكتمل + توقف + تشغيل البث المباشر + مصادر + تشغيل الحلقة + تم إلغاء التنزيل + تم التنزيل + تنززل + تحميل + عُد + التحميل فشل + استخدم سطوع النظام في مشغل التطبيق بدلاً من التراكب الداكن + تم تحميل ملف النسخ الاحتياطي + البحث المتقدم + إزالة الحدود السوداء + ترجمات + يضيف خيار السرعة في المشغل + انقر نقرا مزدوجا للبحث + انقر نقرًا مزدوجًا للإيقاف المؤقت + اللاعب يبحث عن المبلغ (بالثواني) + اسحب من جانب إلى آخر للتحكم بموقعك في الفيديو + ابدأ الحلقة التالية عندما تنتهي الحلقة الحالية + استخدام سطوع النظام + تحديث مراقبة التقدم + قم بمزامنة تقدم الحلقة الحالية تلقائيًا + اسحب لتغيير الإعدادات + استعادة البيانات من النسخة الاحتياطية + فشل في استعادة البيانات من الملف %s + انقر مرتين على الجانب الأيمن أو الأيسر للبحث للأمام أو للخلف + البيانات المخزنة + اضغط مرتين في المنتصف للتوقف مؤقتًا + أذونات التخزين مفقودة. حاول مرة اخرى. + حدث خطأ أثناء النسخ الاحتياطي %s + بحث + مكتبة + معلومات + التحديثات والنسخ الاحتياطي + يعطيك نتائج البحث مفصولة حسب المزود + يرسل فقط البيانات عن الأعطال + عرض المقطورات + عرض الملصقات من كيتسو + حسابات + لا يرسل أي بيانات + عرض حلقة حشو للأنمي + إخفاء جودة الفيديو المحددة في نتائج البحث + تحديثات البرنامج المساعد التلقائي + البحث تلقائيًا عن التحديثات الجديدة بعد بدء تشغيل التطبيق. + التحديث إلى الإصداراالمسبق + تنزيل المكونات الإضافية تلقائيًا + إعادة عملية الإعداد + ابحث عن تحديثات الإصدار التجريبي بدلاً من الإصدارات الكاملة فقط + حدد الوضع لتصفية تنزيل المكونات الإضافية + قم تلقائيًا بتثبيت جميع المكونات الإضافية التي لم يتم تثبيتها بعد من المستودعات المضافة. + إعدادات ترجمات كرومكاست + وضع إيجينجرافي + انتقد للبحث + نسخ إحتياطي للبيانات + إظهار تحديثات التطبيق + إعدادات ترجمات المشغل + ترجمات كرومكاست + قم بالتمرير لأعلى أو لأسفل على الجانب الأيسر أو الأيمن لتغيير السطوع أو مستوى الصوت + التشغيل التلقائي للحلقة القادمة + تطبيق رواية خفيفة من نفس المطورين + أعط بينيني للمطورين + جيتهب + تطبيق انيمي من نفس المطورين + لغة التطبيق + انضم إلى الديسكورد + بنيني معطا + بعض الهواتف لا تدعم مثبت الحزمة الجديد. جرب الخيار القديم إذا لم يتم تثبيت التحديثات. + مثبت تتبيق + اجتاز + الحلقات + موسم + تم نسخ الرابط إلى الحافظة + مسح + وقف + جارٍ تنزيل تحديث التطبيق… + إعادة التعيين إلى القيمة العادية + س + %d%s + لا يتمتع هذا المزود بدعم كرومكاست + لم يتم العثور على أي روابط + تشغيل الحلقة + عذرًا، تعطل التطبيق. سيتم إرسال تقرير خطأ مجهول إلى المطورين + %s%d%s + لا يوجد موسم + حلقة + %d-%d + يي + امسح التاريخ + جارٍ تثبيت تحديث التطبيق… + بدأ + لم يتم العثور على أي حلقات + إظهار تخطي النوافذ المنبثقة للفتح/الإنهاء + الكثير من النص. غير قادر على الحفظ في الحافظة. + وضع علامة كما شاهدت + إزالة من شاهد + حذف ملف + فشل + اكتمل + -30 + +30 + تاريخ + هل أنت متأكد أنك تريد الخروج؟ + نعم + لا + تعذر تثبيت الإصدار الجديد من التطبيق + إرث + منزل المجموعة + التقييم (من الأقل إلى الأعلى) + تم التحديث (من الجديد إلى القديم) + تم التحديث (القديم إلى الجديد) + أبجديًا (من الألف إلى الياء) + مكتبتك فارغة :( +\nقم بتسجيل الدخول إلى حساب المكتبة أو قم بإضافة العروض إلى مكتبتك المحلية. + !تم العثور على ملف الوضع الآمن +\n.عدم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف + ارجع + تحديث العروض المشتركة + الوضع العادي + حرر + ملفات تعريفية + مساعدة + .هنا يمكنك تغيير كيفية ترتيب المصادر. إذا كان للفيديو أولوية أعلى، فسيظهر في مكان أعلى في تحديد المصدر. مجموع أولوية المصدر وأولوية الجودة هو أولوية الفيديو +\n +\nالمصدر أ: 3 +\nالجودة ب: 7 +\nستكون أولوية الفيديو المدمجة .10 +\n +\n!ملاحظة: إذا كان المجموع 10 أو أكثر، فسيقوم اللاعب تلقائيًا بتخطي التحميل عند تحميل هذا الرابط + لقد صوت بالفعل + أبجديًا (ياء إلى ألف) + ترتيب حسب + مشترك + سيتم تحديث التطبيق عند الخروج + رتب + التقييم (من الأعلى إلى الأقل) + حدد المكتبة + افتع مع + .هذه القائمة فارغة. حاول التبديل إلى واحد آخر + %sتم الاشتراك في + %sتم إلغاء الاشتراك من + !%dتم إصدار الحلقة + خلفية الملف الشخصي + %dملف التعريف + واي فاي + بيانات الجوال + استخدم + %sتعذر إنشاء واجهة المستخدم بشكل صحيح، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور + الصفات + \ No newline at end of file diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 425293e4..df95d69f 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -156,7 +156,7 @@ Não enviar nenhum dado Mostrar episódios de Filler em anime Mostrar trailers - Mostrar posters do kitsu + Mostrar posters do Kitsu Esconder qualidades de vídeo selecionadas nos resultados da Pesquisa Atualizações de plugin automáticas Mostrar atualizações do app @@ -183,7 +183,7 @@ S E Nenhum Episódio encontrado - Deletar Arquivo + Apagar Arquivo Deletar Pausar Retomar @@ -410,15 +410,19 @@ Transferido %d %s com sucesso Tudo %s já transferido Transferência em batch - plugin - plugins + Plugin + Plugins Isto irá apagar todos os repositórios de plugins Apagar repositório Transferir lista de sites a usar Transferido: %d Desativado: %d Não transferido: %d - Adicionar um repositório para instalar extensões de sites + CloudStream não tem fontes instaladas por padrão. Você precisa instalar um site de repositórios. +\n +\nPor causa das limitações do DMCA (Digital Millennium Copyright Act ) feito em nome de Sky UK Limited 🤮nós não podemos adicionar site de repositórios no app. +\n +\nEntre no nosso Discord ou pesquise online. Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas @@ -455,7 +459,7 @@ Editar Perfis Exibindo Player - procure na Barra de Progresso - remover dos assitidos + Remover dos assistidos Extensões Alfabética(A => Z) Abrir com @@ -468,7 +472,7 @@ Biblioteca Não Trilhas Sonoras - Votação(Baixa para Alta) + Votação (Baixa para Alta) Atualização iniciada Conteúdo +18 Ajuda @@ -476,7 +480,7 @@ Não pudemos instalar a nova versão do App instalador de pacotes Organizar por - Votação(Alta para Baixa) + Votação (Alta para Baixa) Alfabética(Z => A) Qualidade Perfil de plano de fundo @@ -499,4 +503,51 @@ Atualizando shows inscritos Player oculto - Procure na barra de progresso Conteúdo +18 - + Reiniciar + Parar + Marcar como assistido + Aplicativo precisa ser fechado para atualizar + Mostrar popups pulados para abertura e finalização + %d-%d + Player interno + Tamanho + Abrindo + %s %d%s + %d plugins atualizados + Todos as extensões serão desligadas para ajuda se talvez estejam causando algum bug. + Aplicativo não encontrado + Recapitular + Todas as linguagens + Pula %s + Mistura terminada + Modo seguro ligado + Ranquear: %s + Linguagem + Lista de reprodução HLS + Terminando + %d %s + Adicionado em (antigo para novo) + Introdução + plug-ins não foram encontrados no repositório + Repositório não encontrado, verifique o URL e tente usa uma VPN + Descrição + Versão + Autores + Instale a extensão primeiro + Créditos + Historico + Limpar historico + Tem Muito texto. Não é possível salvar no clipboard. + Player de vídeo preferido + Começar + Suportado + Status + MPV + Abrindo mistura + VLC + Aplicar quando reiniciar + Visualização info de crash + Faixas de áudio + Adicionado em (novo para antigo) + Faixas de video + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 45a6a66c..6892c8fd 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -92,7 +92,7 @@ Abbrechen Kopieren Schließen - Löschen + Leeren Speichern Player-Geschwindigkeit Untertiteleinstellungen @@ -390,7 +390,7 @@ Einrichtung überspringen Aussehen der App passend zu dem des Geräts ändern Absturzmeldung - Was möchtest du anschauen\? + Was möchten Sie sehen\? Fertig Erweiterungen Repository hinzufügen @@ -546,4 +546,10 @@ \nWerden eine kombinierte Videopriorität von 10 haben. \n \nHINWEIS: Wenn die Summe 10 oder mehr beträgt, überspringt der Player automatisch das Laden, wenn der Link geladen wird! - + Filtermodus für Plugin-Downloads auswählen + Es wurde bereits abgestimmt + Keine Plugins im Repository gefunden + Repository nicht gefunden, überprüfe die URL und probiere eine VPN + Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein schwerwiegender Fehler und sollte sofort gemeldet werden. %s + Deaktivieren + \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 46407f76..ac817db0 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -190,7 +190,7 @@ Adatok eltárolva Hiba a biztonsági mentés során %s Fiókok - Szolgáltatás szerinti keresés eredmények + Szolgáltató szerint elkülönítve adja meg a keresési eredményeket Nem küld adatokat Poszterek megjelenítése Kitsu-ról Kiválasztott videóminőségek elrejtése keresési eredményekbe @@ -198,7 +198,7 @@ Bővítmények automatikus letöltése Automatikusan telepíti az összes még nem telepített bővítményt a hozzáadott tárolókból. Alkalmazás frissítések megjelenítése - Automatikusan keressen új frissítéseket indításkor + Automatikusan keressen új frissítéseket indításkor. Frissítés az előzetes kiadásokhoz (prerelease) Csak előzetesen kiadott frissítések (prerelease) keresése a teljes kiadások helyett Github @@ -232,30 +232,30 @@ Lejátszás böngészőben Feliratok letöltése Újracsatlakozás… - Swipe balra vagy jobbra a videólejátszóban az idő vezérléséhez + Húzd balra vagy jobbra a videólejátszóban az idő vezérléséhez Csúsztassa ujját a beállítások módosításához - Csúsztassa az újját bal vagy jobb oldalon a fényerő vagy hangerő megváltoztatásához + Csúsztassa felfelé vagy lefelé a bal vagy jobb oldalon a fényerő vagy a hangerő megváltoztatásához Biztonsági mentés 0 Banán a fejlesztőknek - Swipe to seek + Húzás a kereséshez Következő epizód automatikus lejátszása Következő epizód lejátszása amikor az aktuális epizód véget ér - Dupla koppintás to seek + Dupla koppintás a kereséshez Dupla koppintás a szüneteltetéshez - Player seek amount + Lejátszó keresési értéke (Másodpercben) Koppintson kétszer a jobb vagy bal oldalra az előre vagy hátra ugráshoz - Koppintson középre a szüneteltetéshez + Koppintson kétszer középen a szüneteltetéshez Rendszer fényerejének használata Rendszer fényerejének használata az appban a sötét átfedés helyett Előrehaladás frissítése Automatikusan szinkronizálja az aktuális epizód előrehaladását - Adatok visszaállítása a biztonsági mentésből + Adatok visszaállítása biztonsági mentésből Biztonsági mentés betöltve Információ Folytatás -30 Frissítés elkezdődött - Nem sikerült visszaállítani az adatok a fájlból %s + Nem sikerült visszaállítani az adatokat a %s fájlból Tárolási engedélyek hiányoznak. Kérjük próbálja újra. Csak összeomlásokról küld adatokat APK Telepítő @@ -280,7 +280,7 @@ DNS HTTPS-en keresztül Böngésző Android TV - kézmozdulatok + Kézmozdulatok frissítés kihagyása Alkalmazásfrissítések Szolgáltatók @@ -496,4 +496,18 @@ HQ %d letöltve Start - + Emulátor elrendezés + Nyomkövetés hozzáadása + Telefon elrendezés + Poszter cím helye + Tegye a címet a poszter alá + Az átugrás mértéke, amikor a lejátszó el van rejtve + Jogi nyilatkozat + Lejátszó megjelenítve - Ugrási Érték + Lejátszó elrejtve - Ugrási Érték + Klónozott oldal + Egy meglévő webhely klónjának hozzáadása, más URL-címmel + TV elrendezés + Automatikus + Az átugrás mértéke, amikor a lejátszó látható + \ No newline at end of file diff --git a/app/src/main/res/values-ti/strings.xml b/app/src/main/res/values-ti/strings.xml new file mode 100644 index 00000000..0f64858d --- /dev/null +++ b/app/src/main/res/values-ti/strings.xml @@ -0,0 +1,6 @@ + + + %s ክፋል %d + ክፋል %d በ ላይ ይወጣል + ተዋሳእቲ፡ %s + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index e0db1c0e..8b1b6c39 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -418,7 +418,7 @@ Почалося завантаження %d %s… Завантажено %d %s Всі %s вже завантажено - Пакетне завантаження + Завантажити пакети плагін плагіни Видалити репозиторій @@ -553,4 +553,4 @@ Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували - + \ No newline at end of file diff --git a/fastlane/metadata/android/ar-SA/changelogs/2.txt b/fastlane/metadata/android/ar-SA/changelogs/2.txt new file mode 100644 index 00000000..cc43acf1 --- /dev/null +++ b/fastlane/metadata/android/ar-SA/changelogs/2.txt @@ -0,0 +1 @@ +تمت إضافة سجل التغيير! diff --git a/fastlane/metadata/android/ar-SA/full_description.txt b/fastlane/metadata/android/ar-SA/full_description.txt index 2107b338..9668a9b1 100644 --- a/fastlane/metadata/android/ar-SA/full_description.txt +++ b/fastlane/metadata/android/ar-SA/full_description.txt @@ -1,14 +1,10 @@ -يتيح لك كلاود ستريم -3 بث وتنزيل الأفلام والمسلسلات التلفزيونية والأنيمي. يأتي التطبيق بدون أي إعلانات وتحليلات. و يدعم العديد من مواقع البث الاولي(التريلر) والأفلام والمزيد. وتشمل الميزات: - +يسمح لك كلاود ستريم -3 ببث وتنزيل الأفلام, المسلسلات التلفزيونية, والأنيمي. +يأتي التطبيق بدون أي إعلانات وتحليلات و + يدعم العديد من مواقع البث الاولي(التريلر) ,والأفلام, والمزيد. إشارات مرجعية - -قم بتنزيل ودفق الأفلام والبرامج التلفزيونية والأنيمي - - تنزيلات الترجمة - دعم كروم كاست diff --git a/fastlane/metadata/android/ar-SA/short_description.txt b/fastlane/metadata/android/ar-SA/short_description.txt index f396ff81..7ccd9743 100644 --- a/fastlane/metadata/android/ar-SA/short_description.txt +++ b/fastlane/metadata/android/ar-SA/short_description.txt @@ -1 +1 @@ -بث وتحميل الأفلام والأنمي والمسلسلات التلفزيونية. +بث وتحميل الأفلام, الأنمي, والمسلسلات التلفزيونية. diff --git a/fastlane/metadata/android/ar-SA/title.txt b/fastlane/metadata/android/ar-SA/title.txt index 635e1390..7977b290 100644 --- a/fastlane/metadata/android/ar-SA/title.txt +++ b/fastlane/metadata/android/ar-SA/title.txt @@ -1 +1 @@ -كلاود ستريم +كلاودستريم diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt index df314372..ea2a8750 100644 --- a/fastlane/metadata/android/de-DE/full_description.txt +++ b/fastlane/metadata/android/de-DE/full_description.txt @@ -1,11 +1,11 @@ -Mit CloudStream-3 kannst du Filme, TV-Serien und Anime streamen und herunterladen. +Mit CloudStream-3 kannst du Filme, TV-Serien und Anime streamen und herunterladen. Die App kommt ganz ohne Werbung und Analytik aus. -Sie unterstützt mehrere Trailer-, Filmseiten und vieles mehr. Integrierte Features: +Sie unterstützt zahlreiche Trailer, Filmseiten und vieles mehr, unter anderem: Lesezeichen -Herunterladen und Streamen von Filmen, Fernsehsendungen und Animes +Herunterladen und Streaming von Filmen, Fernsehsendungen und Animes Downloads von Untertiteln From 557003895b65a7b6e1631489523e1225d56cdc34 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Fri, 25 Aug 2023 08:59:37 +0000 Subject: [PATCH 118/156] chore(locales): fix locale issues --- .../com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 3 +++ app/src/main/res/values-ajp/strings.xml | 2 +- app/src/main/res/values-am/strings.xml | 2 +- app/src/main/res/values-ars/strings.xml | 2 +- app/src/main/res/values-bp/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-hu/strings.xml | 2 +- app/src/main/res/values-ti/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- 9 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index f46aac9b..1bd9778e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -54,6 +54,8 @@ fun getCurrentLocale(context: Context): String { // https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes leave blank for auto val appLanguages = arrayListOf( /* begin language list */ + Triple("", "ajp", "ajp"), + Triple("", "አማርኛ", "am"), Triple("", "العربية", "ar"), Triple("", "ars", "ars"), Triple("", "български", "bg"), @@ -96,6 +98,7 @@ val appLanguages = arrayListOf( Triple("", "Soomaaliga", "so"), Triple("", "svenska", "sv"), Triple("", "தமிழ்", "ta"), + Triple("", "ትግርኛ", "ti"), Triple("", "Tagalog", "tl"), Triple("", "Türkçe", "tr"), Triple("", "українська", "uk"), diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index a6b3daec..42eba3cc 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + diff --git a/app/src/main/res/values-am/strings.xml b/app/src/main/res/values-am/strings.xml index 98eb0e0d..5a799eb4 100644 --- a/app/src/main/res/values-am/strings.xml +++ b/app/src/main/res/values-am/strings.xml @@ -2,4 +2,4 @@ %s ክፍል %d ተዋናዮች: %s - \ No newline at end of file + diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index 12d558ad..ea8aa05c 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -200,4 +200,4 @@ استخدم %sتعذر إنشاء واجهة المستخدم بشكل صحيح، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور الصفات - \ No newline at end of file + diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index df95d69f..b70eec12 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -550,4 +550,4 @@ Faixas de áudio Adicionado em (novo para antigo) Faixas de video - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6892c8fd..6739465a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -552,4 +552,4 @@ Repository nicht gefunden, überprüfe die URL und probiere eine VPN Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein schwerwiegender Fehler und sollte sofort gemeldet werden. %s Deaktivieren - \ No newline at end of file + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index ac817db0..05a7f0a7 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -510,4 +510,4 @@ TV elrendezés Automatikus Az átugrás mértéke, amikor a lejátszó látható - \ No newline at end of file + diff --git a/app/src/main/res/values-ti/strings.xml b/app/src/main/res/values-ti/strings.xml index 0f64858d..a9079ed5 100644 --- a/app/src/main/res/values-ti/strings.xml +++ b/app/src/main/res/values-ti/strings.xml @@ -3,4 +3,4 @@ %s ክፋል %d ክፋል %d በ ላይ ይወጣል ተዋሳእቲ፡ %s - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 8b1b6c39..4866ecd4 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -553,4 +553,4 @@ Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували - \ No newline at end of file + From 8193e39b3046192dfb6970e5ff49f30d629d033a Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 25 Aug 2023 23:16:34 +0200 Subject: [PATCH 119/156] bump + refactor --- app/build.gradle.kts | 15 +- .../lagradost/cloudstream3/MainActivity.kt | 18 +- .../cloudstream3/NativeCrashHandler.kt | 4 +- .../ui/player/DownloadedPlayerActivity.kt | 8 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 6 +- .../ui/settings/SettingsGeneral.kt | 4 +- .../cloudstream3/utils/BackupUtils.kt | 1 + .../utils/VideoDownloadManager.kt | 14 +- .../cloudstream3/utils/storage/MediaFile.kt | 389 ------------------ .../cloudstream3/utils/storage/SafeFile.kt | 244 ----------- .../utils/storage/UniFileWrapper.kt | 116 ------ 11 files changed, 40 insertions(+), 779 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dfd2c173..333fbfb8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,11 +32,12 @@ android { enable = true } - externalNativeBuild { - cmake { - path("CMakeLists.txt") - } - } + // disable this for now + //externalNativeBuild { + // cmake { + // path("CMakeLists.txt") + // } + //} signingConfigs { create("prerelease") { @@ -58,7 +59,7 @@ android { targetSdk = 29 versionCode = 59 - versionName = "4.1.7" + versionName = "4.1.8" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") @@ -232,7 +233,7 @@ dependencies { // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") // Util to skip the URI file fuckery 🙏 - implementation("com.github.tachiyomiorg:unifile:17bec43") + implementation("com.github.LagradOst:SafeFile:0.0.2") // API because cba maintaining it myself implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 15b16078..fbad4fce 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -144,6 +144,7 @@ import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser +import com.lagradost.safefile.SafeFile import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.io.File @@ -279,6 +280,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { companion object { const val TAG = "MAINACT" var lastError: String? = null + /** * Setting this will automatically enter the query in the search * next time the search fragment is opened. @@ -366,7 +368,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { nextSearchQuery = try { URLDecoder.decode(query, "UTF-8") - } catch (t : Throwable) { + } catch (t: Throwable) { logError(t) query } @@ -859,7 +861,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { RecyclerView::class.java.declaredMethods.firstOrNull { it.name == "scrollStep" }?.also { it.isAccessible = true } - } catch (t : Throwable) { + } catch (t: Throwable) { null } } @@ -906,11 +908,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (dx > 0) dx else 0 } - if(!NO_MOVE_LIST) { + if (!NO_MOVE_LIST) { parent.smoothScrollBy(rdx, 0) - }else { + } else { val smoothScroll = reflectedScroll - if(smoothScroll == null) { + if (smoothScroll == null) { parent.smoothScrollBy(rdx, 0) } else { try { @@ -920,12 +922,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val out = IntArray(2) smoothScroll.invoke(parent, rdx, 0, out) val scrolledX = out[0] - if(abs(scrolledX) <= 0) { // newFocus.measuredWidth*2 + if (abs(scrolledX) <= 0) { // newFocus.measuredWidth*2 smoothScroll.invoke(parent, -rdx, 0, out) parent.smoothScrollBy(scrolledX, 0) if (NO_MOVE_LIST) targetDx = scrolledX } - } catch (t : Throwable) { + } catch (t: Throwable) { parent.smoothScrollBy(rdx, 0) } } @@ -1131,10 +1133,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { snackbar.show() } } - } } + ioSafe { SafeFile.check(this@MainActivity) } if (PluginManager.checkSafeModeFile()) { normalSafeApiCall { diff --git a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt index 1fe00748..7be90440 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.launch object NativeCrashHandler { // external fun triggerNativeCrash() - private external fun initNativeCrashHandler() + /*private external fun initNativeCrashHandler() private external fun getSignalStatus(): Int private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch { @@ -49,5 +49,5 @@ object NativeCrashHandler { } initSignalPolling() - } + }*/ } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index 03405faf..d181e175 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.player +import android.content.ContentUris import android.content.Intent import android.net.Uri import android.os.Bundle @@ -10,7 +11,7 @@ import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.cloudstream3.utils.storage.SafeFile +import com.lagradost.safefile.SafeFile const val DTAG = "PlayerActivity" @@ -57,7 +58,10 @@ class DownloadedPlayerActivity : AppCompatActivity() { listOf( ExtractorUri( uri = uri, - name = name ?: getString(R.string.downloaded_file) + 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() ) ) ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 341b4ad3..2b9304b6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -21,13 +21,11 @@ import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager import androidx.media3.common.Format.NO_VALUE import androidx.media3.common.MimeTypes -import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding -import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding import com.lagradost.cloudstream3.databinding.PlayerSelectTracksBinding import com.lagradost.cloudstream3.mvvm.* @@ -52,7 +50,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.toPx -import com.lagradost.cloudstream3.utils.storage.SafeFile +import com.lagradost.safefile.SafeFile import kotlinx.coroutines.Job import java.util.* import kotlin.math.abs @@ -136,7 +134,7 @@ class GeneratorPlayer : FullScreenPlayer() { return durPos.position } - var currentVerifyLink: Job? = null + private var currentVerifyLink: Job? = null private fun loadExtractorJob(extractorLink: ExtractorLink?) { currentVerifyLink?.cancel() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index f46aac9b..49f678c4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -38,7 +38,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath -import com.lagradost.cloudstream3.utils.storage.SafeFile +import com.lagradost.safefile.SafeFile fun getCurrentLocale(context: Context): String { val res = context.resources @@ -335,7 +335,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { val currentDir = settingsManager.getString(getString(R.string.download_path_pref), null) - ?: context?.let { ctx -> VideoDownloadManager.getDefaultDir(ctx).toString() } + ?: context?.let { ctx -> VideoDownloadManager.getDefaultDir(ctx)?.filePath() } activity?.showBottomDialog( dirs + listOf("Custom"), diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index 2da54678..41326eb8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -158,6 +158,7 @@ object BackupUtils { val displayName = "CS3_Backup_${date}" val backupFile = getBackup() val stream = setupStream(this, displayName, null, ext, false) + fileStream = stream.openNew() printStream = PrintWriter(fileStream) printStream.print(mapper.writeValueAsString(backupFile)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 7bd863ae..6425ba66 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -35,8 +35,8 @@ import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute -import com.lagradost.cloudstream3.utils.storage.MediaFileContentType -import com.lagradost.cloudstream3.utils.storage.SafeFile +import com.lagradost.safefile.MediaFileContentType +import com.lagradost.safefile.SafeFile import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -554,9 +554,8 @@ object VideoDownloadManager { extension: String, tryResume: Boolean, ): StreamData { - val (base, _) = context.getBasePath() return setupStream( - base ?: throw IOException("Bad config"), + context.getBasePath().first ?: getDefaultDir(context) ?: throw IOException("Bad config"), name, folder, extension, @@ -1401,7 +1400,12 @@ object VideoDownloadManager { metadata.type = DownloadType.IsFailed } } finally { - fileMutex.unlock() + try { + // may cause java.lang.IllegalStateException: Mutex is not locked because of cancelling + fileMutex.unlock() + } catch (t : Throwable) { + logError(t) + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt deleted file mode 100644 index 51b8adfe..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt +++ /dev/null @@ -1,389 +0,0 @@ -package com.lagradost.cloudstream3.utils.storage - -import android.content.ContentResolver -import android.content.ContentUris -import android.content.ContentValues -import android.content.Context -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.provider.MediaStore -import androidx.annotation.RequiresApi -import com.hippo.unifile.UniRandomAccessFile -import com.lagradost.cloudstream3.mvvm.logError -import okhttp3.internal.closeQuietly -import java.io.File -import java.io.FileNotFoundException -import java.io.InputStream -import java.io.OutputStream - - -enum class MediaFileContentType { - Downloads, - Audio, - Video, - Images, -} - -// https://developer.android.com/training/data-storage/shared/media -fun MediaFileContentType.toPath(): String { - return when (this) { - MediaFileContentType.Downloads -> Environment.DIRECTORY_DOWNLOADS - MediaFileContentType.Audio -> Environment.DIRECTORY_MUSIC - MediaFileContentType.Video -> Environment.DIRECTORY_MOVIES - MediaFileContentType.Images -> Environment.DIRECTORY_DCIM - } -} - -fun MediaFileContentType.defaultPrefix(): String { - return Environment.getExternalStorageDirectory().absolutePath -} - -fun MediaFileContentType.toAbsolutePath(): String { - return defaultPrefix() + File.separator + - this.toPath() -} - -fun replaceDuplicateFileSeparators(path: String): String { - return path.replace(Regex("${File.separator}+"), File.separator) -} - -@RequiresApi(Build.VERSION_CODES.Q) -fun MediaFileContentType.toUri(external: Boolean): Uri { - val volume = if (external) MediaStore.VOLUME_EXTERNAL_PRIMARY else MediaStore.VOLUME_INTERNAL - return when (this) { - MediaFileContentType.Downloads -> MediaStore.Downloads.getContentUri(volume) - MediaFileContentType.Audio -> MediaStore.Audio.Media.getContentUri(volume) - MediaFileContentType.Video -> MediaStore.Video.Media.getContentUri(volume) - MediaFileContentType.Images -> MediaStore.Images.Media.getContentUri(volume) - } -} - -@RequiresApi(Build.VERSION_CODES.Q) -class MediaFile( - private val context: Context, - private val folderType: MediaFileContentType, - private val external: Boolean = true, - absolutePath: String, -) : SafeFile { - override fun toString(): String { - return sanitizedAbsolutePath - } - - // this is the path relative to the download directory so "/hello/text.txt" = "hello/text.txt" is in fact "Download/hello/text.txt" - private val sanitizedAbsolutePath: String = - replaceDuplicateFileSeparators(absolutePath) - - // this is only a directory if the filepath ends with a / - private val isDir: Boolean = sanitizedAbsolutePath.endsWith(File.separator) - private val isFile: Boolean = !isDir - - // this is the relative path including the Download directory, so "/hello/text.txt" => "Download/hello" - private val relativePath: String = - replaceDuplicateFileSeparators(folderType.toPath() + File.separator + sanitizedAbsolutePath).substringBeforeLast( - File.separator - ) - - // "/hello/text.txt" => "text.txt" - private val namePath: String = sanitizedAbsolutePath.substringAfterLast(File.separator) - private val baseUri = folderType.toUri(external) - private val contentResolver: ContentResolver = context.contentResolver - - init { - // some standard asserts that should always be hold or else this class wont work - assert(!relativePath.endsWith(File.separator)) - assert(!(isDir && isFile)) - assert(!relativePath.contains(File.separator + File.separator)) - assert(!namePath.contains(File.separator)) - - if (isDir) { - assert(namePath.isBlank()) - } else { - assert(namePath.isNotBlank()) - } - } - - companion object { - private fun splitFilenameExt(name: String): Pair { - val split = name.indexOfLast { it == '.' } - if (split <= 0) return name to null - val ext = name.substring(split + 1 until name.length) - if (ext.isBlank()) return name to null - - return name.substring(0 until split) to ext - } - - private fun splitFilenameMime(name: String): Pair { - val (display, ext) = splitFilenameExt(name) - val mimeType = when (ext) { - - // Absolutely ridiculous, if text/vtt is used as mimetype scoped storage prevents - // downloading to /Downloads yet it works with null - - "vtt" -> null // "text/vtt" - "mp4" -> "video/mp4" - "srt" -> null // "application/x-subrip"//"text/plain" - else -> null - } - return display to mimeType - } - } - - private fun appendRelativePath(path: String, folder: Boolean): MediaFile? { - if (isFile) return null - - // VideoDownloadManager.sanitizeFilename(path.replace(File.separator, "")) - - // in case of duplicate path, aka Download -> Download - if (relativePath == path) return this - - val newPath = - sanitizedAbsolutePath + path + if (folder) File.separator else "" - - return MediaFile( - context = context, - folderType = folderType, - external = external, - absolutePath = newPath - ) - } - - private fun createUri(displayName: String? = namePath): Uri? { - if (displayName == null) return null - if (isFile) return null - val (name, mime) = splitFilenameMime(displayName) - - val newFile = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) - put(MediaStore.MediaColumns.TITLE, name) - if (mime != null) - put(MediaStore.MediaColumns.MIME_TYPE, mime) - put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath) - } - return contentResolver.insert(baseUri, newFile) - } - - override fun createFile(displayName: String?): SafeFile? { - if (isFile || displayName == null) return null - query(displayName)?.uri ?: createUri(displayName) ?: return null - return appendRelativePath(displayName, false) //SafeFile.fromUri(context, ?: return null) - } - - override fun createDirectory(directoryName: String?): SafeFile? { - if (directoryName == null) return null - // we don't create a dir here tbh, just fake create it - return appendRelativePath(directoryName, true) - } - - private data class QueryResult( - val uri: Uri, - val lastModified: Long, - val length: Long, - ) - - @RequiresApi(Build.VERSION_CODES.Q) - private fun query(displayName: String = namePath): QueryResult? { - try { - //val (name, mime) = splitFilenameMime(fullName) - - val projection = arrayOf( - MediaStore.MediaColumns._ID, - MediaStore.MediaColumns.DATE_MODIFIED, - MediaStore.MediaColumns.SIZE, - ) - - val selection = - "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}' AND ${MediaStore.MediaColumns.DISPLAY_NAME}='$displayName'" - - contentResolver.query( - baseUri, - projection, selection, null, null - )?.use { cursor -> - while (cursor.moveToNext()) { - val id = - cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) - - return QueryResult( - uri = ContentUris.withAppendedId( - baseUri, id - ), - lastModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)), - length = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)), - ) - } - } - } catch (t: Throwable) { - logError(t) - } - - return null - } - - override fun uri(): Uri? { - return query()?.uri - } - - override fun name(): String? { - if (isDir) return null - return namePath - } - - override fun type(): String? { - TODO("Not yet implemented") - } - - override fun filePath(): String { - return replaceDuplicateFileSeparators(relativePath + File.separator + namePath) - } - - override fun isDirectory(): Boolean { - return isDir - } - - override fun isFile(): Boolean { - return isFile - } - - override fun lastModified(): Long? { - if (isDir) return null - return query()?.lastModified - } - - override fun length(): Long? { - if (isDir) return null - val query = query() - val length = query?.length ?: return null - if (length <= 0) { - try { - contentResolver.openFileDescriptor(query.uri, "r") - .use { - it?.statSize - }?.let { - return it - } - } catch (e: FileNotFoundException) { - return null - } - - val inputStream: InputStream = openInputStream() ?: return null - return try { - inputStream.available().toLong() - } catch (t: Throwable) { - null - } finally { - inputStream.closeQuietly() - } - } - return length - } - - override fun canRead(): Boolean { - TODO("Not yet implemented") - } - - override fun canWrite(): Boolean { - TODO("Not yet implemented") - } - - private fun delete(uri: Uri): Boolean { - return contentResolver.delete(uri, null, null) > 0 - } - - override fun delete(): Boolean { - return if (isDir) { - (listFiles() ?: return false).all { - it.delete() - } - } else { - delete(uri() ?: return false) - } - } - - override fun exists(): Boolean { - if (isDir) return true - return query() != null - } - - override fun listFiles(): List? { - if (isFile) return null - try { - val projection = arrayOf( - MediaStore.MediaColumns.DISPLAY_NAME - ) - - val selection = - "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}'" - contentResolver.query( - baseUri, - projection, selection, null, null - )?.use { cursor -> - val out = ArrayList(cursor.count) - while (cursor.moveToNext()) { - val nameIdx = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) - if (nameIdx == -1) continue - val name = cursor.getString(nameIdx) - - appendRelativePath(name, false)?.let { new -> - out.add(new) - } - } - - out - } - } catch (t: Throwable) { - logError(t) - } - return null - } - - override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? { - if (isFile || displayName == null) return null - - val new = appendRelativePath(displayName, false) ?: return null - if (new.exists()) { - return new - } - - return null//SafeFile.fromUri(context, query(displayName ?: return null)?.uri ?: return null) - } - - override fun renameTo(name: String?): Boolean { - TODO("Not yet implemented") - } - - override fun openOutputStream(append: Boolean): OutputStream? { - try { - // use current file - uri()?.let { - return contentResolver.openOutputStream( - it, - if (append) "wa" else "wt" - ) - } - - // create a new file if current is not found, - // as we know it is new only write access is needed - createUri()?.let { - return contentResolver.openOutputStream( - it, - "w" - ) - } - return null - } catch (t: Throwable) { - return null - } - } - - override fun openInputStream(): InputStream? { - try { - return contentResolver.openInputStream(uri() ?: return null) - } catch (t: Throwable) { - return null - } - } - - override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? { - TODO("Not yet implemented") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt deleted file mode 100644 index 85a74963..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt +++ /dev/null @@ -1,244 +0,0 @@ -package com.lagradost.cloudstream3.utils.storage - -import android.content.Context -import android.net.Uri -import android.os.Build -import android.os.Environment -import com.hippo.unifile.UniFile -import com.hippo.unifile.UniRandomAccessFile - -import java.io.File -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream - -interface SafeFile { - companion object { - fun fromUri(context: Context, uri: Uri): SafeFile? { - return UniFileWrapper(UniFile.fromUri(context, uri) ?: return null) - } - - fun fromFile(context: Context, file: File?): SafeFile? { - if (file == null) return null - // because UniFile sucks balls on Media we have to do this - val absPath = file.absolutePath.removePrefix(File.separator) - for (value in MediaFileContentType.values()) { - val prefixes = listOf(value.toAbsolutePath(), value.toPath()).map { it.removePrefix(File.separator) } - for (prefix in prefixes) { - if (!absPath.startsWith(prefix)) continue - return fromMedia( - context, - value, - absPath.removePrefix(prefix).ifBlank { File.separator } - ) - } - } - - return UniFileWrapper(UniFile.fromFile(file) ?: return null) - } - - fun fromAsset( - context: Context, - filename: String? - ): SafeFile? { - return UniFileWrapper( - UniFile.fromAsset(context.assets, filename ?: return null) ?: return null - ) - } - - fun fromResource( - context: Context, - id: Int - ): SafeFile? { - return UniFileWrapper( - UniFile.fromResource(context, id) ?: return null - ) - } - - fun fromMedia( - context: Context, - folderType: MediaFileContentType, - path: String = File.separator, - external: Boolean = true, - ): SafeFile? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - //fromUri(context, folderType.toUri(external))?.findFile(folderType.toPath())?.gotoDirectory(path) - - return MediaFile( - context = context, - folderType = folderType, - external = external, - absolutePath = path - ) - } else { - fromFile( - context, - File( - (Environment.getExternalStorageDirectory().absolutePath + File.separator + - folderType.toPath() + File.separator + folderType).replace( - File.separator + File.separator, - File.separator - ) - ) - ) - } - - } - } - - /*val uri: Uri? get() = getUri() - val name: String? get() = getName() - val type: String? get() = getType() - val filePath: String? get() = getFilePath() - val isFile: Boolean? get() = isFile() - val isDirectory: Boolean? get() = isDirectory() - val length: Long? get() = length() - val canRead: Boolean get() = canRead() - val canWrite: Boolean get() = canWrite() - val lastModified: Long? get() = lastModified()*/ - - @Throws(IOException::class) - fun isFileOrThrow(): Boolean { - return isFile() ?: throw IOException("Unable to get if file is a file or directory") - } - - @Throws(IOException::class) - fun lengthOrThrow(): Long { - return length() ?: throw IOException("Unable to get file length") - } - - @Throws(IOException::class) - fun isDirectoryOrThrow(): Boolean { - return isDirectory() ?: throw IOException("Unable to get if file is a directory") - } - - @Throws(IOException::class) - fun filePathOrThrow(): String { - return filePath() ?: throw IOException("Unable to get file path") - } - - @Throws(IOException::class) - fun uriOrThrow(): Uri { - return uri() ?: throw IOException("Unable to get uri") - } - - @Throws(IOException::class) - fun renameOrThrow(name: String?) { - if (!renameTo(name)) { - throw IOException("Unable to rename to $name") - } - } - - @Throws(IOException::class) - fun openOutputStreamOrThrow(append: Boolean = false): OutputStream { - return openOutputStream(append) ?: throw IOException("Unable to open output stream") - } - - @Throws(IOException::class) - fun openInputStreamOrThrow(): InputStream { - return openInputStream() ?: throw IOException("Unable to open input stream") - } - - @Throws(IOException::class) - fun existsOrThrow(): Boolean { - return exists() ?: throw IOException("Unable get if file exists") - } - - @Throws(IOException::class) - fun findFileOrThrow(displayName: String?, ignoreCase: Boolean = false): SafeFile { - return findFile(displayName, ignoreCase) ?: throw IOException("Unable find file") - } - - @Throws(IOException::class) - fun gotoDirectoryOrThrow( - directoryName: String?, - createMissingDirectories: Boolean = true - ): SafeFile { - return gotoDirectory(directoryName, createMissingDirectories) - ?: throw IOException("Unable to go to directory $directoryName") - } - - @Throws(IOException::class) - fun listFilesOrThrow(): List { - return listFiles() ?: throw IOException("Unable to get files") - } - - - @Throws(IOException::class) - fun createFileOrThrow(displayName: String?): SafeFile { - return createFile(displayName) ?: throw IOException("Unable to create file $displayName") - } - - @Throws(IOException::class) - fun createDirectoryOrThrow(directoryName: String?): SafeFile { - return createDirectory( - directoryName ?: throw IOException("Unable to create file with invalid name") - ) - ?: throw IOException("Unable to create directory $directoryName") - } - - @Throws(IOException::class) - fun deleteOrThrow() { - if (!delete()) { - throw IOException("Unable to delete file") - } - } - - /** file.gotoDirectory("a/b/c") -> "file/a/b/c/" where a null or blank directoryName - * returns itself. createMissingDirectories specifies if the dirs should be created - * when travelling or break at a dir not found */ - fun gotoDirectory( - directoryName: String?, - createMissingDirectories: Boolean = true - ): SafeFile? { - if (directoryName == null) return this - - return directoryName.split(File.separatorChar).filter { it.isNotBlank() } - .fold(this) { file: SafeFile?, directory -> - // as MediaFile does not actually create a directory we can do this - if (createMissingDirectories || this is MediaFile) { - file?.createDirectory(directory) - } else { - val next = file?.findFile(directory) - - // we require the file to be a directory - if (next?.isDirectory() != true) { - null - } else { - next - } - } - } - } - - - fun createFile(displayName: String?): SafeFile? - fun createDirectory(directoryName: String?): SafeFile? - fun uri(): Uri? - fun name(): String? - fun type(): String? - fun filePath(): String? - fun isDirectory(): Boolean? - fun isFile(): Boolean? - fun lastModified(): Long? - fun length(): Long? - fun canRead(): Boolean - fun canWrite(): Boolean - fun delete(): Boolean - fun exists(): Boolean? - fun listFiles(): List? - - // fun listFiles(filter: FilenameFilter?): Array? - fun findFile(displayName: String?, ignoreCase: Boolean = false): SafeFile? - - fun renameTo(name: String?): Boolean - - /** Open a stream on to the content associated with the file */ - fun openOutputStream(append: Boolean = false): OutputStream? - - /** Open a stream on to the content associated with the file */ - fun openInputStream(): InputStream? - - /** Get a random access stuff of the UniFile, "r" or "rw" */ - fun createRandomAccessFile(mode: String?): UniRandomAccessFile? -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt deleted file mode 100644 index f1592169..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.lagradost.cloudstream3.utils.storage - -import android.net.Uri -import com.hippo.unifile.UniFile -import com.hippo.unifile.UniRandomAccessFile -import com.lagradost.cloudstream3.mvvm.logError -import okhttp3.internal.closeQuietly -import java.io.InputStream -import java.io.OutputStream - -private fun UniFile.toFile(): SafeFile { - return UniFileWrapper(this) -} - -fun safe(apiCall: () -> T): T? { - return try { - apiCall.invoke() - } catch (throwable: Throwable) { - logError(throwable) - null - } -} - -class UniFileWrapper(val file: UniFile) : SafeFile { - override fun createFile(displayName: String?): SafeFile? { - return file.createFile(displayName)?.toFile() - } - - override fun createDirectory(directoryName: String?): SafeFile? { - return file.createDirectory(directoryName)?.toFile() - } - - override fun uri(): Uri? { - return safe { file.uri } - } - - override fun name(): String? { - return safe { file.name } - } - - override fun type(): String? { - return safe { file.type } - } - - override fun filePath(): String? { - return safe { file.filePath } - } - - override fun isDirectory(): Boolean? { - return safe { file.isDirectory } - } - - override fun isFile(): Boolean? { - return safe { file.isFile } - } - - override fun lastModified(): Long? { - return safe { file.lastModified() } - } - - override fun length(): Long? { - return safe { - val len = file.length() - if (len <= 1) { - val inputStream = this.openInputStream() ?: return@safe null - try { - inputStream.available().toLong() - } finally { - inputStream.closeQuietly() - } - } else { - len - } - } - } - - override fun canRead(): Boolean { - return safe { file.canRead() } ?: false - } - - override fun canWrite(): Boolean { - return safe { file.canWrite() } ?: false - } - - override fun delete(): Boolean { - return safe { file.delete() } ?: false - } - - override fun exists(): Boolean? { - return safe { file.exists() } - } - - override fun listFiles(): List? { - return safe { file.listFiles()?.mapNotNull { it?.toFile() } } - } - - override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? { - return safe { file.findFile(displayName, ignoreCase)?.toFile() } - } - - override fun renameTo(name: String?): Boolean { - return safe { file.renameTo(name) } ?: return false - } - - override fun openOutputStream(append: Boolean): OutputStream? { - return safe { file.openOutputStream(append) } - } - - override fun openInputStream(): InputStream? { - return safe { file.openInputStream() } - } - - override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? { - return safe { file.createRandomAccessFile(mode) } - } -} \ No newline at end of file From f01820059b520912d77be56ce8a39c2530f6f886 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 27 Aug 2023 19:07:08 +0200 Subject: [PATCH 120/156] delete resume watching + delete bookmarks buttons. fixed backup crash --- .../cloudstream3/ui/home/HomeFragment.kt | 6 ++- .../ui/home/HomeParentItemAdapterPreview.kt | 31 ++++++++--- .../cloudstream3/ui/home/HomeViewModel.kt | 36 ++++++++++--- .../cloudstream3/utils/BackupUtils.kt | 53 +++---------------- .../cloudstream3/utils/DataStoreHelper.kt | 12 +++-- 5 files changed, 72 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index fa0b6dfb..b84c619e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -658,12 +658,14 @@ class HomeFragment : Fragment() { return@observeNullable } - bottomSheetDialog = activity?.loadHomepageList(item, expandCallback = { + val (items, delete) = item + + bottomSheetDialog = activity?.loadHomepageList(items, expandCallback = { homeViewModel.expandAndReturn(it) }, dismissCallback = { homeViewModel.popup(null) bottomSheetDialog = null - }) + }, deleteCallback = delete) } homeViewModel.reloadStored() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 13497a99..1d8e1399 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -246,7 +246,7 @@ class HomeParentItemAdapterPreview( private val previewViewpagerText: ViewGroup = 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 var resumeRecyclerView: RecyclerView = itemView.findViewById(R.id.home_watch_child_recyclerview) @@ -257,7 +257,7 @@ class HomeParentItemAdapterPreview( private var homeAccount: View? = itemView.findViewById(R.id.home_preview_switch_account) - private var topPadding : View? = itemView.findViewById(R.id.home_padding) + private var topPadding: View? = itemView.findViewById(R.id.home_padding) private val homeNonePadding: View = itemView.findViewById(R.id.home_none_padding) @@ -283,7 +283,11 @@ class HomeParentItemAdapterPreview( item.plot ?: "" homePreviewText.text = item.name - populateChips(homePreviewTags,item.tags ?: emptyList(), R.style.ChipFilledSemiTransparent) + populateChips( + homePreviewTags, + item.tags ?: emptyList(), + R.style.ChipFilledSemiTransparent + ) homePreviewTags.isGone = item.tags.isNullOrEmpty() @@ -413,7 +417,7 @@ class HomeParentItemAdapterPreview( Pair(itemView.findViewById(R.id.home_plan_to_watch_btt), WatchType.PLANTOWATCH), ) - private val toggleListHolder : ChipGroup? = itemView.findViewById(R.id.home_type_holder) + private val toggleListHolder: ChipGroup? = itemView.findViewById(R.id.home_type_holder) init { previewViewpager.setPageTransformer(HomeScrollTransformer()) @@ -422,8 +426,14 @@ class HomeParentItemAdapterPreview( resumeRecyclerView.adapter = resumeAdapter bookmarkRecyclerView.adapter = bookmarkAdapter - resumeRecyclerView.setLinearListLayout(nextLeft = R.id.nav_rail_view, nextRight = FOCUS_SELF) - bookmarkRecyclerView.setLinearListLayout(nextLeft = R.id.nav_rail_view, nextRight = FOCUS_SELF) + resumeRecyclerView.setLinearListLayout( + nextLeft = R.id.nav_rail_view, + nextRight = FOCUS_SELF + ) + bookmarkRecyclerView.setLinearListLayout( + nextLeft = R.id.nav_rail_view, + nextRight = FOCUS_SELF + ) fixPaddingStatusbarMargin(topPadding) @@ -547,7 +557,10 @@ class HomeParentItemAdapterPreview( resumeWatching, false ), 1, false - ) + ), + deleteCallback = { + viewModel.deleteResumeWatching() + } ) } } @@ -572,7 +585,9 @@ class HomeParentItemAdapterPreview( list, false ), 1, false - ) + ), deleteCallback = { + viewModel.deleteBookmarks(list) + } ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index e8cf8863..b27223ec 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -41,6 +41,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllBookmarkedData +import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllResumeStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData @@ -92,6 +94,21 @@ class HomeViewModel : ViewModel() { } } + fun deleteResumeWatching() { + deleteAllResumeStateIds() + loadResumeWatching() + } + + fun deleteBookmarks(list: List) { + list.forEach { DataStoreHelper.deleteBookmarkedData(it.id) } + loadStoredData() + } + + fun deleteBookmarks() { + deleteAllBookmarkedData() + loadStoredData() + } + var repo: APIRepository? = null private val _apiName = MutableLiveData() @@ -394,11 +411,14 @@ class HomeViewModel : ViewModel() { } - private val _popup = MutableLiveData(null) - val popup: LiveData = _popup + private val _popup = MutableLiveData Unit)?>?>(null) + val popup: LiveData Unit)?>?> = _popup - fun popup(list: ExpandableHomepageList?) { - _popup.postValue(list) + fun popup(list: ExpandableHomepageList?, deleteCallback: (() -> Unit)? = null) { + if (list == null) + _popup.postValue(null) + else + _popup.postValue(list to deleteCallback) } private fun bookmarksUpdated(unused: Boolean) { @@ -436,8 +456,7 @@ class HomeViewModel : ViewModel() { // do nothing } - fun reloadStored() { - loadResumeWatching() + fun loadStoredData() { val list = EnumSet.noneOf(WatchType::class.java) getKey(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let { list.addAll(it) @@ -445,6 +464,11 @@ class HomeViewModel : ViewModel() { loadStoredData(list) } + fun reloadStored() { + loadResumeWatching() + loadStoredData() + } + fun click(load: LoadClickCallback) { loadResult(load.response.url, load.response.apiName, load.action) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index 41326eb8..96593769 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -25,6 +25,7 @@ import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_T import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_USER_KEY import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi.Companion.OPEN_SUBTITLES_USER_KEY +import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs @@ -143,66 +144,26 @@ object BackupUtils { } @SuppressLint("SimpleDateFormat") - fun FragmentActivity.backup() { + fun FragmentActivity.backup() = ioSafe { var fileStream: OutputStream? = null var printStream: PrintWriter? = null try { if (!checkWrite()) { - showToast(getString(R.string.backup_failed), Toast.LENGTH_LONG) + showToast(R.string.backup_failed, Toast.LENGTH_LONG) requestRW() - return + return@ioSafe } val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) - val ext = "json" + val ext = "txt" val displayName = "CS3_Backup_${date}" val backupFile = getBackup() - val stream = setupStream(this, displayName, null, ext, false) + val stream = setupStream(this@backup, displayName, null, ext, false) fileStream = stream.openNew() printStream = PrintWriter(fileStream) printStream.print(mapper.writeValueAsString(backupFile)) - /*val steam = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q - && subDir?.isDownloadDir() == true - ) { - val cr = this.contentResolver - val contentUri = - MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI - //val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - - val newFile = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) - put(MediaStore.MediaColumns.TITLE, displayName) - // While it a json file we store as txt because not - // all file managers support mimetype json - put(MediaStore.MediaColumns.MIME_TYPE, "text/plain") - //put(MediaStore.MediaColumns.RELATIVE_PATH, folder) - } - - val newFileUri = cr.insert( - contentUri, - newFile - ) ?: throw IOException("Error creating file uri") - cr.openOutputStream(newFileUri, "w") - ?: throw IOException("Error opening stream") - } else { - val fileName = "$displayName.$ext" - val rFile = subDir?.findFile(fileName) - if (rFile?.exists() == true) { - rFile.delete() - } - val file = - subDir?.createFile(fileName) - ?: throw IOException("Error creating file") - if (!file.exists()) throw IOException("File does not exist") - file.openOutputStream() - } - - val printStream = PrintWriter(steam) - printStream.print(mapper.writeValueAsString(backupFile)) - printStream.close()*/ - showToast( R.string.backup_success, Toast.LENGTH_LONG @@ -211,7 +172,7 @@ object BackupUtils { logError(e) try { showToast( - getString(R.string.backup_failed_error_format).format(e.toString()), + txt(R.string.backup_failed_error_format, e.toString()), Toast.LENGTH_LONG ) } catch (e: Exception) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 991651dc..2eb2ab01 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -353,6 +353,12 @@ object DataStoreHelper { removeKeys(folder2) } + fun deleteBookmarkedData(id : Int?) { + if (id == null) return + removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString()) + removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) + } + fun getAllResumeStateIds(): List? { val folder = "$currentAccount/$RESULT_RESUME_WATCHING" return getKeys(folder)?.mapNotNull { @@ -519,12 +525,10 @@ object DataStoreHelper { fun setResultWatchState(id: Int?, status: Int) { if (id == null) return - val folder = "$currentAccount/$RESULT_WATCH_STATE" if (status == WatchType.NONE.internalId) { - removeKey(folder, id.toString()) - removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) + deleteBookmarkedData(id) } else { - setKey(folder, id.toString(), status) + setKey("$currentAccount/$RESULT_WATCH_STATE", id.toString(), status) } } From ce1f48978be3aada3e6b68d8726539375c3f14f1 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 28 Aug 2023 20:56:58 +0200 Subject: [PATCH 121/156] fixed download error --- app/build.gradle.kts | 2 +- .../cloudstream3/extractors/SpeedoStream.kt | 13 +++++++++---- .../lagradost/cloudstream3/utils/ExtractorApi.kt | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 333fbfb8..3927d081 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -233,7 +233,7 @@ dependencies { // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") // Util to skip the URI file fuckery 🙏 - implementation("com.github.LagradOst:SafeFile:0.0.2") + implementation("com.github.LagradOst:SafeFile:0.0.3") // API because cba maintaining it myself implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt index 3f6fff2f..213ecdf3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt @@ -7,15 +7,22 @@ import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +class SpeedoStream2 : SpeedoStream() { + override val mainUrl = "https://speedostream.mom" +} + class SpeedoStream1 : SpeedoStream() { override val mainUrl = "https://speedostream.pm" } open class SpeedoStream : ExtractorApi() { override val name = "SpeedoStream" - override val mainUrl = "https://speedostream.mom" + override val mainUrl = "https://speedostream.bond" override val requiresReferer = true + // .bond, .pm, .mom redirect to .bond + private val hostUrl = "https://speedostream.bond" + override suspend fun getUrl(url: String, referer: String?): List { val sources = mutableListOf() app.get(url, referer = referer).document.select("script").map { script -> @@ -26,7 +33,7 @@ open class SpeedoStream : ExtractorApi() { M3u8Helper.generateM3u8( name, it.file, - "$mainUrl/", + "$hostUrl/", ).forEach { m3uData -> sources.add(m3uData) } } } @@ -37,6 +44,4 @@ open class SpeedoStream : ExtractorApi() { private data class File( @JsonProperty("file") val file: String, ) - - } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 83c61542..ffda32d7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -389,6 +389,7 @@ val extractorApis: MutableList = arrayListOf( Acefile(), SpeedoStream(), SpeedoStream1(), + SpeedoStream2(), Zorofile(), Embedgram(), Mvidoo(), From 6089cbc48493d746a110bba8d9997c3d2209b82e Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 30 Aug 2023 00:52:34 +0200 Subject: [PATCH 122/156] fixed subs on downloads --- app/build.gradle.kts | 2 +- .../ui/player/DownloadFileGenerator.kt | 73 +++++++++++-------- .../utils/VideoDownloadManager.kt | 2 +- 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3927d081..55d0f7ae 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -233,7 +233,7 @@ dependencies { // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") // Util to skip the URI file fuckery 🙏 - implementation("com.github.LagradOst:SafeFile:0.0.3") + implementation("com.github.LagradOst:SafeFile:0.0.5") // API because cba maintaining it myself implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index baf7ed52..1b618e45 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -50,6 +50,10 @@ class DownloadFileGenerator( return null } + fun cleanDisplayName(name: String): String { + return name.substringBeforeLast('.').trim() + } + override suspend fun generateLinks( clearCache: Boolean, isCasting: Boolean, @@ -58,39 +62,48 @@ class DownloadFileGenerator( offset: Int, ): Boolean { val meta = episodes[currentIndex + offset] - callback(Pair(null, meta)) + callback(null to meta) - context?.let { ctx -> - val relative = meta.relativePath - val display = meta.displayName + val ctx = context ?: return true + val relative = meta.relativePath ?: return true + val display = meta.displayName ?: return true - if (display == null || relative == null) { - return@let + val cleanDisplay = cleanDisplayName(display) + + VideoDownloadManager.getFolder(ctx, relative, meta.basePath) + ?.forEach { (name, uri) -> + // 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) + + // we only want files with the approx same name + if (!cleanName.startsWith(cleanDisplay, true)) return@forEach + + val realName = cleanName.removePrefix(cleanDisplay) + + subtitleCallback( + SubtitleData( + realName.ifBlank { ctx.getString(R.string.default_subtitles) }, + uri.toString(), + SubtitleOrigin.DOWNLOADED_FILE, + name.toSubtitleMimeType(), + emptyMap() + ) + ) } - VideoDownloadManager.getFolder(ctx, relative, meta.basePath) - ?.forEach { file -> - val name = display.removeSuffix(".mp4") - if (file.first != meta.displayName && file.first.startsWith(name)) { - val realName = file.first.removePrefix(name) - .removeSuffix(".vtt") - .removeSuffix(".srt") - .removeSuffix(".txt") - .trim() - .removePrefix("(") - .removeSuffix(")") - - subtitleCallback( - SubtitleData( - realName.ifBlank { ctx.getString(R.string.default_subtitles) }, - file.second.toString(), - SubtitleOrigin.DOWNLOADED_FILE, - name.toSubtitleMimeType(), - emptyMap() - ) - ) - } - } - } return true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 6425ba66..72eb002a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -505,7 +505,7 @@ object VideoDownloadManager { ): List>? { val base = basePathToFile(context, basePath) val folder = base?.gotoDirectory(relativePath, false) ?: return null - if (folder.isDirectory() != false) return null + //if (folder.isDirectory() != false) return null return folder.listFiles() ?.mapNotNull { (it.name() ?: "") to (it.uri() ?: return@mapNotNull null) } From 9c991f2abd53bdbf50f7ae52f3fe64221d341e57 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Sat, 2 Sep 2023 05:32:18 +0700 Subject: [PATCH 123/156] extractor: fix chillx (#583) * Extractor: added Rabbitstream * Extractor: added Rabbitstream * extractor: fix Chillx * comply --------- Co-authored-by: Sofie99 --- .../cloudstream3/extractors/Chillx.kt | 58 +---------- .../cloudstream3/extractors/Gdriveplayer.kt | 88 +---------------- .../extractors/helper/AesHelper.kt | 95 +++++++++++++++++++ 3 files changed, 102 insertions(+), 139 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt index b4f3d897..bcf8848c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -2,15 +2,12 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.extractors.helper.* +import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities -import javax.crypto.Cipher -import javax.crypto.SecretKeyFactory -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.PBEKeySpec -import javax.crypto.spec.SecretKeySpec class Moviesapi : Chillx() { override val name = "Moviesapi" @@ -32,7 +29,7 @@ open class Chillx : ExtractorApi() { override val requiresReferer = true companion object { - private const val KEY = "11x&W5UBrcqn\$9Yl" + private const val KEY = "m4H6D9%0\$N&F6rQ&" } override suspend fun getUrl( @@ -47,8 +44,7 @@ open class Chillx : ExtractorApi() { referer = referer ).text )?.groupValues?.get(1) - val encData = AppUtils.tryParseJson(base64Decode(master ?: return)) - val decrypt = cryptoAESHandler(encData ?: return, KEY, false) + val decrypt = cryptoAESHandler(master ?: return, KEY.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1) @@ -86,52 +82,6 @@ open class Chillx : ExtractorApi() { } } - private fun cryptoAESHandler( - data: AESData, - pass: String, - encrypt: Boolean = true - ): String { - val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512") - val spec = PBEKeySpec( - pass.toCharArray(), - data.salt?.hexToByteArray(), - data.iterations?.toIntOrNull() ?: 1, - 256 - ) - val key = factory.generateSecret(spec) - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - return if (!encrypt) { - cipher.init( - Cipher.DECRYPT_MODE, - SecretKeySpec(key.encoded, "AES"), - IvParameterSpec(data.iv?.hexToByteArray()) - ) - String(cipher.doFinal(base64DecodeArray(data.ciphertext.toString()))) - } else { - cipher.init( - Cipher.ENCRYPT_MODE, - SecretKeySpec(key.encoded, "AES"), - IvParameterSpec(data.iv?.hexToByteArray()) - ) - base64Encode(cipher.doFinal(data.ciphertext?.toByteArray())) - } - } - - private fun String.hexToByteArray(): ByteArray { - check(length % 2 == 0) { "Must have an even length" } - return chunked(2) - .map { it.toInt(16).toByte() } - - .toByteArray() - } - - data class AESData( - @JsonProperty("ciphertext") val ciphertext: String? = null, - @JsonProperty("iv") val iv: String? = null, - @JsonProperty("salt") val salt: String? = null, - @JsonProperty("iterations") val iterations: String? = null, - ) - data class Tracks( @JsonProperty("file") val file: String? = null, @JsonProperty("label") val label: String? = null, diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt index df9c74a4..8d1a4d07 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt @@ -2,14 +2,10 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import org.jsoup.nodes.Element -import java.security.DigestException -import java.security.MessageDigest -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec class DatabaseGdrive2 : Gdriveplayer() { override var mainUrl = "https://databasegdriveplayer.co" @@ -65,78 +61,6 @@ open class Gdriveplayer : ExtractorApi() { ?.data()?.let { getAndUnpack(it) } } - private fun String.decodeHex(): ByteArray { - check(length % 2 == 0) { "Must have an even length" } - return chunked(2) - .map { it.toInt(16).toByte() } - .toByteArray() - } - - // https://stackoverflow.com/a/41434590/8166854 - private fun GenerateKeyAndIv( - password: ByteArray, - salt: ByteArray, - hashAlgorithm: String = "MD5", - keyLength: Int = 32, - ivLength: Int = 16, - iterations: Int = 1 - ): List? { - - val md = MessageDigest.getInstance(hashAlgorithm) - val digestLength = md.digestLength - val targetKeySize = keyLength + ivLength - val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength - val generatedData = ByteArray(requiredLength) - var generatedLength = 0 - - try { - md.reset() - - while (generatedLength < targetKeySize) { - if (generatedLength > 0) - md.update( - generatedData, - generatedLength - digestLength, - digestLength - ) - - md.update(password) - md.update(salt, 0, 8) - md.digest(generatedData, generatedLength, digestLength) - - for (i in 1 until iterations) { - md.update(generatedData, generatedLength, digestLength) - md.digest(generatedData, generatedLength, digestLength) - } - - generatedLength += digestLength - } - return listOf( - generatedData.copyOfRange(0, keyLength), - generatedData.copyOfRange(keyLength, targetKeySize) - ) - } catch (e: DigestException) { - return null - } - } - - private fun cryptoAESHandler( - data: AesData, - pass: ByteArray, - encrypt: Boolean = true - ): String? { - val (key, iv) = GenerateKeyAndIv(pass, data.s.decodeHex()) ?: return null - val cipher = Cipher.getInstance("AES/CBC/NoPadding") - return if (!encrypt) { - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) - String(cipher.doFinal(base64DecodeArray(data.ct))) - } else { - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) - base64Encode(cipher.doFinal(data.ct.toByteArray())) - - } - } - private fun Regex.first(str: String): String? { return find(str)?.groupValues?.getOrNull(1) } @@ -154,14 +78,14 @@ open class Gdriveplayer : ExtractorApi() { val document = app.get(url).document val eval = unpackJs(document)?.replace("\\", "") ?: return - val data = tryParseJson(Regex("data='(\\S+?)'").first(eval)) ?: return + val data = Regex("data='(\\S+?)'").first(eval) ?: return val password = Regex("null,['|\"](\\w+)['|\"]").first(eval) ?.split(Regex("\\D+")) ?.joinToString("") { Char(it.toInt()).toString() }.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() } ?: throw ErrorLoadingException("can't find password") - val decryptedData = cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "") + val decryptedData = cryptoAESHandler(data, password, false, "AES/CBC/NoPadding")?.let { getAndUnpack(it) }?.replace("\\", "") val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],") val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],") @@ -194,12 +118,6 @@ open class Gdriveplayer : ExtractorApi() { } - data class AesData( - @JsonProperty("ct") val ct: String, - @JsonProperty("iv") val iv: String, - @JsonProperty("s") val s: String - ) - data class Tracks( @JsonProperty("file") val file: String, @JsonProperty("kind") val kind: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt new file mode 100644 index 00000000..b41eae52 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt @@ -0,0 +1,95 @@ +package com.lagradost.cloudstream3.extractors.helper + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.base64DecodeArray +import com.lagradost.cloudstream3.base64Encode +import com.lagradost.cloudstream3.utils.AppUtils +import java.security.DigestException +import java.security.MessageDigest +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +object AesHelper { + + private const val HASH = "AES/CBC/PKCS5PADDING" + private const val KDF = "MD5" + + fun cryptoAESHandler( + data: String, + pass: ByteArray, + encrypt: Boolean = true, + padding: String = HASH, + ): String? { + val parse = AppUtils.tryParseJson(data) ?: return null + val (key, iv) = generateKeyAndIv(pass, parse.s.hexToByteArray()) ?: return null + val cipher = Cipher.getInstance(padding) + return if (!encrypt) { + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + String(cipher.doFinal(base64DecodeArray(parse.ct))) + } else { + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + base64Encode(cipher.doFinal(parse.ct.toByteArray())) + } + } + + // https://stackoverflow.com/a/41434590/8166854 + fun generateKeyAndIv( + password: ByteArray, + salt: ByteArray, + hashAlgorithm: String = KDF, + keyLength: Int = 32, + ivLength: Int = 16, + iterations: Int = 1 + ): Pair? { + + val md = MessageDigest.getInstance(hashAlgorithm) + val digestLength = md.digestLength + val targetKeySize = keyLength + ivLength + val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength + val generatedData = ByteArray(requiredLength) + var generatedLength = 0 + + try { + md.reset() + + while (generatedLength < targetKeySize) { + if (generatedLength > 0) + md.update( + generatedData, + generatedLength - digestLength, + digestLength + ) + + md.update(password) + md.update(salt, 0, 8) + md.digest(generatedData, generatedLength, digestLength) + + for (i in 1 until iterations) { + md.update(generatedData, generatedLength, digestLength) + md.digest(generatedData, generatedLength, digestLength) + } + + generatedLength += digestLength + } + return generatedData.copyOfRange(0, keyLength) to generatedData.copyOfRange(keyLength, targetKeySize) + } catch (e: DigestException) { + return null + } + } + + fun String.hexToByteArray(): ByteArray { + check(length % 2 == 0) { "Must have an even length" } + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + } + + private data class AesData( + @JsonProperty("ct") val ct: String, + @JsonProperty("iv") val iv: String, + @JsonProperty("s") val s: String + ) + +} \ No newline at end of file From 6211b02e85b0dcd4ab5a2954623917e8c27ba552 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 3 Sep 2023 23:32:43 +0200 Subject: [PATCH 124/156] switched from isM3u8 to ExtractorLinkType --- .../cloudstream3/extractors/Pelisplus.kt | 3 +- .../cloudstream3/extractors/Vidstream.kt | 3 +- .../cloudstream3/extractors/WcoStream.kt | 4 +- .../cloudstream3/extractors/Wibufile.kt | 6 +- .../cloudstream3/ui/APIRepository.kt | 14 ++- .../cloudstream3/ui/ControllerActivity.kt | 4 +- .../cloudstream3/ui/player/CS3IPlayer.kt | 12 +- .../ui/player/DownloadFileGenerator.kt | 4 +- .../ui/player/ExtractorLinkGenerator.kt | 7 +- .../cloudstream3/ui/player/IGenerator.kt | 43 ++++++- .../cloudstream3/ui/player/LinkGenerator.kt | 2 +- .../ui/player/PlayerGeneratorViewModel.kt | 12 +- .../ui/player/RepoLinkGenerator.kt | 21 ++-- .../ui/result/ResultViewModel2.kt | 40 +++--- .../cloudstream3/utils/CastHelper.kt | 6 +- .../cloudstream3/utils/ExtractorApi.kt | 119 +++++++++++++++--- .../utils/VideoDownloadManager.kt | 78 +++++++----- 17 files changed, 269 insertions(+), 109 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt index 45ec4c2f..4163cd94 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt @@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.extractorApis import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.loadExtractor @@ -66,7 +67,7 @@ open class Pelisplus(val mainUrl: String) { href, page.url, getQualityFromName(qual), - element.attr("href").contains(".m3u8") + type = INFER_TYPE ) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt index 7eb7fbac..c6493dbe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt @@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.argamap import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.extractorApis import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.loadExtractor @@ -70,7 +71,7 @@ class Vidstream(val mainUrl: String) { href, page.url, getQualityFromName(qual), - element.attr("href").contains(".m3u8") + type = INFER_TYPE ) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt index 6cc486cd..659d7804 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt @@ -7,6 +7,7 @@ import com.lagradost.cloudstream3.extractors.helper.NineAnimeHelper.encrypt import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.Qualities class Vidstreamz : WcoStream() { @@ -126,8 +127,7 @@ open class WcoStream : ExtractorApi() { if (!response.text.startsWith("{")) throw ErrorLoadingException("Seems like 9Anime kiddies changed stuff again, Go touch some grass for bout an hour Or use a different Server") return response.parsed().data.media.sources.map { - ExtractorLink(name, it.file,it.file,host,Qualities.Unknown.value,it.file.contains(".m3u8")) + ExtractorLink(name, it.file, it.file, host, Qualities.Unknown.value, type = INFER_TYPE) } - } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt index ae1e872a..c69f0938 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt @@ -4,8 +4,8 @@ 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.INFER_TYPE import com.lagradost.cloudstream3.utils.Qualities -import java.net.URI open class Wibufile : ExtractorApi() { override val name: String = "Wibufile" @@ -28,10 +28,8 @@ open class Wibufile : ExtractorApi() { video ?: return, "$mainUrl/", Qualities.Unknown.value, - URI(url).path.endsWith(".m3u8") + type = INFER_TYPE ) ) - } - } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index 4ab2e8e2..a075cc2e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -1,16 +1,24 @@ package com.lagradost.cloudstream3.ui -import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.DubStatus +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.HomePageResponse +import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent +import com.lagradost.cloudstream3.MainPageRequest +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.fixUrl import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.ExtractorLink import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope.coroutineContext import kotlinx.coroutines.async import kotlinx.coroutines.delay @@ -174,7 +182,7 @@ class APIRepository(val api: MainAPI) { data: String, isCasting: Boolean, subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit + callback: (ExtractorLink) -> Unit, ): Boolean { if (isInvalidData(data)) return false // this makes providers cleaner return try { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index 46ddce09..6c0e7796 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -25,6 +25,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.sortSubs import com.lagradost.cloudstream3.sortUrls +import com.lagradost.cloudstream3.ui.player.LoadType import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultEpisode @@ -294,7 +295,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi val generator = RepoLinkGenerator(listOf(epData)) val isSuccessful = safeApiCall { - generator.generateLinks(clearCache = false, isCasting = true, + generator.generateLinks( + clearCache = false, type = LoadType.Chromecast, callback = { it.first?.let { link -> currentLinks.add(link) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 2067eb04..fd1da5ca 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -53,9 +53,11 @@ import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList +import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File +import java.lang.IllegalArgumentException import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.SSLSession @@ -1257,10 +1259,12 @@ class CS3IPlayer : IPlayer { HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) } - val mime = when { - link.isM3u8 -> MimeTypes.APPLICATION_M3U8 - link.isDash -> MimeTypes.APPLICATION_MPD - else -> MimeTypes.VIDEO_MP4 + val mime = when(link.type) { + ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8 + ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD + ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4 + ExtractorLinkType.TORRENT -> throw IllegalArgumentException("No torrent support") + ExtractorLinkType.MAGNET -> throw IllegalArgumentException("No magnet support") } val mediaItems = if (link is ExtractorLinkPlayList) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index 1b618e45..b0223bb5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -56,10 +56,10 @@ class DownloadFileGenerator( override suspend fun generateLinks( clearCache: Boolean, - isCasting: Boolean, + type: LoadType, callback: (Pair) -> Unit, subtitleCallback: (SubtitleData) -> Unit, - offset: Int, + offset: Int ): Boolean { val meta = episodes[currentIndex + offset] callback(null to meta) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt index 7c19e97d..d8d2d537 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt @@ -37,14 +37,17 @@ class ExtractorLinkGenerator( override suspend fun generateLinks( clearCache: Boolean, - isCasting: Boolean, + type: LoadType, callback: (Pair) -> Unit, subtitleCallback: (SubtitleData) -> Unit, offset: Int ): Boolean { subtitles.forEach(subtitleCallback) + val allowedTypes = type.toSet() links.forEach { - callback.invoke(it to null) + if(allowedTypes.contains(it.type)) { + callback.invoke(it to null) + } } return true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt index a1287e6a..af74cb57 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt @@ -1,8 +1,43 @@ package com.lagradost.cloudstream3.ui.player import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.ExtractorUri +enum class LoadType { + Unknown, + InApp, + InAppDownload, + ExternalApp, + Browser, + Chromecast +} + +fun LoadType.toSet() : Set { + return when(this) { + LoadType.InApp -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.DASH, + ExtractorLinkType.M3U8 + ) + LoadType.Browser -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.DASH, + ExtractorLinkType.M3U8 + ) + LoadType.InAppDownload -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.M3U8 + ) + LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet() + LoadType.Chromecast -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.DASH, + ExtractorLinkType.M3U8 + ) + } +} + interface IGenerator { val hasCache: Boolean @@ -13,15 +48,15 @@ interface IGenerator { fun goto(index: Int) fun getCurrentId(): Int? // this is used to save data or read data about this id - fun getCurrent(offset : Int = 0): Any? // this is used to get metadata about the current playing, can return null - fun getAll() : List? // this us used to get the metadata about all entries, not needed + fun getCurrent(offset: Int = 0): Any? // this is used to get metadata about the current playing, can return null + fun getAll(): List? // this us used to get the metadata about all entries, not needed /* not safe, must use try catch */ suspend fun generateLinks( clearCache: Boolean, - isCasting: Boolean, + type: LoadType, callback: (Pair) -> Unit, subtitleCallback: (SubtitleData) -> Unit, - offset : Int = 0, + offset: Int = 0, ): Boolean } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt index 0b560857..ba2cdb40 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt @@ -48,7 +48,7 @@ class LinkGenerator( override suspend fun generateLinks( clearCache: Boolean, - isCasting: Boolean, + type: LoadType, callback: (Pair) -> Unit, subtitleCallback: (SubtitleData) -> Unit, offset: Int diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 1b13b519..42659f8d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -78,10 +78,10 @@ class PlayerGeneratorViewModel : ViewModel() { if (generator?.hasCache == true && generator?.hasNext() == true) { safeApiCall { generator?.generateLinks( + type = LoadType.InApp, clearCache = false, - isCasting = false, - {}, - {}, + callback = {}, + subtitleCallback = {}, offset = 1 ) } @@ -147,7 +147,7 @@ class PlayerGeneratorViewModel : ViewModel() { } } - fun loadLinks(clearCache: Boolean = false, isCasting: Boolean = false) { + fun loadLinks(clearCache: Boolean = false, type: LoadType = LoadType.InApp) { Log.i(TAG, "loadLinks") currentJob?.cancel() @@ -162,14 +162,14 @@ class PlayerGeneratorViewModel : ViewModel() { // load more data _loadingLinks.postValue(Resource.Loading()) val loadingState = safeApiCall { - generator?.generateLinks(clearCache = clearCache, isCasting = isCasting, { + generator?.generateLinks(type = type,clearCache = clearCache, callback = { currentLinks.add(it) // Clone to prevent ConcurrentModificationException normalSafeApiCall { // Extra normalSafeApiCall since .toSet() iterates. _currentLinks.postValue(currentLinks.toSet()) } - }, { + }, subtitleCallback = { currentSubs.add(it) normalSafeApiCall { _currentSubs.postValue(currentSubs.toSet()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt index 2ce53ea5..d55da57c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt @@ -67,18 +67,19 @@ class RepoLinkGenerator( override suspend fun generateLinks( clearCache: Boolean, - isCasting: Boolean, + type: LoadType, callback: (Pair) -> Unit, subtitleCallback: (SubtitleData) -> Unit, - offset: Int, + offset: Int ): Boolean { + val allowedTypes = type.toSet() val index = currentIndex val current = episodes.getOrNull(index + offset) ?: return false val (currentLinkCache, currentSubsCache) = if (clearCache) { Pair(mutableSetOf(), mutableSetOf()) } else { - cache[Pair(current.apiName, current.id)] ?: Pair(mutableSetOf(), mutableSetOf()) + cache[current.apiName to current.id] ?: Pair(mutableSetOf(), mutableSetOf()) } //val currentLinkCache = if (clearCache) mutableSetOf() else linkCache[index].toMutableSet() @@ -88,9 +89,9 @@ class RepoLinkGenerator( val currentSubsUrls = mutableSetOf() // makes all subs urls unique val currentSubsNames = mutableSetOf() // makes all subs names unique - currentLinkCache.forEach { link -> + currentLinkCache.filter { allowedTypes.contains(it.type) }.forEach { link -> currentLinks.add(link.url) - callback(Pair(link, null)) + callback(link to null) } currentSubsCache.forEach { sub -> @@ -108,8 +109,8 @@ class RepoLinkGenerator( val result = APIRepository( getApiFromNameNull(current.apiName) ?: throw Exception("This provider does not exist") ).loadLinks(current.data, - isCasting, - { file -> + isCasting = LoadType.Chromecast == type, + subtitleCallback = { file -> val correctFile = PlayerSubtitleHelper.getSubtitleData(file) if (!currentSubsUrls.contains(correctFile.url)) { currentSubsUrls.add(correctFile.url) @@ -132,12 +133,14 @@ class RepoLinkGenerator( } } }, - { link -> + callback = { link -> Log.d(TAG, "Loaded ExtractorLink: $link") if (!currentLinks.contains(link.url)) { if (!currentLinkCache.contains(link)) { currentLinks.add(link.url) - callback(Pair(link, null)) + if (allowedTypes.contains(link.type)) { + callback(Pair(link, null)) + } currentLinkCache.add(link) //linkCache[index] = currentLinkCache } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 82d9a8fe..b2c57137 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -36,6 +36,7 @@ import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.player.IGenerator +import com.lagradost.cloudstream3.ui.player.LoadType import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction @@ -745,7 +746,7 @@ class ResultViewModel2 : ViewModel() { val generator = RepoLinkGenerator(listOf(episode)) val currentLinks = mutableSetOf() val currentSubs = mutableSetOf() - generator.generateLinks(clearCache = false, isCasting = false, callback = { + generator.generateLinks(clearCache = false, LoadType.Chromecast, callback = { it.first?.let { link -> currentLinks.add(link) } @@ -825,7 +826,7 @@ class ResultViewModel2 : ViewModel() { isVisible: Boolean = true ) { if (activity == null) return - loadLinks(result, isVisible = isVisible, isCasting = true) { data -> + loadLinks(result, isVisible = isVisible, LoadType.Chromecast) { data -> startChromecast(activity, result, data.links, data.subs, 0) } } @@ -936,7 +937,7 @@ class ResultViewModel2 : ViewModel() { private fun loadLinks( result: ResultEpisode, isVisible: Boolean, - isCasting: Boolean, + type: LoadType, clearCache: Boolean = false, work: suspend (CoroutineScope.(LinkLoadingResult) -> Unit) ) { @@ -945,7 +946,7 @@ class ResultViewModel2 : ViewModel() { val links = loadLinks( result, isVisible = isVisible, - isCasting = isCasting, + type = type, clearCache = clearCache ) if (!this.isActive) return@ioSafe @@ -956,11 +957,11 @@ class ResultViewModel2 : ViewModel() { private var currentLoadLinkJob: Job? = null private fun acquireSingleLink( result: ResultEpisode, - isCasting: Boolean, + type: LoadType, text: UiText, callback: (Pair) -> Unit, ) { - loadLinks(result, isVisible = true, isCasting = isCasting) { links -> + loadLinks(result, isVisible = true, type) { links -> postPopup( text, links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) { @@ -971,11 +972,10 @@ class ResultViewModel2 : ViewModel() { private fun acquireSingleSubtitle( result: ResultEpisode, - isCasting: Boolean, text: UiText, callback: (Pair) -> Unit, ) { - loadLinks(result, isVisible = true, isCasting = isCasting) { links -> + loadLinks(result, isVisible = true, type = LoadType.Unknown) { links -> postPopup( text, links.subs.map { txt(it.name) }) @@ -988,7 +988,7 @@ class ResultViewModel2 : ViewModel() { private suspend fun CoroutineScope.loadLinks( result: ResultEpisode, isVisible: Boolean, - isCasting: Boolean, + type: LoadType, clearCache: Boolean = false, ): LinkLoadingResult { val tempGenerator = RepoLinkGenerator(listOf(result)) @@ -1002,7 +1002,7 @@ class ResultViewModel2 : ViewModel() { } try { updatePage() - tempGenerator.generateLinks(clearCache, isCasting, { (link, _) -> + tempGenerator.generateLinks(clearCache, type, { (link, _) -> if (link != null) { links += link updatePage() @@ -1272,7 +1272,6 @@ class ResultViewModel2 : ViewModel() { acquireSingleSubtitle( click.data, - false, txt(R.string.episode_action_download_subtitle) ) { (links, index) -> downloadSubtitle( @@ -1317,7 +1316,7 @@ class ResultViewModel2 : ViewModel() { val response = currentResponse ?: return acquireSingleLink( click.data, - false, + LoadType.InAppDownload, txt(R.string.episode_action_download_mirror) ) { (result, index) -> ioSafe { @@ -1347,7 +1346,7 @@ class ResultViewModel2 : ViewModel() { loadLinks( click.data, isVisible = false, - isCasting = false, + type = LoadType.InApp, clearCache = true ) } @@ -1356,7 +1355,7 @@ class ResultViewModel2 : ViewModel() { ACTION_CHROME_CAST_MIRROR -> { acquireSingleLink( click.data, - isCasting = true, + LoadType.Chromecast, txt(R.string.episode_action_chromecast_mirror) ) { (result, index) -> startChromecast(activity, click.data, result.links, result.subs, index) @@ -1365,7 +1364,7 @@ class ResultViewModel2 : ViewModel() { ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink( click.data, - isCasting = true, + LoadType.Browser, txt(R.string.episode_action_play_in_browser) ) { (result, index) -> try { @@ -1380,7 +1379,7 @@ class ResultViewModel2 : ViewModel() { ACTION_COPY_LINK -> { acquireSingleLink( click.data, - isCasting = true, + LoadType.ExternalApp, txt(R.string.episode_action_copy_link) ) { (result, index) -> val act = activity ?: return@acquireSingleLink @@ -1399,7 +1398,7 @@ class ResultViewModel2 : ViewModel() { } ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> { - loadLinks(click.data, isVisible = true, isCasting = true) { links -> + loadLinks(click.data, isVisible = true, LoadType.ExternalApp) { links -> if (links.links.isEmpty()) { showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT) return@loadLinks @@ -1415,7 +1414,7 @@ class ResultViewModel2 : ViewModel() { ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink( click.data, - isCasting = true, + LoadType.Chromecast, txt( R.string.episode_action_play_in_format, txt(R.string.player_settings_play_in_web) @@ -1432,7 +1431,7 @@ class ResultViewModel2 : ViewModel() { ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink( click.data, - isCasting = true, + LoadType.Chromecast, txt( R.string.episode_action_play_in_format, txt(R.string.player_settings_play_in_mpv) @@ -1461,7 +1460,6 @@ class ResultViewModel2 : ViewModel() { if (index >= 0) it.goto(index) } - } ?: return, list ) ) @@ -2173,7 +2171,7 @@ class ResultViewModel2 : ViewModel() { trailerData.extractorUrl, trailerData.referer ?: "", Qualities.Unknown.value, - trailerData.extractorUrl.contains(".m3u8") + type = INFER_TYPE ) ) to arrayListOf() } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt index 6b5e9ec2..d8373165 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt @@ -55,7 +55,11 @@ object CastHelper { val builder = MediaInfo.Builder(link.url) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setContentType(if (link.isM3u8) MimeTypes.APPLICATION_M3U8 else MimeTypes.VIDEO_MP4) + .setContentType(when(link.type) { + ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8 + ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD + else -> MimeTypes.VIDEO_MP4 + }) .setMetadata(movieMetadata) .setMediaTracks(tracks) data?.let { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index ffda32d7..2a539f0d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -4,8 +4,10 @@ import android.net.Uri import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.extractors.* +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay import org.jsoup.Jsoup +import java.net.URL import kotlin.collections.MutableList /** @@ -35,35 +37,101 @@ data class ExtractorLinkPlayList( val playlist: List, override val referer: String, override val quality: Int, - override val isM3u8: Boolean = false, + val isM3u8: Boolean = false, override val headers: Map = mapOf(), /** Used for getExtractorVerifierJob() */ override val extractorData: String? = null, + override val type: ExtractorLinkType, ) : ExtractorLink( - source, - name, - // Blank as un-used - "", - referer, - quality, - isM3u8, - headers, - extractorData -) + source = source, + name = name, + url = "", + referer = referer, + quality = quality, + headers = headers, + extractorData = extractorData, + type = type +) { + constructor( + source: String, + name: String, + playlist: List, + referer: String, + quality: Int, + isM3u8: Boolean = false, + headers: Map = mapOf(), + extractorData: String? = null, + ) : this( + source = source, + name = name, + playlist = playlist, + referer = referer, + quality = quality, + type = if (isM3u8) ExtractorLinkType.M3U8 else ExtractorLinkType.VIDEO, + headers = headers, + extractorData = extractorData, + ) +} +/** Metadata about the file type used for downloads and exoplayer hint, + * if you respond with the wrong one the file will fail to download or be played */ +enum class ExtractorLinkType { + /** Single stream of bytes no matter the actual file type */ + VIDEO, + /** Split into several .ts files, has support for encrypted m3u8s */ + M3U8, + /** Like m3u8 but uses xml, currently no download support */ + DASH, + /** No support at the moment */ + TORRENT, + /** No support at the moment */ + MAGNET, +} +private fun inferTypeFromUrl(url: String): ExtractorLinkType { + val path = normalSafeApiCall { URL(url).path } + return when { + path?.endsWith(".m3u8") == true -> ExtractorLinkType.M3U8 + path?.endsWith(".mpd") == true -> ExtractorLinkType.DASH + path?.endsWith(".torrent") == true -> ExtractorLinkType.TORRENT + url.startsWith("magnet:") -> ExtractorLinkType.MAGNET + else -> ExtractorLinkType.VIDEO + } +} +val INFER_TYPE : ExtractorLinkType? = null open class ExtractorLink constructor( open val source: String, open val name: String, override val url: String, override val referer: String, open val quality: Int, - open val isM3u8: Boolean = false, override val headers: Map = mapOf(), /** Used for getExtractorVerifierJob() */ open val extractorData: String? = null, - open val isDash: Boolean = false, + open val type: ExtractorLinkType, ) : VideoDownloadManager.IDownloadableMinimum { + constructor( + source: String, + name: String, + url: String, + referer: String, + quality: Int, + /** the type of the media, use INFER_TYPE if you want to auto infer the type from the url */ + type: ExtractorLinkType?, + headers: Map = mapOf(), + /** Used for getExtractorVerifierJob() */ + extractorData: String? = null, + ) : this( + source = source, + name = name, + url = url, + referer = referer, + quality = quality, + headers = headers, + extractorData = extractorData, + type = type ?: inferTypeFromUrl(url) + ) + /** * Old constructor without isDash, allows for backwards compatibility with extensions. * Should be removed after all extensions have updated their cloudstream.jar @@ -80,8 +148,30 @@ open class ExtractorLink constructor( extractorData: String? = null ) : this(source, name, url, referer, quality, isM3u8, headers, extractorData, false) + constructor( + source: String, + name: String, + url: String, + referer: String, + quality: Int, + isM3u8: Boolean = false, + headers: Map = mapOf(), + /** Used for getExtractorVerifierJob() */ + extractorData: String? = null, + isDash: Boolean, + ) : this( + source = source, + name = name, + url = url, + referer = referer, + quality = quality, + headers = headers, + extractorData = extractorData, + type = if (isDash) ExtractorLinkType.DASH else if (isM3u8) ExtractorLinkType.M3U8 else ExtractorLinkType.VIDEO + ) + override fun toString(): String { - return "ExtractorLink(name=$name, url=$url, referer=$referer, isM3u8=$isM3u8)" + return "ExtractorLink(name=$name, url=$url, referer=$referer, type=$type)" } } @@ -135,6 +225,7 @@ enum class Qualities(var value: Int, val defaultPriority: Int) { else -> "${qual}p" } } + fun getStringByIntFull(quality: Int): String { return when (quality) { 0 -> "Auto" diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 72eb002a..d108daed 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -53,7 +53,7 @@ import java.io.Closeable import java.io.File import java.io.IOException import java.io.OutputStream -import java.net.URL +import java.lang.IllegalArgumentException import java.util.* const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general" @@ -951,7 +951,10 @@ object VideoDownloadManager { /** how many bytes every connection should be, by default it is 10 MiB */ chuckSize: Long = (1 shl 20) * 10, /** maximum bytes in the buffer that responds */ - bufferSize: Int = DEFAULT_BUFFER_SIZE + bufferSize: Int = DEFAULT_BUFFER_SIZE, + /** how many bytes bytes it should require to use the parallel downloader instead, + * if we download a very small file we don't want it parallel */ + maximumSmallSize : Long = chuckSize * 2 ): LazyStreamDownloadData { // we don't want to make a separate connection for every 1kb require(chuckSize > 1000) @@ -963,7 +966,7 @@ object VideoDownloadManager { var downloadLength: Long? = null var totalLength: Long? = null - val ranges = if (contentLength == null) { + val ranges = if (contentLength == null || contentLength < maximumSmallSize) { // is the equivalent of [startByte..EOF] as we don't know the size we can only do one // connection LongArray(1) { startByte } @@ -1024,6 +1027,7 @@ object VideoDownloadManager { } } + /** download a file that consist of a single stream of data*/ suspend fun downloadThing( context: Context, link: IDownloadableMinimum, @@ -1035,8 +1039,7 @@ object VideoDownloadManager { createNotificationCallback: (CreateNotificationMetadata) -> Unit, parallelConnections: Int = 3 ): DownloadStatus = withContext(Dispatchers.IO) { - // we cant download torrents with this implementation, aria2c might be used in the future - if (link.url.startsWith("magnet") || link.url.endsWith(".torrent") || parallelConnections < 1) { + if (parallelConnections < 1) { return@withContext DOWNLOAD_INVALID_INPUT } @@ -1529,6 +1532,11 @@ object VideoDownloadManager { notificationCallback: (Int, Notification) -> Unit, tryResume: Boolean = false, ): DownloadStatus { + // no support for these file formats + if(link.type == ExtractorLinkType.MAGNET || link.type == ExtractorLinkType.TORRENT || link.type == ExtractorLinkType.DASH) { + return DOWNLOAD_INVALID_INPUT + } + val name = getFileName(context, ep) // Make sure this is cancelled when download is done or cancelled. @@ -1557,35 +1565,39 @@ object VideoDownloadManager { } try { - if (link.isM3u8 || normalSafeApiCall { URL(link.url).path.endsWith(".m3u8") } == true) { - val startIndex = if (tryResume) { - context.getKey( - KEY_DOWNLOAD_INFO, - ep.id.toString(), - null - )?.extraInfo?.toIntOrNull() - } else null + when(link.type) { + ExtractorLinkType.M3U8 -> { + val startIndex = if (tryResume) { + context.getKey( + KEY_DOWNLOAD_INFO, + ep.id.toString(), + null + )?.extraInfo?.toIntOrNull() + } else null - return downloadHLS( - context, - link, - name, - folder ?: "", - ep.id, - startIndex, - callback, parallelConnections = maxConcurrentConnections - ) - } else { - return downloadThing( - context, - link, - name, - folder ?: "", - "mp4", - tryResume, - ep.id, - callback, parallelConnections = maxConcurrentConnections - ) + return downloadHLS( + context, + link, + name, + folder ?: "", + ep.id, + startIndex, + callback, parallelConnections = maxConcurrentConnections + ) + } + ExtractorLinkType.VIDEO -> { + return downloadThing( + context, + link, + name, + folder ?: "", + "mp4", + tryResume, + ep.id, + callback, parallelConnections = maxConcurrentConnections + ) + } + else -> throw IllegalArgumentException("unsuported download type") } } catch (t: Throwable) { return DOWNLOAD_FAILED From cbebafb7b9271780c29b208a60637d7d0675a3a9 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Mon, 4 Sep 2023 18:36:30 +0000 Subject: [PATCH 125/156] Update sdk version --- app/build.gradle.kts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 55d0f7ae..825d0c4b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,13 +50,13 @@ android { } } - compileSdk = 33 - buildToolsVersion = "30.0.3" + compileSdk = 34 + buildToolsVersion = "34.0.0" defaultConfig { applicationId = "com.lagradost.cloudstream3" minSdk = 21 - targetSdk = 29 + targetSdk = 34 versionCode = 59 versionName = "4.1.8" From 3c3ca21728a9ee094296594f8fa1c3a435a9176a Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Mon, 4 Sep 2023 18:42:33 +0000 Subject: [PATCH 126/156] Let's not be too radical --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 825d0c4b..e31de078 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,13 +50,13 @@ android { } } - compileSdk = 34 + compileSdk = 33 buildToolsVersion = "34.0.0" defaultConfig { applicationId = "com.lagradost.cloudstream3" minSdk = 21 - targetSdk = 34 + targetSdk = 33 versionCode = 59 versionName = "4.1.8" From 0839775172db1f55b4703d189e1aa7e8f3aed3f3 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Mon, 4 Sep 2023 20:36:36 +0000 Subject: [PATCH 127/156] Upgrade SDK (#590) * Update sdk version * Let's not be too radical --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 55d0f7ae..e31de078 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -51,12 +51,12 @@ android { } compileSdk = 33 - buildToolsVersion = "30.0.3" + buildToolsVersion = "34.0.0" defaultConfig { applicationId = "com.lagradost.cloudstream3" minSdk = 21 - targetSdk = 29 + targetSdk = 33 versionCode = 59 versionName = "4.1.8" From 3fe247fb193ada9bbb7ff391d38e02a24630c1e0 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 6 Sep 2023 20:53:43 +0200 Subject: [PATCH 128/156] added drm player support --- .../cloudstream3/ui/player/CS3IPlayer.kt | 87 ++++++++++++++++--- .../cloudstream3/utils/ExtractorApi.kt | 55 ++++++++++++ 2 files changed, 132 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index fd1da5ca..c779943b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.player +import android.annotation.SuppressLint import android.content.Context import android.net.Uri import android.os.Handler @@ -7,6 +8,7 @@ import android.os.Looper import android.util.Log import android.util.Rational import android.widget.FrameLayout +import androidx.media3.common.C import androidx.media3.common.C.* import androidx.media3.common.Format import androidx.media3.common.MediaItem @@ -31,6 +33,10 @@ import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.SeekParameters +import androidx.media3.exoplayer.dash.DashMediaSource +import androidx.media3.exoplayer.drm.DefaultDrmSessionManager +import androidx.media3.exoplayer.drm.FrameworkMediaDrm +import androidx.media3.exoplayer.drm.LocalMediaDrmCallback import androidx.media3.exoplayer.source.ClippingMediaSource import androidx.media3.exoplayer.source.ConcatenatingMediaSource import androidx.media3.exoplayer.source.DefaultMediaSourceFactory @@ -50,6 +56,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.DrmExtractorLink import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList @@ -58,6 +65,7 @@ import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File import java.lang.IllegalArgumentException +import java.util.UUID import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.SSLSession @@ -104,7 +112,16 @@ class CS3IPlayer : IPlayer { * */ data class MediaItemSlice( val mediaItem: MediaItem, - val durationUs: Long + val durationUs: Long, + val drm: DrmMetadata? = null + ) + + data class DrmMetadata( + val kid: String, + val key: String, + val uuid: UUID, + val kty: String, + val keyRequestParameters: HashMap, ) override fun getDuration(): Long? = exoPlayer?.duration @@ -340,6 +357,7 @@ class CS3IPlayer : IPlayer { }.flatten() } + @SuppressLint("UnsafeOptInUsageError") private fun Tracks.Group.getFormats(): List> { return (0 until this.mediaTrackGroup.length).mapNotNull { i -> if (this.isSupported) @@ -368,6 +386,7 @@ class CS3IPlayer : IPlayer { ) } + @SuppressLint("UnsafeOptInUsageError") override fun getVideoTracks(): CurrentTracks { val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList() val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO } @@ -387,6 +406,7 @@ class CS3IPlayer : IPlayer { /** * @return True if the player should be reloaded * */ + @SuppressLint("UnsafeOptInUsageError") override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean { Log.i(TAG, "setPreferredSubtitles init $subtitle") currentSubtitles = subtitle @@ -465,6 +485,7 @@ class CS3IPlayer : IPlayer { } } + @SuppressLint("UnsafeOptInUsageError") override fun getAspectRatio(): Rational? { return exoPlayer?.videoFormat?.let { format -> Rational(format.width, format.height) @@ -475,6 +496,7 @@ class CS3IPlayer : IPlayer { subtitleHelper.setSubStyle(style) } + @SuppressLint("UnsafeOptInUsageError") override fun saveData() { Log.i(TAG, "saveData") updatedTime() @@ -548,6 +570,7 @@ class CS3IPlayer : IPlayer { var requestSubtitleUpdate: (() -> Unit)? = null + @SuppressLint("UnsafeOptInUsageError") private fun createOnlineSource(headers: Map): HttpDataSource.Factory { val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT) return source.apply { @@ -555,6 +578,7 @@ class CS3IPlayer : IPlayer { } } + @SuppressLint("UnsafeOptInUsageError") private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory { val provider = getApiFromNameNull(link.source) val interceptor = provider?.getVideoInterceptor(link) @@ -632,6 +656,7 @@ class CS3IPlayer : IPlayer { return Pair(subSources, activeSubtitles) }*/ + @SuppressLint("UnsafeOptInUsageError") private fun getCache(context: Context, cacheSize: Long): SimpleCache? { return try { val databaseProvider = StandaloneDatabaseProvider(context) @@ -663,6 +688,7 @@ class CS3IPlayer : IPlayer { return getMediaItemBuilder(mimeType).setUri(url).build() } + @SuppressLint("UnsafeOptInUsageError") private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector { val trackSelector = DefaultTrackSelector(context) trackSelector.parameters = trackSelector.buildUponParameters() @@ -676,6 +702,7 @@ class CS3IPlayer : IPlayer { var currentTextRenderer: CustomTextRenderer? = null + @SuppressLint("UnsafeOptInUsageError") private fun buildExoPlayer( context: Context, mediaItemSlices: List, @@ -760,15 +787,33 @@ class CS3IPlayer : IPlayer { // If there is only one item then treat it as normal, if multiple: concatenate the items. val videoMediaSource = if (mediaItemSlices.size == 1) { - factory.createMediaSource(mediaItemSlices.first().mediaItem) + val item = mediaItemSlices.first() + + item.drm?.let { drm -> + val drmCallback = + LocalMediaDrmCallback("{\"keys\":[{\"kty\":\"${drm.kty}\",\"k\":\"${drm.key}\",\"kid\":\"${drm.kid}\"}],\"type\":\"temporary\"}".toByteArray()) + val manager = DefaultDrmSessionManager.Builder() + .setPlayClearSamplesWithoutKeys(true) + .setMultiSession(false) + .setKeyRequestParameters(drm.keyRequestParameters) + .setUuidAndExoMediaDrmProvider(drm.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER) + .build(drmCallback) + val manifestDataSourceFactory = DefaultHttpDataSource.Factory() + + DashMediaSource.Factory(manifestDataSourceFactory) + .setDrmSessionManagerProvider { manager } + .createMediaSource(item.mediaItem) + } ?: run { + factory.createMediaSource(item.mediaItem) + } } else { val source = ConcatenatingMediaSource() - mediaItemSlices.map { + mediaItemSlices.map { item -> source.addMediaSource( // The duration MUST be known for it to work properly, see https://github.com/google/ExoPlayer/issues/4727 ClippingMediaSource( - factory.createMediaSource(it.mediaItem), - it.durationUs + factory.createMediaSource(item.mediaItem), + item.durationUs ) ) } @@ -1105,6 +1150,8 @@ class CS3IPlayer : IPlayer { } private var lastTimeStamps: List = emptyList() + + @SuppressLint("UnsafeOptInUsageError") override fun addTimeStamps(timeStamps: List) { lastTimeStamps = timeStamps timeStamps.forEach { timestamp -> @@ -1122,6 +1169,7 @@ class CS3IPlayer : IPlayer { updatedTime() } + @SuppressLint("UnsafeOptInUsageError") fun onRenderFirst() { if (hasUsedFirstRender) { // this insures that we only call this once per player load return @@ -1188,6 +1236,7 @@ class CS3IPlayer : IPlayer { } } + @SuppressLint("UnsafeOptInUsageError") private fun getSubSources( onlineSourceFactory: HttpDataSource.Factory?, offlineSourceFactory: DataSource.Factory?, @@ -1243,6 +1292,7 @@ class CS3IPlayer : IPlayer { return exoPlayer != null } + @SuppressLint("UnsafeOptInUsageError") private fun loadOnlinePlayer(context: Context, link: ExtractorLink) { Log.i(TAG, "loadOnlinePlayer $link") try { @@ -1259,7 +1309,7 @@ class CS3IPlayer : IPlayer { HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) } - val mime = when(link.type) { + val mime = when (link.type) { ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8 ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4 @@ -1267,12 +1317,29 @@ class CS3IPlayer : IPlayer { ExtractorLinkType.MAGNET -> throw IllegalArgumentException("No magnet support") } - val mediaItems = if (link is ExtractorLinkPlayList) { - link.playlist.map { + + val mediaItems = when (link) { + is ExtractorLinkPlayList -> link.playlist.map { MediaItemSlice(getMediaItem(mime, it.url), it.durationUs) } - } else { - listOf( + + is DrmExtractorLink -> { + listOf( + // Single sliced list with unset length + MediaItemSlice( + getMediaItem(mime, link.url), Long.MIN_VALUE, + drm = DrmMetadata( + kid = link.kid, + key = link.key, + uuid = link.uuid ?: C.CLEARKEY_UUID, + kty = link.kty, + keyRequestParameters = link.keyRequestParameters + ) + ) + ) + } + + else -> listOf( // Single sliced list with unset length MediaItemSlice(getMediaItem(mime, link.url), Long.MIN_VALUE) ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 2a539f0d..85e88819 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -8,6 +8,7 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay import org.jsoup.Jsoup import java.net.URL +import java.util.UUID import kotlin.collections.MutableList /** @@ -99,6 +100,60 @@ private fun inferTypeFromUrl(url: String): ExtractorLinkType { } } val INFER_TYPE : ExtractorLinkType? = null + +open class DrmExtractorLink private constructor( + override val source: String, + override val name: String, + override val url: String, + override val referer: String, + override val quality: Int, + override val headers: Map = mapOf(), + /** Used for getExtractorVerifierJob() */ + override val extractorData: String? = null, + override val type: ExtractorLinkType, + open val kid : String, + open val key : String, + /** if null then it uses the UUID for the ClearKey DRM scheme */ + open val uuid : UUID?, + open val kty : String, + + open val keyRequestParameters : HashMap +) : ExtractorLink( + source, name, url, referer, quality, type, headers, extractorData +) { + constructor( + source: String, + name: String, + url: String, + referer: String, + quality: Int, + /** the type of the media, use INFER_TYPE if you want to auto infer the type from the url */ + type: ExtractorLinkType?, + headers: Map = mapOf(), + /** Used for getExtractorVerifierJob() */ + extractorData: String? = null, + kid : String, + key : String, + uuid : UUID? = null, + kty : String = "oct", + keyRequestParameters : HashMap = hashMapOf(), + ) : this( + source = source, + name = name, + url = url, + referer = referer, + quality = quality, + headers = headers, + extractorData = extractorData, + type = type ?: inferTypeFromUrl(url), + kid = kid, + key = key, + uuid = uuid, + keyRequestParameters = keyRequestParameters, + kty = kty, + ) +} + open class ExtractorLink constructor( open val source: String, open val name: String, From 49731cd6995dc2b784fef17554faeb90a1b895c0 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 6 Sep 2023 22:42:22 +0200 Subject: [PATCH 129/156] changed drm API a bit --- .../cloudstream3/ui/player/CS3IPlayer.kt | 2 +- .../cloudstream3/utils/ExtractorApi.kt | 225 +++++++++++++++++- 2 files changed, 220 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index c779943b..4a88a2e7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -1331,7 +1331,7 @@ class CS3IPlayer : IPlayer { drm = DrmMetadata( kid = link.kid, key = link.key, - uuid = link.uuid ?: C.CLEARKEY_UUID, + uuid = link.uuid, kty = link.kty, keyRequestParameters = link.keyRequestParameters ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 85e88819..0a926374 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -1,15 +1,204 @@ package com.lagradost.cloudstream3.utils import android.net.Uri -import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.USER_AGENT +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.extractors.AStreamHub +import com.lagradost.cloudstream3.extractors.Acefile +import com.lagradost.cloudstream3.extractors.Ahvsh +import com.lagradost.cloudstream3.extractors.Aico +import com.lagradost.cloudstream3.extractors.AsianLoad +import com.lagradost.cloudstream3.extractors.Bestx +import com.lagradost.cloudstream3.extractors.Blogger +import com.lagradost.cloudstream3.extractors.BullStream +import com.lagradost.cloudstream3.extractors.ByteShare +import com.lagradost.cloudstream3.extractors.Cda +import com.lagradost.cloudstream3.extractors.Cdnplayer +import com.lagradost.cloudstream3.extractors.Chillx +import com.lagradost.cloudstream3.extractors.CineGrabber +import com.lagradost.cloudstream3.extractors.Cinestart +import com.lagradost.cloudstream3.extractors.DBfilm +import com.lagradost.cloudstream3.extractors.Dailymotion +import com.lagradost.cloudstream3.extractors.DatabaseGdrive +import com.lagradost.cloudstream3.extractors.DatabaseGdrive2 +import com.lagradost.cloudstream3.extractors.DesuArcg +import com.lagradost.cloudstream3.extractors.DesuDrive +import com.lagradost.cloudstream3.extractors.DesuOdchan +import com.lagradost.cloudstream3.extractors.DesuOdvip +import com.lagradost.cloudstream3.extractors.Dokicloud +import com.lagradost.cloudstream3.extractors.DoodCxExtractor +import com.lagradost.cloudstream3.extractors.DoodLaExtractor +import com.lagradost.cloudstream3.extractors.DoodPmExtractor +import com.lagradost.cloudstream3.extractors.DoodShExtractor +import com.lagradost.cloudstream3.extractors.DoodSoExtractor +import com.lagradost.cloudstream3.extractors.DoodToExtractor +import com.lagradost.cloudstream3.extractors.DoodWatchExtractor +import com.lagradost.cloudstream3.extractors.DoodWfExtractor +import com.lagradost.cloudstream3.extractors.DoodWsExtractor +import com.lagradost.cloudstream3.extractors.DoodYtExtractor +import com.lagradost.cloudstream3.extractors.Dooood +import com.lagradost.cloudstream3.extractors.Embedgram +import com.lagradost.cloudstream3.extractors.Evoload +import com.lagradost.cloudstream3.extractors.Evoload1 +import com.lagradost.cloudstream3.extractors.FEmbed +import com.lagradost.cloudstream3.extractors.FEnet +import com.lagradost.cloudstream3.extractors.Fastream +import com.lagradost.cloudstream3.extractors.FeHD +import com.lagradost.cloudstream3.extractors.Fembed9hd +import com.lagradost.cloudstream3.extractors.FileMoon +import com.lagradost.cloudstream3.extractors.FileMoonIn +import com.lagradost.cloudstream3.extractors.FileMoonSx +import com.lagradost.cloudstream3.extractors.Filesim +import com.lagradost.cloudstream3.extractors.Fplayer +import com.lagradost.cloudstream3.extractors.GMPlayer +import com.lagradost.cloudstream3.extractors.Gdriveplayer +import com.lagradost.cloudstream3.extractors.Gdriveplayerapi +import com.lagradost.cloudstream3.extractors.Gdriveplayerapp +import com.lagradost.cloudstream3.extractors.Gdriveplayerbiz +import com.lagradost.cloudstream3.extractors.Gdriveplayerco +import com.lagradost.cloudstream3.extractors.Gdriveplayerfun +import com.lagradost.cloudstream3.extractors.Gdriveplayerio +import com.lagradost.cloudstream3.extractors.Gdriveplayerme +import com.lagradost.cloudstream3.extractors.Gdriveplayerorg +import com.lagradost.cloudstream3.extractors.Gdriveplayerus +import com.lagradost.cloudstream3.extractors.Gofile +import com.lagradost.cloudstream3.extractors.GuardareStream +import com.lagradost.cloudstream3.extractors.Guccihide +import com.lagradost.cloudstream3.extractors.Hxfile +import com.lagradost.cloudstream3.extractors.JWPlayer +import com.lagradost.cloudstream3.extractors.Jawcloud +import com.lagradost.cloudstream3.extractors.Jeniusplay +import com.lagradost.cloudstream3.extractors.Keephealth +import com.lagradost.cloudstream3.extractors.KotakAnimeid +import com.lagradost.cloudstream3.extractors.Kotakajair +import com.lagradost.cloudstream3.extractors.Krakenfiles +import com.lagradost.cloudstream3.extractors.LayarKaca +import com.lagradost.cloudstream3.extractors.Linkbox +import com.lagradost.cloudstream3.extractors.Luxubu +import com.lagradost.cloudstream3.extractors.Lvturbo +import com.lagradost.cloudstream3.extractors.Maxstream +import com.lagradost.cloudstream3.extractors.Mcloud +import com.lagradost.cloudstream3.extractors.Megacloud +import com.lagradost.cloudstream3.extractors.Meownime +import com.lagradost.cloudstream3.extractors.MixDrop +import com.lagradost.cloudstream3.extractors.MixDropBz +import com.lagradost.cloudstream3.extractors.MixDropCh +import com.lagradost.cloudstream3.extractors.MixDropTo +import com.lagradost.cloudstream3.extractors.Movhide +import com.lagradost.cloudstream3.extractors.Moviehab +import com.lagradost.cloudstream3.extractors.MoviehabNet +import com.lagradost.cloudstream3.extractors.Moviesapi +import com.lagradost.cloudstream3.extractors.Moviesm4u +import com.lagradost.cloudstream3.extractors.Mp4Upload +import com.lagradost.cloudstream3.extractors.Mvidoo +import com.lagradost.cloudstream3.extractors.MwvnVizcloudInfo +import com.lagradost.cloudstream3.extractors.Neonime7n +import com.lagradost.cloudstream3.extractors.Neonime8n +import com.lagradost.cloudstream3.extractors.OkRu +import com.lagradost.cloudstream3.extractors.OkRuHttps +import com.lagradost.cloudstream3.extractors.Okrulink +import com.lagradost.cloudstream3.extractors.Pixeldrain +import com.lagradost.cloudstream3.extractors.PlayLtXyz +import com.lagradost.cloudstream3.extractors.PlayerVoxzer +import com.lagradost.cloudstream3.extractors.Rabbitstream +import com.lagradost.cloudstream3.extractors.Rasacintaku +import com.lagradost.cloudstream3.extractors.SBfull +import com.lagradost.cloudstream3.extractors.Sbasian +import com.lagradost.cloudstream3.extractors.Sbface +import com.lagradost.cloudstream3.extractors.Sbflix +import com.lagradost.cloudstream3.extractors.Sblona +import com.lagradost.cloudstream3.extractors.Sblongvu +import com.lagradost.cloudstream3.extractors.Sbnet +import com.lagradost.cloudstream3.extractors.Sbrapid +import com.lagradost.cloudstream3.extractors.Sbsonic +import com.lagradost.cloudstream3.extractors.Sbspeed +import com.lagradost.cloudstream3.extractors.Sbthe +import com.lagradost.cloudstream3.extractors.Sendvid +import com.lagradost.cloudstream3.extractors.ShaveTape +import com.lagradost.cloudstream3.extractors.Solidfiles +import com.lagradost.cloudstream3.extractors.SpeedoStream +import com.lagradost.cloudstream3.extractors.SpeedoStream1 +import com.lagradost.cloudstream3.extractors.SpeedoStream2 +import com.lagradost.cloudstream3.extractors.Ssbstream +import com.lagradost.cloudstream3.extractors.StreamM4u +import com.lagradost.cloudstream3.extractors.StreamSB +import com.lagradost.cloudstream3.extractors.StreamSB1 +import com.lagradost.cloudstream3.extractors.StreamSB10 +import com.lagradost.cloudstream3.extractors.StreamSB11 +import com.lagradost.cloudstream3.extractors.StreamSB2 +import com.lagradost.cloudstream3.extractors.StreamSB3 +import com.lagradost.cloudstream3.extractors.StreamSB4 +import com.lagradost.cloudstream3.extractors.StreamSB5 +import com.lagradost.cloudstream3.extractors.StreamSB6 +import com.lagradost.cloudstream3.extractors.StreamSB7 +import com.lagradost.cloudstream3.extractors.StreamSB8 +import com.lagradost.cloudstream3.extractors.StreamSB9 +import com.lagradost.cloudstream3.extractors.StreamTape +import com.lagradost.cloudstream3.extractors.StreamTapeNet +import com.lagradost.cloudstream3.extractors.StreamhideCom +import com.lagradost.cloudstream3.extractors.StreamhideTo +import com.lagradost.cloudstream3.extractors.Streamhub2 +import com.lagradost.cloudstream3.extractors.Streamlare +import com.lagradost.cloudstream3.extractors.StreamoUpload +import com.lagradost.cloudstream3.extractors.Streamplay +import com.lagradost.cloudstream3.extractors.Streamsss +import com.lagradost.cloudstream3.extractors.Supervideo +import com.lagradost.cloudstream3.extractors.Tantifilm +import com.lagradost.cloudstream3.extractors.Tomatomatela +import com.lagradost.cloudstream3.extractors.TomatomatelalClub +import com.lagradost.cloudstream3.extractors.Tubeless +import com.lagradost.cloudstream3.extractors.Upstream +import com.lagradost.cloudstream3.extractors.UpstreamExtractor +import com.lagradost.cloudstream3.extractors.Uqload +import com.lagradost.cloudstream3.extractors.Uqload1 +import com.lagradost.cloudstream3.extractors.Uqload2 +import com.lagradost.cloudstream3.extractors.Userload +import com.lagradost.cloudstream3.extractors.Userscloud +import com.lagradost.cloudstream3.extractors.Uservideo +import com.lagradost.cloudstream3.extractors.Vanfem +import com.lagradost.cloudstream3.extractors.Vicloud +import com.lagradost.cloudstream3.extractors.VidSrcExtractor +import com.lagradost.cloudstream3.extractors.VidSrcExtractor2 +import com.lagradost.cloudstream3.extractors.VideoVard +import com.lagradost.cloudstream3.extractors.VideovardSX +import com.lagradost.cloudstream3.extractors.Vidgomunime +import com.lagradost.cloudstream3.extractors.Vidgomunimesb +import com.lagradost.cloudstream3.extractors.Vidmoly +import com.lagradost.cloudstream3.extractors.Vidmolyme +import com.lagradost.cloudstream3.extractors.Vido +import com.lagradost.cloudstream3.extractors.Vidstreamz +import com.lagradost.cloudstream3.extractors.Vizcloud +import com.lagradost.cloudstream3.extractors.Vizcloud2 +import com.lagradost.cloudstream3.extractors.VizcloudCloud +import com.lagradost.cloudstream3.extractors.VizcloudDigital +import com.lagradost.cloudstream3.extractors.VizcloudInfo +import com.lagradost.cloudstream3.extractors.VizcloudLive +import com.lagradost.cloudstream3.extractors.VizcloudOnline +import com.lagradost.cloudstream3.extractors.VizcloudSite +import com.lagradost.cloudstream3.extractors.VizcloudXyz +import com.lagradost.cloudstream3.extractors.Voe +import com.lagradost.cloudstream3.extractors.Watchx +import com.lagradost.cloudstream3.extractors.WcoStream +import com.lagradost.cloudstream3.extractors.Wibufile +import com.lagradost.cloudstream3.extractors.XStreamCdn +import com.lagradost.cloudstream3.extractors.YourUpload +import com.lagradost.cloudstream3.extractors.YoutubeExtractor +import com.lagradost.cloudstream3.extractors.YoutubeMobileExtractor +import com.lagradost.cloudstream3.extractors.YoutubeNoCookieExtractor +import com.lagradost.cloudstream3.extractors.YoutubeShortLinkExtractor +import com.lagradost.cloudstream3.extractors.Yufiles +import com.lagradost.cloudstream3.extractors.Zorofile +import com.lagradost.cloudstream3.extractors.Zplayer +import com.lagradost.cloudstream3.extractors.ZplayerV2 +import com.lagradost.cloudstream3.extractors.Ztreamhub import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.extractors.* import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay import org.jsoup.Jsoup import java.net.URL import java.util.UUID -import kotlin.collections.MutableList /** * For use in the ConcatenatingMediaSource. @@ -101,6 +290,31 @@ private fun inferTypeFromUrl(url: String): ExtractorLinkType { } val INFER_TYPE : ExtractorLinkType? = null +/** + * UUID for the ClearKey DRM scheme. + * + * + * ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up. + */ +val CLEARKEY_UUID = UUID(-0x1d8e62a7567a4c37L, 0x781AB030AF78D30EL) + +/** + * UUID for the Widevine DRM scheme. + * + * + * Widevine is supported on Android devices running Android 4.3 (API Level 18) and up. + */ +val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L) + +/** + * UUID for the PlayReady DRM scheme. + * + * + * PlayReady is supported on all AndroidTV devices. Note that most other Android devices do not + * provide PlayReady support. + */ +val PLAYREADY_UUID = UUID(-0x65fb0f8667bfbd7aL, -0x546d19a41f77a06bL) + open class DrmExtractorLink private constructor( override val source: String, override val name: String, @@ -113,8 +327,7 @@ open class DrmExtractorLink private constructor( override val type: ExtractorLinkType, open val kid : String, open val key : String, - /** if null then it uses the UUID for the ClearKey DRM scheme */ - open val uuid : UUID?, + open val uuid : UUID, open val kty : String, open val keyRequestParameters : HashMap @@ -134,7 +347,7 @@ open class DrmExtractorLink private constructor( extractorData: String? = null, kid : String, key : String, - uuid : UUID? = null, + uuid : UUID = CLEARKEY_UUID, kty : String = "oct", keyRequestParameters : HashMap = hashMapOf(), ) : this( From 4ddd78ebb6892742543b082a6395553d9642dc8e Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:30:00 +0530 Subject: [PATCH 130/156] fook jitpack (#595) --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e31de078..f52d6e5e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -250,9 +250,9 @@ dependencies { // used for subtitle decoding https://github.com/albfernandez/juniversalchardet implementation("com.github.albfernandez:juniversalchardet:2.4.0") - // newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204 + // newpipe yt taken from https://github.com/TeamNewPipe/NewPipeExtractor/commits/dev // this should be updated frequently to avoid trailer fu*kery - implementation("com.github.TeamNewPipe:NewPipeExtractor:1f08d28") + implementation("com.github.teamnewpipe:NewPipeExtractor:1f08d28") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6") // Library/extensions searching with Levenshtein distance From f05c65cf5c62964c73b9756c4f5c2dedb7cfb919 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Fri, 8 Sep 2023 10:01:11 +0200 Subject: [PATCH 131/156] Translated using Weblate (Odia) (#574) Currently translated at 39.5% (249 of 630 strings) Translated using Weblate (Odia) Currently translated at 25.0% (1 of 4 strings) Translated using Weblate (Odia) Currently translated at 38.8% (245 of 630 strings) Translated using Weblate (German) Currently translated at 99.8% (629 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Tamil) Currently translated at 20.9% (132 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Ukrainian) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (630 of 630 strings) Translated using Weblate (Croatian) Currently translated at 99.8% (629 of 630 strings) Translated using Weblate (Croatian) Currently translated at 99.3% (626 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Arabic (Najdi)) Currently translated at 54.9% (346 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Ukrainian) Currently translated at 100.0% (630 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (French) Currently translated at 100.0% (630 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Hungarian) Currently translated at 85.7% (540 of 630 strings) Translated using Weblate (Romanian) Currently translated at 95.7% (603 of 630 strings) Translated using Weblate (Arabic (Najdi)) Currently translated at 50.1% (316 of 630 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (630 of 630 strings) Translated using Weblate (Portuguese (Brazil)) Currently translated at 96.1% (606 of 630 strings) Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Translated using Weblate (Arabic (Najdi)) Currently translated at 44.9% (283 of 630 strings) Translated using Weblate (Arabic (Najdi)) Currently translated at 39.6% (250 of 630 strings) Translated using Weblate (Arabic (Najdi)) Currently translated at 39.5% (249 of 630 strings) Translated using Weblate (German) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (German) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (German) Currently translated at 99.8% (629 of 630 strings) Translated using Weblate (German) Currently translated at 99.8% (629 of 630 strings) Translated using Weblate (Arabic (Najdi)) Currently translated at 33.4% (211 of 630 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (630 of 630 strings) Translated using Weblate (German) Currently translated at 99.8% (629 of 630 strings) Translated using Weblate (German) Currently translated at 99.8% (629 of 630 strings) Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Update translation files Updated by "Remove blank strings" hook in Weblate. Translated using Weblate (Arabic (Najdi)) Currently translated at 32.0% (202 of 630 strings) Translated using Weblate (Portuguese (Brazil)) Currently translated at 91.5% (577 of 630 strings) Translated using Weblate (Arabic (Saudi Arabia)) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Arabic (Najdi)) Currently translated at 23.9% (151 of 630 strings) Added translation using Weblate (Arabic (South Levantine)) Translated using Weblate (Amharic) Currently translated at 14.9% (94 of 630 strings) Translated using Weblate (Tigrinya) Currently translated at 15.0% (95 of 630 strings) Added translation using Weblate (Amharic) Added translation using Weblate (Tigrinya) Merge remote-tracking branch 'origin/master' Translated using Weblate (Hungarian) Currently translated at 85.2% (537 of 630 strings) Translated using Weblate (Hungarian) Currently translated at 81.4% (513 of 630 strings) Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/am/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ars/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/or/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ro/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ti/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ar_SA/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/de/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/or/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/uk/ Translation: Cloudstream/App Translation: Cloudstream/Fastlane Co-authored-by: Alexandru Co-authored-by: Anarchydr Co-authored-by: Cait Martin Newnham <85128509+helloiamcait@users.noreply.github.com> Co-authored-by: Carlos Luiz Co-authored-by: Chi Uma Co-authored-by: GobinathAL Co-authored-by: Gyuri Bajzik Co-authored-by: Joel Brink Co-authored-by: Julian Co-authored-by: Mubarek Seyd Juhar Co-authored-by: Sam Cooper Co-authored-by: Skrripy Co-authored-by: Subham Jena Co-authored-by: mbottari Co-authored-by: pedrolinharesmoreira Co-authored-by: tabtomi8 --- app/src/main/res/values-ars/strings.xml | 67 +++++++++++++- app/src/main/res/values-bp/strings.xml | 43 ++++++--- app/src/main/res/values-de/strings.xml | 92 +++++++++---------- app/src/main/res/values-fr/strings.xml | 6 +- app/src/main/res/values-hr/strings.xml | 13 ++- app/src/main/res/values-hu/strings.xml | 7 +- app/src/main/res/values-or/strings.xml | 7 +- app/src/main/res/values-ro/strings.xml | 3 +- app/src/main/res/values-ta/strings.xml | 26 ++++-- app/src/main/res/values-uk/strings.xml | 38 ++++---- fastlane/metadata/android/or/changelogs/2.txt | 1 + fastlane/metadata/android/uk/changelogs/2.txt | 2 +- 12 files changed, 209 insertions(+), 96 deletions(-) create mode 100644 fastlane/metadata/android/or/changelogs/2.txt diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index ea8aa05c..5135c97e 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -200,4 +200,69 @@ استخدم %sتعذر إنشاء واجهة المستخدم بشكل صحيح، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور الصفات - + نوع الحافة + العب + حدث خطأ أثناء تحميل الروابط + التخزين الداخلي + الترجمة + استئناف تحميل + معلومات + وقفة التحميل + الغي + احفظ + إعدادات الترجمة + لون الخط + لون المخطط التفصيلي + اقفل + امسح + سرعة اللاعب + لون الخلفية + لون النافذة + ارتفاع الترجمة + حذف ملف + تعطيل الإبلاغ التلقائي عن الأخطاء + بدأ التحديث + انسخ + بث + ملف اللعب + مزيد من المعلومات + تصفية الإشارات المرجعية + إشارات مرجعية + زيل + ضبط حالة المشاهدة + مدبلجة + اخفي + قدم + وصف + يستمر التشغيل في مشغل مصغر فوق التطبيقات الأخرى + نهائيا %sسيؤدي هذا الى حذف +\nهل أنت متأكد؟ + الخط + حجم الخط + زيل + هذا المزود عبارة عن تورنت، ويوصى باستخدام فيبيان + لا يتم توفير البيانات الوصفية بواسطة الموقع، وسيفشل تحميل الفيديو إذا لم يكن موجودًا في الموقع. + جاري التنفيذ + مكتمل + حالة + التحديد التلقائي للغة + زر تغيير حجم المشغل + مواصلة المشاهدة + مزيد من المعلومات + البحث باستخدام مقدمي الخدمات + البحث باستخدام الأنواع + بنيني الى المطورين %d تم منح + لم يتم تقديم بنيني + تحميل اللغات + لغة الترجمة + اضغط لإعادة التعيين إلى الوضع الافتراضي + %s قم باستيراد الخطوط بوضعها في + قد تكون هناك حاجة إلى فيبيان حتى يعمل هذا المزود بشكل صحيح + لم يتم العثور على قطعة أرض + لم يتم العثور على وصف + 🐈عرض لوجكات + سجل + صور في صور + %d +\nباقي + \ No newline at end of file diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index b70eec12..daa352a7 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -157,7 +157,7 @@ Mostrar episódios de Filler em anime Mostrar trailers Mostrar posters do Kitsu - Esconder qualidades de vídeo selecionadas nos resultados da Pesquisa + Esconder qualidades de vídeo selecionadas nos resultados da pesquisa Atualizações de plugin automáticas Mostrar atualizações do app Automaticamente procurar por novas atualizações ao abrir @@ -222,7 +222,7 @@ Filme Série Desenho Animado - @string/anime + Anime @string/ova Torrent Documentário @@ -265,14 +265,14 @@ Cache do vídeo em disco Limpar cache de vídeo e imagem Causará travamentos aleatórios se definido muito alto. Não mude caso tiver pouca memória RAM, como um Android TV ou um telefone antigo - Pode causar problemas em sistemas com pouco espaço de armazenamento se definido muito alto, como em dispositivos Android TV + Causa problemas em sistemas com pouco espaço de armazenamento se definido muito alto, como em dispositivos Android TV. DNS sobre HTTPS Útil para burlar bloqueios de provedores de internet Clonar site Remover site Adiciona um clone de um site existente, com uma URL diferente Caminho para Download - Url do servidor Nginx + URL do servidor NGINX Mostrar Anime Dublado/Legendado Ajustar para a Tela Esticar @@ -338,7 +338,7 @@ Sombreado Em Relevo Sincronizar legendas - 1000ms + 1000 ms Atraso de legenda Use isto se as legendas forem mostradas %dms adiantadas Use isto se as legendas forem mostradas %dms atrasadas @@ -382,9 +382,9 @@ Resolução e título Título Resolução - Id invalida + ID inválido Dado invalido - URL invalido + URL inválida Erro Remover legendas ocultas(CC) das legendas Remover bloat das legendas @@ -406,8 +406,8 @@ Plugin Carregado Plugin Apagado Falha ao carregar %s - Iniciada a transferência %d %s - Transferido %d %s com sucesso + Iniciada a transferência %d %s… + Transferido %d %s Tudo %s já transferido Transferência em batch Plugin @@ -444,7 +444,7 @@ Navegador Copia de Segurança A Barra de Progresso pode ser usada quando o player estiver oculto - Inscrever + Inscrito Essa lista está vazia. Tente mudar para outra. Reproduzir Livestream Log do Teste @@ -493,10 +493,10 @@ \nNOTA: Se a soma for 10 ou mais, o Player pulará automaticamente o carregamento quando o link for carregado! Arquivo de modo de segurança encontrado! \nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido. - Inscrevel em %d + Inscrito em %s Episódio %d Lançado Selecionar padrão - Disinscrevel em %d + Desinscrito de %s Alguns aparelhos não possuem suporte para este pacote de instalação. Tente a opção legada se a atualização não instalar. Dados móveis Perfil %d @@ -550,4 +550,21 @@ Faixas de áudio Adicionado em (novo para antigo) Faixas de video - + Legendas + Navegador + 18+ + Links + Funcionalidades do Player + Instalador APK + Aparência + Desativar + Usar + Link da stream + Gestos + Plugin baixado + Não foi possível se conectar ao GitHub. Ativando proxy jsDelivr… + Cache + Vídeo + Android TV + Wi-Fi + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6739465a..233e38e4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -5,19 +5,19 @@ Episode %d wird veröffentlicht in Vorschaubild Vorschaubild - Halten, um auf die Standardeinstellungen zurückzusetzen + Halten, um auf Standardeinstellungen zurückzusetzen Wiederherstellung der Daten aus der Datei %s fehlgeschlagen Daten erfolgreich gesichert Fehler beim Sichern von %s Dieser Anbieter hat keine Chromecast-Unterstützung Chromecast-Mirror In App wiedergeben - Vermischte Openings + Gemischte Openings Abspann Intro Verlauf löschen Verlauf - Überspringen Knopf für Openings/Endings anzeigen + Button zum Überspringen für Openings/Endings anzeigen Zu viel Text. Kann nicht in der Zwischenablage gespeichert werden. Episodenvorschaubild Medienvorschaubild @@ -34,7 +34,7 @@ CloudStream Mit CloudStream abspielen Startseite - Suchen + Suche Downloads Einstellungen Suchen… @@ -44,8 +44,8 @@ Nächste Episode Genres Teilen - In Browser öffnen - Puffern überspringen + Im Browser öffnen + Laden überspringen Lädt… Am schauen Pausiert @@ -79,7 +79,7 @@ Datei abspielen Download fortsetzen Download pausieren - Automatische Fehlerberichterstattung deaktivieren + Automatische Fehlerberichtserstattung deaktivieren Mehr Infos Verstecken Abspielen @@ -106,8 +106,8 @@ Schriftgröße Suche anhand Anbietern Suche anhand Typen - %d Benenes an die Devs verteilt - Noch keine Benenes verteilt + %d Benenes an die Devs geschenkt + Noch keine Benenes verschenkt Sprache automatisch wählen Sprachen herunterladen Untertitelsprache @@ -117,8 +117,8 @@ Mehr Infos @string/home_play Damit dieser Anbieter korrekt funktioniert, ist möglicherweise ein VPN erforderlich - Dieser Anbieter bietet Torrents an, ein VPN wird dringend empfohlen - Metadaten werden nicht von der Website bereitgestellt, das Laden des Videos schlägt fehl, wenn sie auf der Website nicht vorhanden sind. + Dieser Anbieter bietet Torrents an, ein VPN wird deswegen dringend empfohlen + Metadaten werden nicht von der Website bereitgestellt, das Laden des Videos schlägt fehl, wenn sie nicht auf der Website vorhanden sind. Beschreibung Keine Handlung gefunden Keine Beschreibung gefunden @@ -143,7 +143,7 @@ Doppeltippen zum Pausieren Zeit für vor- und zurückspulen im Player (Sekunden) Zweimal auf die rechte oder linke Seite tippen, um vor- oder zurückzuspulen - Doppelt in die Mitte tippen, um zu pausieren + Zweimal in die Mitte tippen, um zu pausieren Systemhelligkeit verwenden Systemhelligkeit anstelle eines dunklen Overlay im Player verwenden Episodenfortschritt aktualisieren @@ -163,7 +163,7 @@ Füller-Episoden für Animes anzeigen Trailer anzeigen Vorschaubilder von Kitsu anzeigen - Ausgewählte Videoqualität bei Suchergebnissen ausblenden + Ausgewählte Videoqualität in den Suchergebnissen ausblenden Automatische Plugin-Updates App-Updates anzeigen Automatisches Suchen nach neuen Updates nach dem Start. @@ -172,11 +172,11 @@ Github Light Novel App von denselben Entwicklern Anime App von denselben Entwicklern - Discord beitreten - Eine Benene an die Devs verteilen - Verteilte Benenes + Trete dem Discord Server bei + Eine Benene an die Devs schenken + Geschenkte Benenes App-Sprache - Keine Verlinkung gefunden + Keine Links gefunden Link in die Zwischenablage kopiert Episode abspielen Auf Standardwert zurücksetzen @@ -240,7 +240,7 @@ Remote-Fehler Renderfehler Unerwarteter Playerfehler - Downloadfehler, Speicherberechtigungen prüfen + Downloadfehler, bitte überprüfen sie die Speicherberechtigungen Chromecast-Episode In %s wiedergeben In Browser wiedergeben @@ -255,7 +255,7 @@ Titel UI-Elemente auf Vorschaubild umschalten Kein Update gefunden - Auf Update prüfen + Auf Updates prüfen Sperren Skalieren Quelle @@ -270,16 +270,16 @@ Videopufferlänge Video-Cache in Speicher Video- und Bild-Cache leeren - Verursacht Abstürze, wenn zu hoch eingestellt. Nicht ändern, wenn wenig Arbeitsspeicher verfügbar ist, wie z.B. ein Android TV oder ein altes Telefon. - Kann auf Systemen mit geringem Speicherplatz, wie z. B. Android TV-Geräten, zu Problemen führen, wenn der Wert zu hoch eingestellt ist. + Verursacht Abstürze, wenn zu hoch eingestellt. Nicht ändern, wenn wenig Arbeitsspeicher verfügbar ist, wie z.B. auf einem Android TV oder auf einem alten Smartphone. + Kann auf Systemen mit geringem Speicherplatz, wie z. B. auf Android TV-Geräten, zu Problemen führen, wenn der Wert zu hoch eingestellt ist. DNS über HTTPS - Nützlich für die Umgehung von ISP-Sperren + Nützlich zur Umgehung von ISP-Sperren Website klonen Website entfernen Einen Klon einer bestehenden Website mit einer anderen URL hinzufügen Downloadpfad Nginx-Server-URL - Dubbed/Subbed Anime anzeigen (Synchronisiert/Untertitelt) + Dubbed/Subbed Anime anzeigen An Bildschirm anpassen Strecken Vergrößern @@ -308,7 +308,7 @@ 127.0.0.1 MeineCooleSeite example.com - Sprachcode (en) + Sprachencode (en) %s %s Account Ausloggen @@ -317,13 +317,13 @@ Account hinzufügen Account erstellen Synchronisation hinzufügen - Hinzugefügt %s + %s hinzugefügt Sync Bewertung %d / 10 /\?\? /%d - Authentifiziert %s + %s authentifiziert Die Authentifizierung bei %s ist fehlgeschlagen Keine Normal @@ -335,10 +335,10 @@ Schatten Erhöht Untertitel synchronisieren - 1000ms + 1000 ms Untertitelverzögerung - Verwenden, wenn die Untertitel %dms zu früh angezeigt werden - Verwenden, wenn die Untertitel %dms zu spät angezeigt werden + Verwenden, wenn die Untertitel %d ms zu früh angezeigt werden + Verwenden, wenn die Untertitel %d ms zu spät angezeigt werden Keine Untertitelverzögerung Vogel Quax zwickt Johnys Pferd Bim Empfohlen @@ -359,7 +359,7 @@ HD TS TC - BlueRay + Blue-ray WP DVD 4K @@ -408,7 +408,7 @@ Plugins Dadurch werden auch alle Repository-Plugins gelöscht Repository löschen - Lade eine Liste der Websiten herunter, welche du verwenden möchtest + Lade eine Liste der Websites herunter, welche du verwenden möchtest Heruntergeladen: %d Deaktiviert: %d Nicht heruntergeladen: %d @@ -416,7 +416,7 @@ \n \nAufgrund eines hirnlosen DMCA-Takedowns durch Sky UK Limited 🤮 können wir die Repository-Site nicht in der App verlinken. \n -\nTrete unserem Discord bei oder suche online. +\nTrete unserem Discord Server bei oder suche online. Community-Repositories anzeigen Öffentliche Liste Alle Untertitel in Großbuchstaben @@ -427,7 +427,7 @@ Videospuren Bei Neustart anwenden Abgesicherter Modus aktiviert - Alle Erweiterungen wurden aufgrund eines Absturzes deaktiviert, damit Sie diejenige finden können, die Probleme verursacht. + Alle Erweiterungen wurden aufgrund eines Absturzes deaktiviert, damit Sie diejenige finden können, welche Probleme verursacht. Absturzinfo ansehen Bewertung: %s Beschreibung @@ -460,7 +460,7 @@ Automatische Installation aller noch nicht installierten Plugins aus hinzugefügten Repositories. Einrichtungsvorgang wiederholen APK-Installer - Einige Telefone unterstützen den neuen Package-Installer nicht. Benutze die Legacy-Option, wenn sich die Updates nicht installieren lassen. + Einige Smartphones unterstützen den neuen Package-Installer nicht. Benutze die Legacy-Option, wenn sich die Updates nicht installieren lassen. %s %d%s Links App-Updates @@ -482,7 +482,7 @@ Nein App-Update wird heruntergeladen… App-Update wird installiert… - Konnte die neue Version der App nicht installieren + Die neue Version der App konnte nicht installieren werden Legacy PackageInstaller Aktualisierung gestartet @@ -493,18 +493,18 @@ Browser Sortieren nach Sortieren - Bewertung (gut bis schlecht) - Bewertung (schlecht bis gut) - Aktualisiert (neu bis alt) - Aktualisiert (alt bis neu) - Alphabetisch (A bis Z) - Alphabetisch (Z bis A) + Bewertung (gut zu schlecht) + Bewertung (schlecht zu gut) + Aktualisiert (neu zu alt) + Aktualisiert (alt zu neu) + Alphabetisch (A zu Z) + Alphabetisch (Z zu A) Bibliothek auswählen Öffnen mit Deine Bibliothek ist leer :( \nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu. - Diese Liste ist leer. Versuche zu einer anderen Liste zu wechseln. - Datei für abgesicherten Modus gefunden! + Diese Liste ist leer. Versuch zu einer anderen Liste zu wechseln. + Datei für den abgesicherten Modus gefunden! \nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird. Player ausgeblendet - Betrag zum vor- und zurückspulen Der Betrag, welcher verwendet wird, wenn der Player eingeblendet ist @@ -549,7 +549,7 @@ Filtermodus für Plugin-Downloads auswählen Es wurde bereits abgestimmt Keine Plugins im Repository gefunden - Repository nicht gefunden, überprüfe die URL und probiere eine VPN - Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein schwerwiegender Fehler und sollte sofort gemeldet werden. %s + Repository nicht gefunden, überprüf die URL und versuch ein VPN + Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein SCHWERWIEGENDER FEHLER und sollte sofort gemeldet werden. %s Deaktivieren - + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 208e6140..2849b744 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -540,7 +540,7 @@ \n \nREMARQUE : Si la somme est de 10 ou plus, le joueur sautera automatiquement le chargement lorsque ce lien est chargé ! Aucun plugin trouvé dans ce dossier - Dossier non trouvé, vérifiez l\'url et essayé un VPN + Dépôt introuvable, vérifiez l\'URL et essayez avec un VPN Données mobiles Définir par défaut Utiliser @@ -552,4 +552,6 @@ Qualités L\'interface utilisateur n\'a pas pu être créée correctement. Il s\'agit d\'un bogue majeur qui doit être signalé immédiatement %s Sélectionnez le mode pour filtrer le téléchargement des plugins - + Fond de profil + \@string/default_subtitles + \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 35df36ac..a0bf44ca 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -566,4 +566,15 @@ Pozadina profila Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s Odaberi modus za filtriranje preuzimanja dodataka - + Onemogući + \@string/default_subtitles + U repozitoriju nisu pronađeni dodaci + Repozitorij nije pronađen, provjerite URL i pokušajte koristiti VPN + Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je video prioritet. +\n +\nIzvor A: 3 +\nKvaliteta B: 7 +\nImat će kombinirani prioritet videozapisa od 10. +\n +\nNAPOMENA: Ako je zbroj 10 ili više, video player će automatski preskočiti učitavanje kada se ta poveznica učita! + \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 05a7f0a7..4c30caaf 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -219,7 +219,7 @@ Folyamatban levő Év Webhely - Szinopszis + Összegzés Nincsenek feliratok Távoli hiba Render hiba @@ -237,7 +237,7 @@ Csúsztassa felfelé vagy lefelé a bal vagy jobb oldalon a fényerő vagy a hangerő megváltoztatásához Biztonsági mentés 0 Banán a fejlesztőknek - Húzás a kereséshez + Húzd el, hogy beless Következő epizód automatikus lejátszása Következő epizód lejátszása amikor az aktuális epizód véget ér Dupla koppintás a kereséshez @@ -510,4 +510,5 @@ TV elrendezés Automatikus Az átugrás mértéke, amikor a lejátszó látható - + Válassza ki a módot a pluginek letöltésének szűréséhez + \ No newline at end of file diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 9b9385c2..e5044571 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -153,4 +153,9 @@ ଆପ୍ ଅଦ୍ୟତନ ଦେଖାଇବା ଅଦ୍ୟତନ ଆରମ୍ଭ ହୋଇଛି ସନ୍ଧାନ କରିବା… - + ସଂକ୍ଷିପ୍ତବୃତ୍ତି + ଚଳଚ୍ଚିତ୍ର ଚଲାଅ + %s ସନ୍ଧାନ କରିବା… + ପରବର୍ତ୍ତୀ ଅଧ୍ୟାୟ + କୌଣସି ତଥ୍ୟ ନାହିଁ + \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 1f288d2a..3db9accb 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -570,4 +570,5 @@ UI nu a putut fi creată corect, acesta este un BUG MAJOR și trebuie raportat imediat %s Selectați modul de filtrare a descărcării plugin-urilor @string/default_subtitles - + Ați votat deja + \ No newline at end of file diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index affb04bf..41cfa846 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -6,12 +6,12 @@ முகப்பு தேடு பதிவிறக்கம் - தகவல் எதுவும் இல்லை + தரவு இல்லை மேலும் விருப்பங்கள் - அடுத்த எபிசோட் + அடுத்த அத்தியாயம் வகைகள் பகிர் - Browser இல் திற + உலாவியில் திற ஏற்றுவதைத் தவிர் பார்த்து கொண்டிருப்பது நிறுத்தி வைக்கப்பட்டுள்ளது @@ -21,9 +21,9 @@ ஸ்ட்ரீம் டோரண்ட் வசன வரிகள் பின் செல் - எபிசோடை இயக்கு + அத்தியாயத்தை இயக்கு எபிசோட் பதிவிற்கான அனுமதி கொடுக்கவும் - பதிவிறக்கம் செய்யப்பட்டது + பதிவிறக்கப்பட்டது பதிவிறக்குகிறது பதிவிறக்கம் இடைநிறுத்தப்பட்டது பதிவிறக்கம் தொடங்கியது @@ -67,10 +67,10 @@ ஏற்றுகிறது… கைவிடப்பட்டது பதிவிறக்கம் முடிந்தது - இணைப்பை மீண்டும் முயற்சிக்கவும்… + இணைப்பை மீண்டும் முயலவும்… திரைப்படத்தை இயக்கு லைவ்ஸ்ட்ரீம் இயக்கு - டிரெய்லரை இயக்கவும் + டிரெய்லரை இயக்கு மூலம் இணைப்புகளை ஏற்றுவதில் பிழை இயக்கு @@ -107,4 +107,14 @@ இடைநிறுத்துவதற்கு இருமுறை தட்டவும் Chromecast வசன அமைப்புகள் இருண்ட மேலடுக்குக்குப் பதிலாக ஆப் பிளேயரில் சிஸ்டம் பிரகாசத்தைப் பயன்படுத்தவும் - + அத்தியாயம் %d-இன் வெளியீட்டு நேரம் + %dம %dநி + %dநி + அடுத்து ஏதாவது + உலாவி + %d நிமி + CloudStream-உடன் இயக்கு + புதிய புதுப்பிப்பு உள்ளது +\n%s->%s + நிரப்பி + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 4866ecd4..8e0dd88e 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -18,7 +18,7 @@ Попередній перегляд фону Швидкість (%.2fx) Знайдено нове оновлення! -\n%s -> %s +\n%s –> %s Пошук Завантаження %d хв @@ -37,7 +37,7 @@ Покинуто Переглянути фільм Переглянути трейлер - Трансляція через торрент + Трансляція через торент Повторити підключення… Назад Переглянути епізод @@ -75,7 +75,7 @@ Продовжити перегляд Вилучити Детальніше - Цей постачальник є торрентом, рекомендується VPN + Цей постачальник є торентом, рекомендується використовувати VPN Опис Сюжет не знайдено Опис не знайдено @@ -86,9 +86,9 @@ Субтитри Chromecast Налаштування субтитрів Chromecast Режим Eigengravy - Проведіть пальцем, щоб змінити налаштування + Проведіть, щоб змінити налаштування Проведіть вгору або вниз з лівого або правого боку, щоб змінити яскравість чи гучність - Відтворювати наступний епізод після закінчення поточного + Відтворює наступний епізод після закінчення поточного Головна CloudStream Філер @@ -130,7 +130,7 @@ Картинка в картинці Налаштування субтитрів плеєра Додає опцію керування швидкістю в плеєрі - Проведіть пальцем, щоб перемотати + Проведіть, щоб перемотати Двічі торкніться, щоб перемотати Двічі торкніться для паузи Крок перемотки (секунди) @@ -224,7 +224,7 @@ Двічі торкніться праворуч або ліворуч, щоб перемотати відео вперед або назад Використовуйте системну яскравість у плеєрі замість темної накладки Завантажено файл резервної копії - Торренти + Торенти Автоматична синхронізація прогресу поточного епізоду Відсутні дозволи на зберігання. Будь ласка, спробуйте ще раз. Показувати постери від Kitsu @@ -256,7 +256,7 @@ NSFW Фільм OVA - Торрент + Торент Мітка якості NSFW Переглянути в браузері @@ -294,9 +294,9 @@ Основний колір Тема застосунку Розташування назви постера - Розмістіть назву під постером + Розмістити назву під постером Пароль123 - Моє круте ім\'я + Моє круте ім’я hello@world.com Мій крутий сайт Код мови (uk) @@ -348,9 +348,9 @@ Роздільна здатність відеоплеєра Довжина буфера відео Очистити кеш відео та зображень - Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об\'ємом пам\'яті, наприклад Android TV. + Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об’ємом пам’яті, наприклад Android TV. Корисно для обходу блокувань провайдера - Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об\'ємом вільної пам\'яті, наприклад Android TV. + Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об’ємом вільної пам’яті, наприклад Android TV. DNS через HTTPS Шлях завантаження Додайте клон існуючого сайту, з іншою URL-адресою @@ -374,10 +374,10 @@ Оцінений Завантажити з файлу Макс. - Щастям б\'єш жук їх глицю в фон й ґедзь пріч + Щастям б’єш жук їх глицю в фон й ґедзь пріч 1000 мс - Використовуйте цей параметр, якщо субтитри з\'являються на %d мс занадто рано - Використовуйте це, якщо субтитри з\'являються із запізненням на %d мс + Використовуйте цей параметр, якщо субтитри з’являються на %d мс занадто рано + Використовуйте це, якщо субтитри з’являються із запізненням на %d мс Завантажено %s Підтримка Фон @@ -507,8 +507,8 @@ Файл безпечного режиму знайдено! \nРозширеня не завантажуються під час запуску, доки файл не буде видалено. Android TV - Плеєр сховано - обсяг перемотки - Плеєр показано - обсяг перемотки + Плеєр сховано – обсяг перемотки + Плеєр показано – обсяг перемотки Обсяг перемотки, який використовується, коли плеєр видимий Обсяг перемотки, який використовується, коли плеєр прихований Тест провалено @@ -532,7 +532,7 @@ Встановити за замовчуванням Профілі Допомога - Тут ви можете змінити порядок джерел. Якщо відео має вищий пріоритет, воно з\'явиться вище у списку джерел. Сума пріоритету джерела та пріоритету якості є пріоритетом відео. + Тут ви можете змінити порядок джерел. Якщо відео має вищий пріоритет, воно з’явиться вище у списку джерел. Сума пріоритету джерела та пріоритету якості є пріоритетом відео. \n \nДжерело A: 3 \nЯкість B: 7 @@ -553,4 +553,4 @@ Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували - + \ No newline at end of file diff --git a/fastlane/metadata/android/or/changelogs/2.txt b/fastlane/metadata/android/or/changelogs/2.txt new file mode 100644 index 00000000..e8b23e5f --- /dev/null +++ b/fastlane/metadata/android/or/changelogs/2.txt @@ -0,0 +1 @@ +- ପରିବର୍ତ୍ତନ ପୋଥି ଯୋଡ଼ାଗଲା! diff --git a/fastlane/metadata/android/uk/changelogs/2.txt b/fastlane/metadata/android/uk/changelogs/2.txt index 2c8d9f7e..97e84fa8 100644 --- a/fastlane/metadata/android/uk/changelogs/2.txt +++ b/fastlane/metadata/android/uk/changelogs/2.txt @@ -1 +1 @@ -- Додано журнал змін! +– Додано журнал змін! From 1629db2fc9c617d6bbe9617035213f3064a822ba Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Fri, 8 Sep 2023 08:01:34 +0000 Subject: [PATCH 132/156] chore(locales): fix locale issues --- app/src/main/res/values-ars/strings.xml | 2 +- app/src/main/res/values-bp/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 4 ++-- app/src/main/res/values-hr/strings.xml | 4 ++-- app/src/main/res/values-hu/strings.xml | 2 +- app/src/main/res/values-or/strings.xml | 2 +- app/src/main/res/values-ro/strings.xml | 2 +- app/src/main/res/values-ta/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index 5135c97e..a1042b7e 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -265,4 +265,4 @@ صور في صور %d \nباقي - \ No newline at end of file + diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index daa352a7..016fbe43 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -567,4 +567,4 @@ Vídeo Android TV Wi-Fi - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 233e38e4..3efc4072 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -552,4 +552,4 @@ Repository nicht gefunden, überprüf die URL und versuch ein VPN Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein SCHWERWIEGENDER FEHLER und sollte sofort gemeldet werden. %s Deaktivieren - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2849b744..63d03a6b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -553,5 +553,5 @@ L\'interface utilisateur n\'a pas pu être créée correctement. Il s\'agit d\'un bogue majeur qui doit être signalé immédiatement %s Sélectionnez le mode pour filtrer le téléchargement des plugins Fond de profil - \@string/default_subtitles - \ No newline at end of file + @string/default_subtitles + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index a0bf44ca..477ab92b 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -567,7 +567,7 @@ Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s Odaberi modus za filtriranje preuzimanja dodataka Onemogući - \@string/default_subtitles + @string/default_subtitles U repozitoriju nisu pronađeni dodaci Repozitorij nije pronađen, provjerite URL i pokušajte koristiti VPN Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je video prioritet. @@ -577,4 +577,4 @@ \nImat će kombinirani prioritet videozapisa od 10. \n \nNAPOMENA: Ako je zbroj 10 ili više, video player će automatski preskočiti učitavanje kada se ta poveznica učita! - \ No newline at end of file + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 4c30caaf..677beaf8 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -511,4 +511,4 @@ Automatikus Az átugrás mértéke, amikor a lejátszó látható Válassza ki a módot a pluginek letöltésének szűréséhez - \ No newline at end of file + diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index e5044571..177f7ea1 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -158,4 +158,4 @@ %s ସନ୍ଧାନ କରିବା… ପରବର୍ତ୍ତୀ ଅଧ୍ୟାୟ କୌଣସି ତଥ୍ୟ ନାହିଁ - \ No newline at end of file + diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 3db9accb..b6971c37 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -571,4 +571,4 @@ Selectați modul de filtrare a descărcării plugin-urilor @string/default_subtitles Ați votat deja - \ No newline at end of file + diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 41cfa846..3f4134e5 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -117,4 +117,4 @@ புதிய புதுப்பிப்பு உள்ளது \n%s->%s நிரப்பி - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 8e0dd88e..f9dccfc4 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -553,4 +553,4 @@ Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували - \ No newline at end of file + From 130cc16e258a08c1a3a2ba75e5669d9ffee6d024 Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Fri, 8 Sep 2023 22:13:04 +0000 Subject: [PATCH 133/156] Simkl API optimizations (#581) * Fix episode removal in simkl * Simkl API optimizations --- .../syncproviders/providers/SimklApi.kt | 570 ++++++++++++------ 1 file changed, 377 insertions(+), 193 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index b4a9d789..cd1df562 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -5,7 +5,9 @@ import androidx.core.net.toUri import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.AcraApplication import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -13,6 +15,7 @@ import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mapper import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.debugPrint import com.lagradost.cloudstream3.mvvm.logError @@ -33,6 +36,9 @@ import java.text.SimpleDateFormat import java.time.Instant import java.util.Date import java.util.TimeZone +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration class SimklApi(index: Int) : AccountManager(index), SyncAPI { override var name = "Simkl" @@ -59,6 +65,80 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { */ private var lastScoreTime = -1L + private object SimklCache { + private const val SIMKL_CACHE_KEY = "SIMKL_API_CACHE" + + enum class CacheTimes(val value: String) { + OneMonth("30d"), + ThirtyMinutes("30m") + } + + private class SimklCacheWrapper( + @JsonProperty("obj") val obj: T?, + @JsonProperty("validUntil") val validUntil: Long, + @JsonProperty("cacheTime") val cacheTime: Long = unixTime, + ) { + /** Returns true if cache is newer than cacheDays */ + fun isFresh(): Boolean { + return validUntil > unixTime + } + + fun remainingTime(): Duration { + val unixTime = unixTime + return if (validUntil > unixTime) { + (validUntil - unixTime).toDuration(DurationUnit.SECONDS) + } else { + Duration.ZERO + } + } + } + + fun cleanOldCache() { + getKeys(SIMKL_CACHE_KEY)?.forEach { + val isOld = AcraApplication.getKey>(it)?.isFresh() == false + if (isOld) { + removeKey(it) + } + } + } + + fun setKey(path: String, value: T, cacheTime: Duration) { + debugPrint { "Set cache: $SIMKL_CACHE_KEY/$path for ${cacheTime.inWholeDays} days or ${cacheTime.inWholeSeconds} seconds." } + setKey( + SIMKL_CACHE_KEY, + path, + // Storing as plain sting is required to make generics work. + SimklCacheWrapper(value, unixTime + cacheTime.inWholeSeconds).toJson() + ) + } + + /** + * Gets cached object, if object is not fresh returns null and removes it from cache + */ + inline fun getKey(path: String): T? { + // Required for generic otherwise "LinkedHashMap cannot be cast to MediaObject" + val type = mapper.typeFactory.constructParametricType( + SimklCacheWrapper::class.java, + T::class.java + ) + val cache = getKey(SIMKL_CACHE_KEY, path)?.let { + mapper.readValue>(it, type) + } + + return if (cache?.isFresh() == true) { + debugPrint { + "Cache hit at: $SIMKL_CACHE_KEY/$path. " + + "Remains fresh for ${cache.remainingTime().inWholeDays} days or ${cache.remainingTime().inWholeSeconds} seconds." + } + cache.obj + } else { + debugPrint { "Cache miss at: $SIMKL_CACHE_KEY/$path" } + removeKey(SIMKL_CACHE_KEY, path) + null + } + } + } + companion object { private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET @@ -210,18 +290,18 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("img") val img: String? ) { companion object { - fun convertToEpisodes(list: List?): List { + fun convertToEpisodes(list: List?): List? { return list?.map { MediaObject.Season.Episode(it.episode) - } ?: emptyList() + } } - fun convertToSeasons(list: List?): List { + fun convertToSeasons(list: List?): List? { return list?.filter { it.season != null }?.groupBy { it.season - }?.map { (season, episodes) -> - MediaObject.Season(season!!, convertToEpisodes(episodes)) - } ?: emptyList() + }?.mapNotNull { (season, episodes) -> + convertToEpisodes(episodes)?.let { MediaObject.Season(season!!, it) } + }?.ifEmpty { null } } } } @@ -235,11 +315,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("title") val title: String?, @JsonProperty("year") val year: Int?, @JsonProperty("ids") val ids: Ids?, + @JsonProperty("total_episodes") val total_episodes: Int? = null, + @JsonProperty("status") val status: String? = null, @JsonProperty("poster") val poster: String? = null, @JsonProperty("type") val type: String? = null, @JsonProperty("seasons") val seasons: List? = null, @JsonProperty("episodes") val episodes: List? = null ) { + fun hasEnded(): Boolean { + return status == "released" || status == "ended" + } + @JsonInclude(JsonInclude.Include.NON_EMPTY) data class Season( @JsonProperty("number") val number: Int, @@ -281,6 +367,194 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } } + class SimklScoreBuilder private constructor() { + data class Builder( + private var url: String? = null, + private var interceptor: Interceptor? = null, + private var ids: MediaObject.Ids? = null, + private var score: Int? = null, + private var status: Int? = null, + private var addEpisodes: Pair?, List?>? = null, + private var removeEpisodes: Pair?, List?>? = null, + ) { + fun interceptor(interceptor: Interceptor) = apply { this.interceptor = interceptor } + fun apiUrl(url: String) = apply { this.url = url } + fun ids(ids: MediaObject.Ids) = apply { this.ids = ids } + fun score(score: Int?, oldScore: Int?) = apply { + if (score != oldScore) { + this.score = score + } + } + + fun status(newStatus: Int?, oldStatus: Int?) = apply { + // Only set status if its new + if (newStatus != oldStatus) { + this.status = newStatus + } else { + this.status = null + } + } + + fun episodes( + allEpisodes: List?, + newEpisodes: Int?, + oldEpisodes: Int?, + ) = apply { + if (allEpisodes == null || newEpisodes == null) return@apply + + fun getEpisodes(rawEpisodes: List) = + if (rawEpisodes.any { it.season != null }) { + EpisodeMetadata.convertToSeasons(rawEpisodes) to null + } else { + null to EpisodeMetadata.convertToEpisodes(rawEpisodes) + } + + // Do not add episodes if there is no change + if (newEpisodes > (oldEpisodes ?: 0)) { + this.addEpisodes = getEpisodes(allEpisodes.take(newEpisodes)) + } + if ((oldEpisodes ?: 0) > newEpisodes) { + this.removeEpisodes = getEpisodes(allEpisodes.drop(newEpisodes)) + } + } + + suspend fun execute(): Boolean { + val time = getDateTime(unixTime) + + return if (this.status == SimklListStatusType.None.value) { + app.post( + "$url/sync/history/remove", + json = StatusRequest( + shows = listOf(HistoryMediaObject(ids = ids)), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } else { + val episodeRemovalResponse = removeEpisodes?.let { (seasons, episodes) -> + app.post( + "${this.url}/sync/history/remove", + json = StatusRequest( + shows = listOf( + HistoryMediaObject( + ids = ids, + seasons = seasons, + episodes = episodes + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } ?: true + + val historyResponse = + // Only post if there are episodes or score to upload + if (addEpisodes != null || score != null) { + app.post( + "${this.url}/sync/history", + json = StatusRequest( + shows = listOf( + HistoryMediaObject( + null, + null, + ids, + addEpisodes?.first, + addEpisodes?.second, + score, + score?.let { time }, + ) + ), movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } else { + true + } + + val statusResponse = status?.let { setStatus -> + val newStatus = + SimklListStatusType.values() + .firstOrNull { it.value == setStatus }?.originalName + ?: SimklListStatusType.Watching.originalName!! + + app.post( + "${this.url}/sync/add-to-list", + json = StatusRequest( + shows = listOf( + StatusMediaObject( + null, + null, + ids, + newStatus, + ) + ), movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } ?: true + + statusResponse && episodeRemovalResponse && historyResponse + } + } + } + } + + suspend fun getEpisodes( + simklId: Int?, + type: String?, + episodes: Int?, + hasEnded: Boolean? + ): Array? { + if (simklId == null) return null + + val cacheKey = "Episodes/$simklId" + val cache = SimklCache.getKey>(cacheKey) + + // Return cached result if its higher or equal the amount of episodes. + if (cache != null && cache.size >= (episodes ?: 0)) { + return cache + } + + // There is always one season in Anime -> no request necessary + if (type == "anime" && episodes != null) { + return episodes.takeIf { it > 0 }?.let { + (1..it).map { episode -> + EpisodeMetadata( + null, null, null, episode, null + ) + }.toTypedArray() + } + } + val url = when (type) { + "anime" -> "https://api.simkl.com/anime/episodes/$simklId" + "tv" -> "https://api.simkl.com/tv/episodes/$simklId" + "movie" -> return null + else -> return null + } + + debugPrint { "Requesting episodes from $url" } + return app.get(url, params = mapOf("client_id" to clientId)) + .parsedSafe>()?.also { + val cacheTime = + if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value + + // 1 Month cache + SimklCache.setKey(cacheKey, it, Duration.parse(cacheTime)) + } + } + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + class HistoryMediaObject( + @JsonProperty("title") title: String? = null, + @JsonProperty("year") year: Int? = null, + @JsonProperty("ids") ids: Ids? = null, + @JsonProperty("seasons") seasons: List? = null, + @JsonProperty("episodes") episodes: List? = null, + @JsonProperty("rating") val rating: Int? = null, + @JsonProperty("rated_at") val rated_at: String? = null, + ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) + @JsonInclude(JsonInclude.Include.NON_EMPTY) class RatingMediaObject( @JsonProperty("title") title: String?, @@ -299,15 +573,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime) ) : MediaObject(title, year, ids) - @JsonInclude(JsonInclude.Include.NON_EMPTY) - class HistoryMediaObject( - @JsonProperty("title") title: String?, - @JsonProperty("year") year: Int?, - @JsonProperty("ids") ids: Ids?, - @JsonProperty("seasons") seasons: List?, - @JsonProperty("episodes") episodes: List?, - ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) - @JsonInclude(JsonInclude.Include.NON_EMPTY) data class StatusRequest( @JsonProperty("movies") val movies: List, @@ -404,13 +669,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } data class ShowMetadata( - override val last_watched_at: String?, - override val status: String, - override val user_rating: Int?, - override val last_watched: String?, - override val watched_episodes_count: Int?, - override val total_episodes_count: Int?, - val show: Show + @JsonProperty("last_watched_at") override val last_watched_at: String?, + @JsonProperty("status") override val status: String, + @JsonProperty("user_rating") override val user_rating: Int?, + @JsonProperty("last_watched") override val last_watched: String?, + @JsonProperty("watched_episodes_count") override val watched_episodes_count: Int?, + @JsonProperty("total_episodes_count") override val total_episodes_count: Int?, + @JsonProperty("show") val show: Show ) : Metadata { override fun getIds(): Show.Ids { return this.show.ids @@ -435,23 +700,23 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } data class Show( - val title: String, - val poster: String?, - val year: Int?, - val ids: Ids, + @JsonProperty("title") val title: String, + @JsonProperty("poster") val poster: String?, + @JsonProperty("year") val year: Int?, + @JsonProperty("ids") val ids: Ids, ) { data class Ids( - val simkl: Int, - val slug: String?, - val imdb: String?, - val zap2it: String?, - val tmdb: String?, - val offen: String?, - val tvdb: String?, - val mal: String?, - val anidb: String?, - val anilist: String?, - val traktslug: String? + @JsonProperty("simkl") val simkl: Int, + @JsonProperty("slug") val slug: String?, + @JsonProperty("imdb") val imdb: String?, + @JsonProperty("zap2it") val zap2it: String?, + @JsonProperty("tmdb") val tmdb: String?, + @JsonProperty("offen") val offen: String?, + @JsonProperty("tvdb") val tvdb: String?, + @JsonProperty("mal") val mal: String?, + @JsonProperty("anidb") val anidb: String?, + @JsonProperty("anilist") val anilist: String?, + @JsonProperty("traktslug") val traktslug: String? ) { fun matchesId(database: SyncServices, id: String): Boolean { return when (database) { @@ -491,20 +756,58 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } } + /** + * Useful to get episodes on demand to prevent unnecessary requests. + */ + class SimklEpisodeConstructor( + private val simklId: Int?, + private val type: String?, + private val totalEpisodeCount: Int?, + private val hasEnded: Boolean? + ) { + suspend fun getEpisodes(): Array? { + return getEpisodes(simklId, type, totalEpisodeCount, hasEnded) + } + } + class SimklSyncStatus( override var status: Int, override var score: Int?, + val oldScore: Int?, override var watchedEpisodes: Int?, - val episodes: Array?, + val episodeConstructor: SimklEpisodeConstructor, override var isFavorite: Boolean? = null, override var maxEpisodes: Int? = null, /** Save seen episodes separately to know the change from old to new. * Required to remove seen episodes if count decreases */ val oldEpisodes: Int, + val oldStatus: String? ) : SyncAPI.AbstractSyncStatus() override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { val realIds = readIdFromString(id) + + // Key which assumes all ids are the same each time :/ + // This could be some sort of reference system to make multiple IDs + // point to the same key. + val idKey = + realIds.toList().map { "${it.first.originalName}=${it.second}" }.sorted().joinToString() + + val cachedObject = SimklCache.getKey(idKey) + val searchResult: MediaObject = cachedObject + ?: (searchByIds(realIds)?.firstOrNull()?.also { result -> + val cacheTime = + if (result.hasEnded()) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value + SimklCache.setKey(idKey, result, Duration.parse(cacheTime)) + }) ?: return null + + val episodeConstructor = SimklEpisodeConstructor( + searchResult.ids?.simkl, + searchResult.type, + searchResult.total_episodes, + searchResult.hasEnded() + ) + val foundItem = getSyncListSmart()?.let { list -> listOf(list.shows, list.anime, list.movies).flatten().firstOrNull { show -> realIds.any { (database, id) -> @@ -513,172 +816,63 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } } - // Search to get episodes - val searchResult = searchByIds(realIds)?.firstOrNull() - val episodes = getEpisodes(searchResult?.ids?.simkl, searchResult?.type) - if (foundItem != null) { return SimklSyncStatus( status = foundItem.status?.let { SimklListStatusType.fromString(it)?.value } ?: return null, score = foundItem.user_rating, watchedEpisodes = foundItem.watched_episodes_count, - maxEpisodes = foundItem.total_episodes_count, - episodes = episodes, + maxEpisodes = searchResult.total_episodes, + episodeConstructor = episodeConstructor, oldEpisodes = foundItem.watched_episodes_count ?: 0, + oldScore = foundItem.user_rating, + oldStatus = foundItem.status ) } else { - return if (searchResult != null) { - SimklSyncStatus( - status = SimklListStatusType.None.value, - score = 0, - watchedEpisodes = 0, - maxEpisodes = if (searchResult.type == "movie") 0 else null, - episodes = episodes, - oldEpisodes = 0, - ) - } else { - null - } + return SimklSyncStatus( + status = SimklListStatusType.None.value, + score = 0, + watchedEpisodes = 0, + maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes, + episodeConstructor = episodeConstructor, + oldEpisodes = 0, + oldStatus = null, + oldScore = null + ) } } override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { val parsedId = readIdFromString(id) lastScoreTime = unixTime - - if (status.status == SimklListStatusType.None.value) { - return app.post( - "$mainUrl/sync/history/remove", - json = StatusRequest( - shows = listOf( - HistoryMediaObject( - null, - null, - MediaObject.Ids.fromMap(parsedId), - emptyList(), - emptyList() - ) - ), - movies = emptyList() - ), - interceptor = interceptor - ).isSuccessful - } - - val realScore = status.score - val ratingResponseSuccess = if (realScore != null) { - // Remove rating if score is 0 - val ratingsSuffix = if (realScore == 0) "/remove" else "" - debugPrint { "Rate ${this.name} item: rating=$realScore" } - app.post( - "$mainUrl/sync/ratings$ratingsSuffix", - json = StatusRequest( - // Not possible to know if TV or Movie - shows = listOf( - RatingMediaObject( - null, - null, - MediaObject.Ids.fromMap(parsedId), - realScore - ) - ), - movies = emptyList() - ), - interceptor = interceptor - ).isSuccessful - } else { - true - } - val simklStatus = status as? SimklSyncStatus + + val builder = SimklScoreBuilder.Builder() + .apiUrl(this.mainUrl) + .score(status.score, simklStatus?.oldScore) + .status(status.status, (status as? SimklSyncStatus)?.oldStatus?.let { oldStatus -> + SimklListStatusType.values().firstOrNull { + it.originalName == oldStatus + }?.value + }) + .interceptor(interceptor) + .ids(MediaObject.Ids.fromMap(parsedId)) + + + // Get episodes only when required + val episodes = simklStatus?.episodeConstructor?.getEpisodes() + // All episodes if marked as completed val watchedEpisodes = if (status.status == SimklListStatusType.Completed.value) { - simklStatus?.episodes?.size + episodes?.size } else { status.watchedEpisodes } - // Only post episodes if available episodes and the status is correct - val episodeResponseSuccess = - if (simklStatus != null && watchedEpisodes != null && !simklStatus.episodes.isNullOrEmpty() && listOf( - SimklListStatusType.Paused.value, - SimklListStatusType.Dropped.value, - SimklListStatusType.Watching.value, - SimklListStatusType.Completed.value, - SimklListStatusType.ReWatching.value - ).contains(status.status) - ) { - suspend fun postEpisodes( - url: String, - rawEpisodes: List - ): Boolean { - val (seasons, episodes) = if (rawEpisodes.any { it.season != null }) { - EpisodeMetadata.convertToSeasons(rawEpisodes) to null - } else { - null to EpisodeMetadata.convertToEpisodes(rawEpisodes) - } - debugPrint { "Synced history using $url: seasons=${seasons?.toList()}, episodes=${episodes?.toList()}" } - return app.post( - url, - json = StatusRequest( - shows = listOf( - HistoryMediaObject( - null, - null, - MediaObject.Ids.fromMap(parsedId), - seasons, - episodes - ) - ), - movies = emptyList() - ), - interceptor = interceptor - ).isSuccessful - } + builder.episodes(episodes?.toList(), watchedEpisodes, simklStatus?.oldEpisodes) - // If episodes decrease: remove all episodes beyond watched episodes. - val removeResponse = if (simklStatus.oldEpisodes > watchedEpisodes) { - val removeEpisodes = simklStatus.episodes - .drop(watchedEpisodes) - postEpisodes("$mainUrl/sync/history/remove", removeEpisodes) - } else { - true - } - val cutEpisodes = simklStatus.episodes.take(watchedEpisodes) - val addResponse = postEpisodes("$mainUrl/sync/history/", cutEpisodes) - - removeResponse && addResponse - } else true - - val newStatus = - SimklListStatusType.values().firstOrNull { it.value == status.status }?.originalName - ?: SimklListStatusType.Watching.originalName - - val statusResponseSuccess = if (newStatus != null) { - debugPrint { "Add to ${this.name} list: status=$newStatus" } - app.post( - "$mainUrl/sync/add-to-list", - json = StatusRequest( - shows = listOf( - StatusMediaObject( - null, - null, - MediaObject.Ids.fromMap(parsedId), - newStatus - ) - ), - movies = emptyList() - ), - interceptor = interceptor - ).isSuccessful - } else { - true - } - - debugPrint { "All scoring complete: rating=$ratingResponseSuccess, status=$statusResponseSuccess, episode=$episodeResponseSuccess" } requireLibraryRefresh = true - return ratingResponseSuccess && statusResponseSuccess && episodeResponseSuccess + return builder.execute() } @@ -694,17 +888,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ).parsedSafe() } - suspend fun getEpisodes(simklId: Int?, type: String?): Array? { - if (simklId == null) return null - val url = when (type) { - "anime" -> "https://api.simkl.com/anime/episodes/$simklId" - "tv" -> "https://api.simkl.com/tv/episodes/$simklId" - "movie" -> return null - else -> return null - } - return app.get(url, params = mapOf("client_id" to clientId)).parsedSafe() - } - override suspend fun search(name: String): List? { return app.get( "$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name) @@ -737,16 +920,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { return null } - private suspend fun getSyncListSince(since: Long?): AllItemsResponse { + private suspend fun getSyncListSince(since: Long?): AllItemsResponse? { val params = getDateTime(since)?.let { mapOf("date_from" to it) } ?: emptyMap() + // Can return null on no change. return app.get( "$mainUrl/sync/all-items/", params = params, interceptor = interceptor - ).parsed() + ).parsedSafe() } private suspend fun getActivities(): ActivitiesResponse? { From b6e99d7358ee5d13127be79869fa162a505f3a09 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 9 Sep 2023 23:18:21 +0200 Subject: [PATCH 134/156] backend change for events in player --- .../ui/player/AbstractPlayerFragment.kt | 95 ++++++-- .../cloudstream3/ui/player/CS3IPlayer.kt | 215 +++++++----------- .../ui/player/FullScreenPlayer.kt | 4 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 11 +- .../cloudstream3/ui/player/IPlayer.kt | 136 +++++++++-- .../ui/result/ResultFragmentPhone.kt | 4 +- .../ui/result/ResultTrailerPlayer.kt | 10 +- 7 files changed, 289 insertions(+), 186 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 53ee5e12..c6f02f1a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -92,11 +92,11 @@ abstract class AbstractPlayerFragment( throw NotImplementedError() } - open fun playerPositionChanged(posDur: Pair) { + open fun playerPositionChanged(position: Long, duration : Long) { throw NotImplementedError() } - open fun playerDimensionsLoaded(widthHeight: Pair) { + open fun playerDimensionsLoaded(width: Int, height : Int) { throw NotImplementedError() } @@ -132,8 +132,8 @@ abstract class AbstractPlayerFragment( } } - private fun updateIsPlaying(playing: Pair) { - val (wasPlaying, isPlaying) = playing + private fun updateIsPlaying(wasPlaying : CSPlayerLoading, + isPlaying : CSPlayerLoading) { val isPlayingRightNow = CSPlayerLoading.IsPlaying == isPlaying val isPausedRightNow = CSPlayerLoading.IsPaused == isPlaying @@ -206,7 +206,7 @@ abstract class AbstractPlayerFragment( CSPlayerEvent.values()[intent.getIntExtra( EXTRA_CONTROL_TYPE, 0 - )] + )], source = PlayerEventSource.UI ) } } @@ -216,7 +216,7 @@ abstract class AbstractPlayerFragment( val isPlaying = player.getIsPlaying() val isPlayingValue = if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused - updateIsPlaying(Pair(isPlayingValue, isPlayingValue)) + updateIsPlaying(isPlayingValue, isPlayingValue) } else { // Restore the full-screen UI. piphide?.isVisible = true @@ -249,7 +249,7 @@ abstract class AbstractPlayerFragment( } } - open fun playerError(exception: Exception) { + open fun playerError(exception: Throwable) { fun showToast(message: String, gotoNext: Boolean = false) { if (gotoNext && hasNextMirror()) { showToast( @@ -326,6 +326,7 @@ abstract class AbstractPlayerFragment( } } + @SuppressLint("UnsafeOptInUsageError") private fun playerUpdated(player: Any?) { if (player is ExoPlayer) { context?.let { ctx -> @@ -366,6 +367,71 @@ abstract class AbstractPlayerFragment( // } //} + /** This receives the events from the player, if you want to append functionality you do it here, + * do note that this only receives events for UI changes, + * and returning early WONT stop it from changing in eg the player time or pause status */ + open fun mainCallback(event : PlayerEvent) { + when(event) { + is ResizedEvent -> { + playerDimensionsLoaded(event.width, event.height) + } + is PlayerAttachedEvent -> { + playerUpdated(event.player) + } + is SubtitlesUpdatedEvent -> { + subtitlesChanged() + } + is TimestampSkippedEvent -> { + onTimestampSkipped(event.timestamp) + } + is TimestampInvokedEvent -> { + onTimestamp(event.timestamp) + } + is TracksChangedEvent -> { + onTracksInfoChanged() + } + is EmbeddedSubtitlesFetchedEvent -> { + embeddedSubtitlesFetched(event.tracks) + } + is ErrorEvent -> { + playerError(event.error) + } + is RequestAudioFocusEvent -> { + requestAudioFocus() + } + is EpisodeSeekEvent -> { + when(event.offset) { + -1 -> prevEpisode() + 1 -> nextEpisode() + else -> {} + } + } + is StatusEvent -> { + updateIsPlaying(wasPlaying = event.wasPlaying, isPlaying = event.isPlaying) + } + is PositionEvent -> { + playerPositionChanged(position = event.toMs, duration = event.durationMs) + } + is VideoEndedEvent -> { + context?.let { ctx -> + // Only play next episode if autoplay is on (default) + if (PreferenceManager.getDefaultSharedPreferences(ctx) + ?.getBoolean( + ctx.getString(R.string.autoplay_next_key), + true + ) == true + ) { + player.handleEvent( + CSPlayerEvent.NextEpisode, + source = PlayerEventSource.Player + ) + } + } + } + is PauseEvent -> Unit + is PlayEvent -> Unit + } + } @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -374,25 +440,13 @@ abstract class AbstractPlayerFragment( player.releaseCallbacks() player.initCallbacks( - playerUpdated = ::playerUpdated, - updateIsPlaying = ::updateIsPlaying, - playerError = ::playerError, - requestAutoFocus = ::requestAudioFocus, - nextEpisode = ::nextEpisode, - prevEpisode = ::prevEpisode, - playerPositionChanged = ::playerPositionChanged, - playerDimensionsLoaded = ::playerDimensionsLoaded, + eventHandler = ::mainCallback, requestedListeningPercentages = listOf( SKIP_OP_VIDEO_PERCENTAGE, PRELOAD_NEXT_EPISODE_PERCENTAGE, NEXT_WATCH_EPISODE_PERCENTAGE, UPDATE_SYNC_PROGRESS_PERCENTAGE, ), - subtitlesUpdates = ::subtitlesChanged, - embeddedSubtitlesFetched = ::embeddedSubtitlesFetched, - onTracksInfoChanged = ::onTracksInfoChanged, - onTimestampInvoked = ::onTimestamp, - onTimestampSkipped = ::onTimestampSkipped ) if (player is CS3IPlayer) { @@ -461,6 +515,7 @@ abstract class AbstractPlayerFragment( resize(PlayerResize.values()[resize], showToast) } + @SuppressLint("UnsafeOptInUsageError") fun resize(resize: PlayerResize, showToast: Boolean) { setKey(RESIZE_MODE_KEY, resize.ordinal) val type = when (resize) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 4a88a2e7..fe4e3423 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -8,7 +8,6 @@ import android.os.Looper import android.util.Log import android.util.Rational import android.widget.FrameLayout -import androidx.media3.common.C import androidx.media3.common.C.* import androidx.media3.common.Format import androidx.media3.common.MediaItem @@ -135,80 +134,24 @@ class CS3IPlayer : IPlayer { * Boolean = if it's active * */ private var playerSelectedSubtitleTracks = listOf>() - - /** isPlaying */ - private var updateIsPlaying: ((Pair) -> Unit)? = null - private var requestAutoFocus: (() -> Unit)? = null - private var playerError: ((Exception) -> Unit)? = null - private var subtitlesUpdates: (() -> Unit)? = null - - /** width x height */ - private var playerDimensionsLoaded: ((Pair) -> Unit)? = null - - /** used for playerPositionChanged */ private var requestedListeningPercentages: List? = null - /** Fired when seeking the player or on requestedListeningPercentages, - * used to make things appear on que - * position, duration */ - private var playerPositionChanged: ((Pair) -> Unit)? = null + private var eventHandler: ((PlayerEvent) -> Unit)? = null - private var nextEpisode: (() -> Unit)? = null - private var prevEpisode: (() -> Unit)? = null - - private var playerUpdated: ((Any?) -> Unit)? = null - private var embeddedSubtitlesFetched: ((List) -> Unit)? = null - private var onTracksInfoChanged: (() -> Unit)? = null - private var onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)? = null - private var onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)? = null + fun event(event: PlayerEvent) { + eventHandler?.invoke(event) + } override fun releaseCallbacks() { - playerUpdated = null - updateIsPlaying = null - requestAutoFocus = null - playerError = null - playerDimensionsLoaded = null - requestedListeningPercentages = null - playerPositionChanged = null - nextEpisode = null - prevEpisode = null - subtitlesUpdates = null - onTracksInfoChanged = null - onTimestampInvoked = null - requestSubtitleUpdate = null - onTimestampSkipped = null + eventHandler = null } override fun initCallbacks( - playerUpdated: (Any?) -> Unit, - updateIsPlaying: ((Pair) -> Unit)?, - requestAutoFocus: (() -> Unit)?, - playerError: ((Exception) -> Unit)?, - playerDimensionsLoaded: ((Pair) -> Unit)?, + eventHandler: ((PlayerEvent) -> Unit), requestedListeningPercentages: List?, - playerPositionChanged: ((Pair) -> Unit)?, - nextEpisode: (() -> Unit)?, - prevEpisode: (() -> Unit)?, - subtitlesUpdates: (() -> Unit)?, - embeddedSubtitlesFetched: ((List) -> Unit)?, - onTracksInfoChanged: (() -> Unit)?, - onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)?, - onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)?, ) { - this.playerUpdated = playerUpdated - this.updateIsPlaying = updateIsPlaying - this.requestAutoFocus = requestAutoFocus - this.playerError = playerError - this.playerDimensionsLoaded = playerDimensionsLoaded this.requestedListeningPercentages = requestedListeningPercentages - this.playerPositionChanged = playerPositionChanged - this.nextEpisode = nextEpisode - this.prevEpisode = prevEpisode - this.subtitlesUpdates = subtitlesUpdates - this.embeddedSubtitlesFetched = embeddedSubtitlesFetched - this.onTracksInfoChanged = onTracksInfoChanged - this.onTimestampInvoked = onTimestampInvoked - this.onTimestampSkipped = onTimestampSkipped + this.eventHandler = eventHandler } // I know, this is not a perfect solution, however it works for fixing subs @@ -217,7 +160,7 @@ class CS3IPlayer : IPlayer { try { Handler(it).post { try { - seekTime(1L) + seekTime(1L, source = PlayerEventSource.Player) } catch (e: Exception) { logError(e) } @@ -271,8 +214,9 @@ class CS3IPlayer : IPlayer { subtitleHelper.setAllSubtitles(subtitles) } - var currentSubtitles: SubtitleData? = null + private var currentSubtitles: SubtitleData? = null + @SuppressLint("UnsafeOptInUsageError") private fun List.getTrack(id: String?): Pair? { if (id == null) return null // This beast of an expression does: @@ -526,14 +470,14 @@ class CS3IPlayer : IPlayer { Log.i(TAG, "onStop") saveData() - exoPlayer?.pause() + handleEvent(CSPlayerEvent.Pause, PlayerEventSource.Player) //releasePlayer() } override fun onPause() { Log.i(TAG, "onPause") saveData() - exoPlayer?.pause() + handleEvent(CSPlayerEvent.Pause, PlayerEventSource.Player) //releasePlayer() } @@ -611,6 +555,7 @@ class CS3IPlayer : IPlayer { } } + @SuppressLint("UnsafeOptInUsageError") private fun Context.createOfflineSource(): DataSource.Factory { return DefaultDataSourceFactory(this, USER_AGENT) } @@ -846,43 +791,55 @@ class CS3IPlayer : IPlayer { return null } - fun updatedTime(writePosition: Long? = null) { + fun updatedTime( + writePosition: Long? = null, + source: PlayerEventSource = PlayerEventSource.Player + ) { val position = writePosition ?: exoPlayer?.currentPosition getCurrentTimestamp(position)?.let { timestamp -> - onTimestampInvoked?.invoke(timestamp) + event(TimestampInvokedEvent(timestamp, source)) } val duration = exoPlayer?.contentDuration if (duration != null && position != null) { - playerPositionChanged?.invoke(Pair(position, duration)) + event( + PositionEvent( + source, + fromMs = exoPlayer?.currentPosition ?: 0, + position, + duration + ) + ) } } - override fun seekTime(time: Long) { - exoPlayer?.seekTime(time) + override fun seekTime(time: Long, source: PlayerEventSource) { + exoPlayer?.seekTime(time, source) } - override fun seekTo(time: Long) { - updatedTime(time) + override fun seekTo(time: Long, source: PlayerEventSource) { + updatedTime(time, source) exoPlayer?.seekTo(time) } - private fun ExoPlayer.seekTime(time: Long) { - updatedTime(currentPosition + time) + private fun ExoPlayer.seekTime(time: Long, source: PlayerEventSource) { + updatedTime(currentPosition + time, source) seekTo(currentPosition + time) } - override fun handleEvent(event: CSPlayerEvent) { + override fun handleEvent(event: CSPlayerEvent, source: PlayerEventSource) { Log.i(TAG, "handleEvent ${event.name}") try { exoPlayer?.apply { when (event) { CSPlayerEvent.Play -> { + event(PlayEvent(source)) play() } CSPlayerEvent.Pause -> { + event(PauseEvent(source)) pause() } @@ -899,32 +856,32 @@ class CS3IPlayer : IPlayer { CSPlayerEvent.PlayPauseToggle -> { if (isPlaying) { - pause() + handleEvent(CSPlayerEvent.Pause, source) } else { - play() + handleEvent(CSPlayerEvent.Play, source) } } - CSPlayerEvent.SeekForward -> seekTime(seekActionTime) - CSPlayerEvent.SeekBack -> seekTime(-seekActionTime) - CSPlayerEvent.NextEpisode -> nextEpisode?.invoke() - CSPlayerEvent.PrevEpisode -> prevEpisode?.invoke() + CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source) + CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source) + CSPlayerEvent.NextEpisode -> event(EpisodeSeekEvent(offset = 1, source = source)) + CSPlayerEvent.PrevEpisode -> event(EpisodeSeekEvent(offset = -1, source = source)) CSPlayerEvent.SkipCurrentChapter -> { //val dur = this@CS3IPlayer.getDuration() ?: return@apply getCurrentTimestamp()?.let { lastTimeStamp -> if (lastTimeStamp.skipToNextEpisode) { - handleEvent(CSPlayerEvent.NextEpisode) + handleEvent(CSPlayerEvent.NextEpisode, source) } else { seekTo(lastTimeStamp.endMs + 1L) } - onTimestampSkipped?.invoke(lastTimeStamp) + event(TimestampSkippedEvent(timestamp = lastTimeStamp, source = source)) } } } } - } catch (e: Exception) { - Log.e(TAG, "handleEvent error", e) - playerError?.invoke(e) + } catch (t: Throwable) { + Log.e(TAG, "handleEvent error", t) + event(ErrorEvent(t)) } } @@ -963,18 +920,14 @@ class CS3IPlayer : IPlayer { requestSubtitleUpdate = ::reloadSubs - playerUpdated?.invoke(exoPlayer) + event(PlayerAttachedEvent(exoPlayer)) exoPlayer?.prepare() exoPlayer?.let { exo -> - updateIsPlaying?.invoke( - Pair( - CSPlayerLoading.IsBuffering, - CSPlayerLoading.IsBuffering - ) - ) + event(StatusEvent(CSPlayerLoading.IsBuffering, CSPlayerLoading.IsBuffering)) isPlaying = exo.isPlaying } + exoPlayer?.addListener(object : Player.Listener { override fun onTracksChanged(tracks: Tracks) { normalSafeApiCall { @@ -1008,18 +961,19 @@ class CS3IPlayer : IPlayer { ) } - embeddedSubtitlesFetched?.invoke(exoPlayerReportedTracks) - onTracksInfoChanged?.invoke() - subtitlesUpdates?.invoke() + event(EmbeddedSubtitlesFetchedEvent(tracks = exoPlayerReportedTracks)) + event(TracksChangedEvent()) + event(SubtitlesUpdatedEvent()) } } + @SuppressLint("UnsafeOptInUsageError") override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { exoPlayer?.let { exo -> - updateIsPlaying?.invoke( - Pair( - if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused, - if (playbackState == Player.STATE_BUFFERING) CSPlayerLoading.IsBuffering else if (exo.isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused + event( + StatusEvent( + wasPlaying = if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused, + isPlaying = if (playbackState == Player.STATE_BUFFERING) CSPlayerLoading.IsBuffering else if (exo.isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused ) ) isPlaying = exo.isPlaying @@ -1041,23 +995,15 @@ class CS3IPlayer : IPlayer { } Player.STATE_ENDED -> { - // Only play next episode if autoplay is on (default) - if (PreferenceManager.getDefaultSharedPreferences(context) - ?.getBoolean( - context.getString(com.lagradost.cloudstream3.R.string.autoplay_next_key), - true - ) == true - ) { - handleEvent(CSPlayerEvent.NextEpisode) - } + event(VideoEndedEvent()) } Player.STATE_BUFFERING -> { - updatedTime() + updatedTime(source = PlayerEventSource.Player) } Player.STATE_IDLE -> { - // IDLE + } else -> Unit @@ -1082,7 +1028,7 @@ class CS3IPlayer : IPlayer { } else -> { - playerError?.invoke(error) + event(ErrorEvent(error)) } } @@ -1096,7 +1042,7 @@ class CS3IPlayer : IPlayer { override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) if (isPlaying) { - requestAutoFocus?.invoke() + event(RequestAudioFocusEvent()) onRenderFirst() } } @@ -1116,12 +1062,15 @@ class CS3IPlayer : IPlayer { true ) == true ) { - handleEvent(CSPlayerEvent.NextEpisode) + handleEvent( + CSPlayerEvent.NextEpisode, + source = PlayerEventSource.Player + ) } } Player.STATE_BUFFERING -> { - updatedTime() + updatedTime(source = PlayerEventSource.Player) } Player.STATE_IDLE -> { @@ -1134,18 +1083,18 @@ class CS3IPlayer : IPlayer { override fun onVideoSizeChanged(videoSize: VideoSize) { super.onVideoSizeChanged(videoSize) - playerDimensionsLoaded?.invoke(Pair(videoSize.width, videoSize.height)) + event(ResizedEvent(height = videoSize.height, width = videoSize.width)) } override fun onRenderedFirstFrame() { super.onRenderedFirstFrame() onRenderFirst() - updatedTime() + updatedTime(source = PlayerEventSource.Player) } }) - } catch (e: Exception) { - Log.e(TAG, "loadExo error", e) - playerError?.invoke(e) + } catch (t: Throwable) { + Log.e(TAG, "loadExo error", t) + event(ErrorEvent(t)) } } @@ -1156,7 +1105,7 @@ class CS3IPlayer : IPlayer { lastTimeStamps = timeStamps timeStamps.forEach { timestamp -> exoPlayer?.createMessage { _, _ -> - updatedTime() + updatedTime(source = PlayerEventSource.Player) //if (payload is EpisodeSkip.SkipStamp) // this should always be true // onTimestampInvoked?.invoke(payload) } @@ -1166,7 +1115,7 @@ class CS3IPlayer : IPlayer { ?.setDeleteAfterDelivery(false) ?.send() } - updatedTime() + updatedTime(source = PlayerEventSource.Player) } @SuppressLint("UnsafeOptInUsageError") @@ -1187,7 +1136,7 @@ class CS3IPlayer : IPlayer { if (invalid) { releasePlayer(saveTime = false) - playerError?.invoke(InvalidFileException("Too short playback")) + event(ErrorEvent(InvalidFileException("Too short playback"))) return } @@ -1196,7 +1145,7 @@ class CS3IPlayer : IPlayer { val width = format?.width val height = format?.height if (height != null && width != null) { - playerDimensionsLoaded?.invoke(Pair(width, height)) + event(ResizedEvent(width = width, height = height)) updatedTime() exoPlayer?.apply { requestedListeningPercentages?.forEach { percentage -> @@ -1230,9 +1179,9 @@ class CS3IPlayer : IPlayer { subtitleHelper.setActiveSubtitles(activeSubtitles.toSet()) loadExo(context, listOf(MediaItemSlice(mediaItem, Long.MIN_VALUE)), subSources) - } catch (e: Exception) { - Log.e(TAG, "loadOfflinePlayer error", e) - playerError?.invoke(e) + } catch (t: Throwable) { + Log.e(TAG, "loadOfflinePlayer error", t) + event(ErrorEvent(t)) } } @@ -1365,9 +1314,9 @@ class CS3IPlayer : IPlayer { } loadExo(context, mediaItems, subSources, cacheFactory) - } catch (e: Exception) { - Log.e(TAG, "loadOnlinePlayer error", e) - playerError?.invoke(e) + } catch (t: Throwable) { + Log.e(TAG, "loadOnlinePlayer error", t) + event(ErrorEvent(t)) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index 0f3c189d..6dabb5b7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -874,7 +874,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { currentTouch )?.let { seekTo -> if (abs(seekTo - startTime) > MINIMUM_SEEK_TIME) { - player.seekTo(seekTo) + player.seekTo(seekTo, PlayerEventSource.UI) } } } @@ -909,7 +909,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } else -> { - player.handleEvent(CSPlayerEvent.PlayPauseToggle) + player.handleEvent(CSPlayerEvent.PlayPauseToggle, PlayerEventSource.UI) } } } else if (doubleTapEnabled && isFullScreenPlayer) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 2b9304b6..b2542ffa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -551,7 +551,7 @@ class GeneratorPlayer : FullScreenPlayer() { //println("CURRENT SELECTED :$currentSelectedSubtitles of $currentSubs") context?.let { ctx -> val isPlaying = player.getIsPlaying() - player.handleEvent(CSPlayerEvent.Pause) + player.handleEvent(CSPlayerEvent.Pause, PlayerEventSource.UI) val currentSubtitles = sortSubs(currentSubs) val sourceDialog = Dialog(ctx, R.style.AlertDialogCustomBlack) @@ -883,7 +883,7 @@ class GeneratorPlayer : FullScreenPlayer() { } - override fun playerError(exception: Exception) { + override fun playerError(exception: Throwable) { Log.i(TAG, "playerError = $currentSelectedLink") super.playerError(exception) } @@ -945,14 +945,13 @@ class GeneratorPlayer : FullScreenPlayer() { var maxEpisodeSet: Int? = null var hasRequestedStamps: Boolean = false - override fun playerPositionChanged(posDur: Pair) { + override fun playerPositionChanged(position: Long, duration : Long) { // Don't save livestream data if ((currentMeta as? ResultEpisode)?.tvType?.isLiveStream() == true) return // Don't save NSFW data if ((currentMeta as? ResultEpisode)?.tvType == TvType.NSFW) return - val (position, duration) = posDur if (duration <= 0L) return // idk how you achieved this, but div by zero crash if (!hasRequestedStamps) { hasRequestedStamps = true @@ -1209,8 +1208,8 @@ class GeneratorPlayer : FullScreenPlayer() { } } - override fun playerDimensionsLoaded(widthHeight: Pair) { - setPlayerDimen(widthHeight) + override fun playerDimensionsLoaded(width: Int, height : Int) { + setPlayerDimen(width to height) } private fun unwrapBundle(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index 3038cb8d..ec006234 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -45,9 +45,120 @@ enum class CSPlayerLoading { IsPaused, IsPlaying, IsBuffering, - //IsDone, } +enum class PlayerEventSource { + /** This event was invoked from the user pressing some button or selecting something */ + UI, + + /** This event was invoked automatically */ + Player, + + /** This event was invoked from a external sync tool like WatchTogether */ + Sync, +} + +abstract class PlayerEvent { + abstract val source: PlayerEventSource +} + +/** this is used to update UI based of the current time, + * using requestedListeningPercentages as well as saving time */ +data class PositionEvent( + override val source: PlayerEventSource, + val fromMs: Long, + val toMs: Long, + /** duration of the entire video */ + val durationMs: Long, +) : PlayerEvent() { + /** how many ms (+-) we have skipped */ + val seekMs : Long get() = toMs - fromMs +} + +/** player error when rendering or misc, used to display toast or log */ +data class ErrorEvent( + val error: Throwable, + override val source: PlayerEventSource = PlayerEventSource.Player, +) : PlayerEvent() + +/** Event when timestamps appear, null when it should disappear */ +data class TimestampInvokedEvent( + val timestamp: EpisodeSkip.SkipStamp, + override val source: PlayerEventSource = PlayerEventSource.Player, +) : PlayerEvent() + +/** Event for when a chapter is skipped, aka when event is handled (or for future use when skip automatically ads/sponsor) */ +data class TimestampSkippedEvent( + val timestamp: EpisodeSkip.SkipStamp, + override val source: PlayerEventSource = PlayerEventSource.Player, +) : PlayerEvent() + +/** this is used by the player to load the next or prev episode */ +data class EpisodeSeekEvent( + /** -1 = prev, 1 = next, will never be 0, atm the user cant seek more than +-1 */ + val offset: Int, + override val source: PlayerEventSource = PlayerEventSource.Player, +) : PlayerEvent() { + init { + assert(offset != 0) + } +} + +/** Event when the video is resized aka changed resolution or mirror */ +data class ResizedEvent( + val height: Int, + val width: Int, + override val source: PlayerEventSource = PlayerEventSource.Player, +) : PlayerEvent() + +/** Event when the player status update, along with the previous status (for animation)*/ +data class StatusEvent( + val wasPlaying: CSPlayerLoading, + val isPlaying: CSPlayerLoading, + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Event when tracks are changed, used for UI changes */ +data class TracksChangedEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Event from player to give all embedded subtitles */ +data class EmbeddedSubtitlesFetchedEvent( + val tracks: List, + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** on attach player to view */ +data class PlayerAttachedEvent( + val player: Any?, + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Event from player to inform that subtitles have updated in some way */ +data class SubtitlesUpdatedEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** current player starts, asking for all other programs to shut the fuck up */ +data class RequestAudioFocusEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Pause event, separate from StatusEvent */ +data class PauseEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Play event, separate from StatusEvent */ +data class PlayEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Event when the player video has ended, up to the settings on what to do when that happens */ +data class VideoEndedEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() interface Track { /** @@ -108,27 +219,16 @@ interface IPlayer { fun getDuration(): Long? fun getPosition(): Long? - fun seekTime(time: Long) - fun seekTo(time: Long) + fun seekTime(time: Long, source: PlayerEventSource = PlayerEventSource.UI) + fun seekTo(time: Long, source: PlayerEventSource = PlayerEventSource.UI) fun getSubtitleOffset(): Long // in ms fun setSubtitleOffset(offset: Long) // in ms fun initCallbacks( - playerUpdated: (Any?) -> Unit, // attach player to view - updateIsPlaying: ((Pair) -> Unit)? = null, // (wasPlaying, isPlaying) - requestAutoFocus: (() -> Unit)? = null, // current player starts, asking for all other programs to shut the fuck up - playerError: ((Exception) -> Unit)? = null, // player error when rendering or misc, used to display toast or log - playerDimensionsLoaded: ((Pair) -> Unit)? = null, // (with, height), for UI - requestedListeningPercentages: List? = null, // this is used to request when the player should report back view percentage - playerPositionChanged: ((Pair) -> Unit)? = null,// (position, duration) this is used to update UI based of the current time - nextEpisode: (() -> Unit)? = null, // this is used by the player to load the next episode - prevEpisode: (() -> Unit)? = null, // this is used by the player to load the previous episode - subtitlesUpdates: (() -> Unit)? = null, // callback from player to inform that subtitles have updated in some way - embeddedSubtitlesFetched: ((List) -> Unit)? = null, // callback from player to give all embedded subtitles - onTracksInfoChanged: (() -> Unit)? = null, // Callback when tracks are changed, used for UI changes - onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)? = null, // Callback when timestamps appear, null when it should disappear - onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)? = null, // callback for when a chapter is skipped, aka when event is handled (or for future use when skip automatically ads/sponsor) + eventHandler: ((PlayerEvent) -> Unit), + /** this is used to request when the player should report back view percentage */ + requestedListeningPercentages: List? = null, ) fun releaseCallbacks() @@ -155,7 +255,7 @@ interface IPlayer { fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean // returns true if the player requires a reload, null for nothing fun getCurrentPreferredSubtitle(): SubtitleData? - fun handleEvent(event: CSPlayerEvent) + fun handleEvent(event: CSPlayerEvent, source: PlayerEventSource = PlayerEventSource.UI) fun onStop() fun onPause() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index a932a57c..ef2ed0df 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -130,8 +130,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { return currentTrailerIndex + 1 < currentTrailers.size } - override fun playerError(exception: Exception) { - if (player.getIsPlaying()) { // because we dont want random toasts in player + override fun playerError(exception: Throwable) { + if (player.getIsPlaying()) { // because we don't want random toasts in player super.playerError(exception) } else { nextMirror() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index 91e97dfc..5208e4a5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.ui.result import android.animation.ValueAnimator import android.content.Context import android.content.res.Configuration -import android.graphics.Rect import android.os.Bundle import android.view.View import android.view.ViewGroup @@ -12,6 +11,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CSPlayerEvent +import com.lagradost.cloudstream3.ui.player.PlayerEventSource import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.utils.IOnBackPressed @@ -32,7 +32,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed { override fun prevEpisode() {} - override fun playerPositionChanged(posDur: Pair) {} + override fun playerPositionChanged(position: Long, duration : Long) {} override fun nextMirror() {} @@ -99,8 +99,8 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed { } } - override fun playerDimensionsLoaded(widthHeight: Pair) { - playerWidthHeight = widthHeight + override fun playerDimensionsLoaded(width: Int, height : Int) { + playerWidthHeight = width to height fixPlayerSize() } @@ -164,7 +164,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed { playerBinding?.playerIntroPlay?.setOnClickListener { playerBinding?.playerIntroPlay?.isGone = true - player.handleEvent(CSPlayerEvent.Play) + player.handleEvent(CSPlayerEvent.Play, PlayerEventSource.UI) updateUIVisibility() fixPlayerSize() } From 85c4c74222188ef1b998bdef62eb011cea8efedc Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 9 Sep 2023 23:53:35 +0200 Subject: [PATCH 135/156] fixed shitty external extractor code --- .../main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 0a926374..62217a0b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -378,6 +378,9 @@ open class ExtractorLink constructor( open val extractorData: String? = null, open val type: ExtractorLinkType, ) : VideoDownloadManager.IDownloadableMinimum { + val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8 + val isDash : Boolean get() = type == ExtractorLinkType.DASH + constructor( source: String, name: String, From 0afbc90cd2a57c0cd5bb6d0709dcd44c0fc85b26 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 9 Sep 2023 23:57:18 +0200 Subject: [PATCH 136/156] fixed last fix --- .../main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 62217a0b..5edff7a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -227,7 +227,6 @@ data class ExtractorLinkPlayList( val playlist: List, override val referer: String, override val quality: Int, - val isM3u8: Boolean = false, override val headers: Map = mapOf(), /** Used for getExtractorVerifierJob() */ override val extractorData: String? = null, From f6b0ea8dfa4253446e9532689531a0aba6505f54 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sun, 10 Sep 2023 16:31:01 +0300 Subject: [PATCH 137/156] Stream button type switch support (#597) * Stream button type switch support * Hide Hls playlist switch --- .../com/lagradost/cloudstream3/ui/player/LinkGenerator.kt | 5 ++--- app/src/main/res/layout/stream_input.xml | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt index ba2cdb40..ca2d9c81 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt @@ -67,9 +67,8 @@ class LinkGenerator( link.name ?: link.url, unshortenLinkSafe(link.url), // unshorten because it might be a raw link referer ?: "", - Qualities.Unknown.value, isM3u8 ?: normalSafeApiCall { - URI(link.url).path?.substringAfterLast(".")?.contains("m3u") - } ?: false + Qualities.Unknown.value, + type = INFER_TYPE, ) to null ) } diff --git a/app/src/main/res/layout/stream_input.xml b/app/src/main/res/layout/stream_input.xml index 20a91b4a..83605ce3 100644 --- a/app/src/main/res/layout/stream_input.xml +++ b/app/src/main/res/layout/stream_input.xml @@ -49,7 +49,8 @@ android:layout_marginTop="10dp" android:text="@string/hls_playlist" android:textColor="?attr/textColor" - android:textSize="16sp" /> + android:textSize="16sp" + android:visibility="invisible" /> From 7f7c81828a294a6f77cbc563dcf1b77b96173e6e Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:37:02 +0200 Subject: [PATCH 138/156] added UI event for seekbar --- .../ui/player/AbstractPlayerFragment.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index c6f02f1a..4316bbc6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -7,6 +7,7 @@ import android.graphics.drawable.AnimatedVectorDrawable import android.media.metrics.PlaybackErrorEvent import android.os.Build import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -25,8 +26,10 @@ import androidx.media3.common.PlaybackException import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.DefaultTimeBar import androidx.media3.ui.PlayerView import androidx.media3.ui.SubtitleView +import androidx.media3.ui.TimeBar import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode @@ -371,6 +374,7 @@ abstract class AbstractPlayerFragment( * do note that this only receives events for UI changes, * and returning early WONT stop it from changing in eg the player time or pause status */ open fun mainCallback(event : PlayerEvent) { + Log.i(TAG, "Handle event: $event") when(event) { is ResizedEvent -> { playerDimensionsLoaded(event.width, event.height) @@ -433,7 +437,7 @@ abstract class AbstractPlayerFragment( } } - @SuppressLint("SetTextI18n") + @SuppressLint("SetTextI18n", "UnsafeOptInUsageError") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { resizeMode = getKey(RESIZE_MODE_KEY) ?: 0 resize(resizeMode, false) @@ -454,6 +458,19 @@ abstract class AbstractPlayerFragment( subStyle = SubtitlesFragment.getCurrentSavedStyle() player.initSubtitles(subView, subtitleHolder, subStyle) + /** this might seam a bit fucky and that is because it is, the seek event is captured twice, once by the player + * and once by the UI even if it should only be registered once by the UI */ + playerView?.findViewById(R.id.exo_progress)?.addListener(object : TimeBar.OnScrubListener { + override fun onScrubStart(timeBar: TimeBar, position: Long) = Unit + override fun onScrubMove(timeBar: TimeBar, position: Long) = Unit + override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) { + if (canceled) return + val playerDuration = player.getDuration() ?: return + val playerPosition = player.getPosition() ?: return + mainCallback(PositionEvent(source = PlayerEventSource.UI, durationMs = playerDuration, fromMs = playerPosition, toMs = position)) + } + }) + SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged try { From 10bc688eaf00d34bd41da06d53f1142fc7888f09 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:29:30 +0200 Subject: [PATCH 139/156] fixed tracker on dub --- .../lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index b2c57137..b398b54e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -1538,7 +1538,11 @@ class ResultViewModel2 : ViewModel() { this.name, this.japName ).filter { it.length > 2 } - .distinct(), // the reason why we filter is due to not wanting smth like " " or "?" + .distinct().map { + // this actually would be nice if we improved a bit as 3rd season == season 3 == III ect + // right now it just removes the dubbed status + it.lowercase().replace(Regex("""\(?[ds]ub(bed)?\)?(\s|$)""") , "").trim() + }, TrackerType.getTypes(this.type), this.year ) From 01e7acdeace32f20e6c23f5cb187977bd6131d45 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Mon, 11 Sep 2023 19:31:11 +0700 Subject: [PATCH 140/156] getTracker: switched to anilist api (#593) * getTracker: switched to anilist api --------- Co-authored-by: Sofie99 --- .../com/lagradost/cloudstream3/MainAPI.kt | 119 +++++++++++++----- 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 80332445..0175e0d0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -22,8 +22,10 @@ import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf +import com.lagradost.nicehttp.RequestBodyTypes import okhttp3.Interceptor -import org.mozilla.javascript.Scriptable +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody import java.text.SimpleDateFormat import java.util.* import kotlin.math.absoluteValue @@ -180,7 +182,7 @@ object APIHolder { /** * Get anime tracker information based on title, year and type. * Both titles are attempted to be matched with both Romaji and English title. - * Uses the consumet api. + * Uses the anilist api. * * @param titles uses first index to search, but if you have multiple titles and want extra guarantee to match you can also have that * @param types Optional parameter to narrow down the scope to Movies, TV, etc. See TrackerType.getTypes() @@ -189,7 +191,8 @@ object APIHolder { suspend fun getTracker( titles: List, types: Set?, - year: Int? + year: Int?, + lessAccurate: Boolean = false ): Tracker? { return try { require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" } @@ -197,30 +200,70 @@ object APIHolder { val mainTitle = titles[0] val search = trackerCache[mainTitle] - ?: app.get("https://api.consumet.org/meta/anilist/$mainTitle") - .parsedSafe()?.also { - trackerCache[mainTitle] = it - } ?: return null + ?: searchAnilist(mainTitle)?.also { + trackerCache[mainTitle] = it + } ?: return null - val res = search.results?.find { media -> - val matchingYears = year == null || media.releaseDate == year + val res = search.data?.page?.media?.find { media -> + val matchingYears = year == null || media.seasonYear == year val matchingTitles = media.title?.let { title -> titles.any { userTitle -> title.isMatchingTitles(userTitle) } } ?: false - val matchingTypes = types?.any { it.name.equals(media.type, true) } == true - matchingTitles && matchingTypes && matchingYears + val matchingTypes = types?.any { it.name.equals(media.format, true) } == true + if(lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears } ?: return null - Tracker(res.malId, res.aniId, res.image, res.cover) + Tracker(res.idMal, res.id.toString(), res.coverImage?.extraLarge ?: res.coverImage?.large, res.bannerImage) } catch (t: Throwable) { logError(t) null } } + private suspend fun searchAnilist( + title: String?, + ): AniSearch? { + val query = """ + query ( + ${'$'}page: Int = 1 + ${'$'}search: String + ${'$'}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC] + ${'$'}type: MediaType + ) { + Page(page: ${'$'}page, perPage: 20) { + media( + search: ${'$'}search + sort: ${'$'}sort + type: ${'$'}type + ) { + id + idMal + title { romaji english } + coverImage { extraLarge large } + bannerImage + seasonYear + format + } + } + } + """.trimIndent().trim() + + val data = mapOf( + "query" to query, + "variables" to mapOf( + "search" to title, + "sort" to "SEARCH_MATCH", + "type" to "ANIME", + ) + ).toJson().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull()) + + return app.post("https://graphql.anilist.co", requestBody = data) + .parsedSafe() + } + fun Context.getApiSettings(): HashSet { //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) @@ -1730,30 +1773,42 @@ data class Tracker( val cover: String? = null, ) -data class Title( - @JsonProperty("romaji") val romaji: String? = null, - @JsonProperty("english") val english: String? = null, +data class AniSearch( + @JsonProperty("data") var data: Data? = Data() ) { - fun isMatchingTitles(title: String?): Boolean { - if (title == null) return false - return english.equals(title, true) || romaji.equals(title, true) + data class Data( + @JsonProperty("Page") var page: Page? = Page() + ) { + data class Page( + @JsonProperty("media") var media: ArrayList = arrayListOf() + ) { + data class Media( + @JsonProperty("title") var title: Title? = null, + @JsonProperty("id") var id: Int? = null, + @JsonProperty("idMal") var idMal: Int? = null, + @JsonProperty("seasonYear") var seasonYear: Int? = null, + @JsonProperty("format") var format: String? = null, + @JsonProperty("coverImage") var coverImage: CoverImage? = null, + @JsonProperty("bannerImage") var bannerImage: String? = null, + ) { + data class CoverImage( + @JsonProperty("extraLarge") var extraLarge: String? = null, + @JsonProperty("large") var large: String? = null, + ) + data class Title( + @JsonProperty("romaji") var romaji: String? = null, + @JsonProperty("english") var english: String? = null, + ) { + fun isMatchingTitles(title: String?): Boolean { + if (title == null) return false + return english.equals(title, true) || romaji.equals(title, true) + } + } + } + } } } -data class Results( - @JsonProperty("id") val aniId: String? = null, - @JsonProperty("malId") val malId: Int? = null, - @JsonProperty("title") val title: Title? = null, - @JsonProperty("releaseDate") val releaseDate: Int? = null, - @JsonProperty("type") val type: String? = null, - @JsonProperty("image") val image: String? = null, - @JsonProperty("cover") val cover: String? = null, -) - -data class AniSearch( - @JsonProperty("results") val results: ArrayList? = arrayListOf() -) - /** * used for the getTracker() method **/ From 2baa75496ec16ba5e8cf2eba13641563bcf6f8fc Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Mon, 11 Sep 2023 18:13:42 +0000 Subject: [PATCH 141/156] Fix opensubtitles (#598) * Fix OpenSubtitles --- .../providers/OpenSubtitlesApi.kt | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt index 3e372c2d..4030649d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -2,12 +2,13 @@ package com.lagradost.cloudstream3.syncproviders.providers import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty -import com.google.common.collect.BiMap -import com.google.common.collect.HashBiMap -import com.lagradost.cloudstream3.* 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 @@ -15,8 +16,8 @@ import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager import com.lagradost.cloudstream3.utils.AppUtils -import java.net.URLEncoder -import java.nio.charset.StandardCharsets +import okhttp3.Interceptor +import okhttp3.Response class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi { override val idPrefix = "opensubtitles" @@ -36,6 +37,23 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi var currentSession: SubtitleOAuthEntity? = null } + private val headerInterceptor = OpenSubtitleInterceptor() + + /** Automatically adds required api headers */ + private class OpenSubtitleInterceptor : Interceptor { + /** Required user agent! */ + private val userAgent = "Cloudstream3 v0.1" + override fun intercept(chain: Interceptor.Chain): Response { + return chain.proceed( + chain.request().newBuilder() + .removeHeader("user-agent") + .addHeader("user-agent", userAgent) + .addHeader("Api-Key", apiKey) + .build() + ) + } + } + private fun canDoRequest(): Boolean { return unixTimeMs > currentCoolDown } @@ -98,13 +116,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val response = app.post( url = "$host/login", headers = mapOf( - "Api-Key" to apiKey, - "Content-Type" to "application/json" + "Content-Type" to "application/json", ), data = mapOf( "username" to username, "password" to password - ) + ), + interceptor = headerInterceptor ) //Log.i(TAG, "Responsecode = ${response.code}") //Log.i(TAG, "Result => ${response.text}") @@ -149,11 +167,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi // "pt" to "pt-PT", // "pt" to "pt-BR" ) - private fun fixLanguage(language: String?) : String? { + + private fun fixLanguage(language: String?): String? { return languageExceptions[language] ?: language } + // O(n) but good enough, BiMap did not want to work properly - private fun fixLanguageReverse(language: String?) : String? { + private fun fixLanguageReverse(language: String?): String? { return languageExceptions.entries.firstOrNull { it.value == language }?.key ?: language } @@ -183,9 +203,9 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val req = app.get( url = searchQueryUrl, headers = mapOf( - Pair("Api-Key", apiKey), Pair("Content-Type", "application/json") - ) + ), + interceptor = headerInterceptor ) Log.i(TAG, "Search Req => ${req.text}") if (!req.isSuccessful) { @@ -207,7 +227,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi //Use any valid name/title in hierarchy val name = filename ?: featureDetails?.movieName ?: featureDetails?.title ?: featureDetails?.parentTitle ?: attr.release ?: query.query - val lang = fixLanguageReverse(attr.language)?: "" + val lang = fixLanguageReverse(attr.language) ?: "" val resEpNum = featureDetails?.episodeNumber ?: query.epNumber val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber val year = featureDetails?.year ?: query.year @@ -251,13 +271,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi "Authorization", "Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}" ), - Pair("Api-Key", apiKey), Pair("Content-Type", "application/json"), Pair("Accept", "*/*") ), data = mapOf( Pair("file_id", data.data) - ) + ), + interceptor = headerInterceptor ) Log.i(TAG, "Request result => (${req.code}) ${req.text}") //Log.i(TAG, "Request headers => ${req.headers}") From 7d6ba8c7a4feed2636ab7fd386d1c76ffa73f2de Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 11 Sep 2023 23:05:10 +0200 Subject: [PATCH 142/156] tv changes for better centering + bigger text + better contrast --- .../lagradost/cloudstream3/CommonActivity.kt | 37 ++++++++++++-- .../lagradost/cloudstream3/MainActivity.kt | 50 ++++++++++++++++--- .../ui/player/FullScreenPlayer.kt | 15 +----- .../ui/result/ResultTrailerPlayer.kt | 2 + .../main/res/layout/fragment_result_tv.xml | 40 +++++++++------ 5 files changed, 107 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 0bcd4152..a7d899b6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -7,9 +7,14 @@ import android.content.Context import android.content.pm.PackageManager import android.content.res.Resources import android.os.Build +import android.util.DisplayMetrics import android.util.Log -import android.view.* +import android.view.Gravity +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View import android.view.View.NO_ID +import android.view.ViewGroup import android.widget.TextView import android.widget.Toast import androidx.activity.ComponentActivity @@ -40,7 +45,9 @@ import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode import com.lagradost.cloudstream3.utils.UIHelper.toPx import org.schabi.newpipe.extractor.NewPipe import java.lang.ref.WeakReference -import java.util.* +import java.util.Locale +import kotlin.math.max +import kotlin.math.min enum class FocusDirection { Start, @@ -63,6 +70,19 @@ object CommonActivity { return (this as MainActivity?)?.mSessionManager?.currentCastSession } + val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics + + // screenWidth and screenHeight does always + // refer to the screen while in landscape mode + val screenWidth: Int + get() { + return max(displayMetrics.widthPixels, displayMetrics.heightPixels) + } + val screenHeight: Int + get() { + return min(displayMetrics.widthPixels, displayMetrics.heightPixels) + } + var canEnterPipMode: Boolean = false var canShowPipMode: Boolean = false @@ -328,6 +348,14 @@ object CommonActivity { currentLook = currentLook.parent as? View ?: break }*/ + private fun View.hasContent() : Boolean { + return isShown && when(this) { + //is RecyclerView -> this.childCount > 0 + is ViewGroup -> this.childCount > 0 + else -> true + } + } + /** skips the initial stage of searching for an id using the view, see getNextFocus for specification */ fun continueGetNextFocus( root: Any?, @@ -348,16 +376,17 @@ object CommonActivity { } ?: return null next = localLook(view, nextId) ?: next + val shown = next.hasContent() // if cant focus but visible then break and let android decide // the exception if is the view is a parent and has children that wants focus val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent -> parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0 } ?: false - if (!next.isFocusable && next.isShown && !hasChildrenThatWantsFocus) return null + if (!next.isFocusable && shown && !hasChildrenThatWantsFocus) return null // if not shown then continue because we will "skip" over views to get to a replacement - if (!next.isShown) { + if (!shown) { // we don't want a while true loop, so we let android decide if we find a recursive view if (next == view) return null return getNextFocus(root, next, direction, depth + 1) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index fbad4fce..a07ae2c2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.content.res.Configuration +import android.graphics.Rect import android.net.Uri import android.os.Build import android.os.Bundle @@ -52,6 +53,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.snackbar.Snackbar +import com.google.common.collect.Comparators.min import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.APIHolder.apis @@ -64,13 +66,13 @@ import com.lagradost.cloudstream3.CommonActivity.loadThemes import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint +import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.updateLocale import com.lagradost.cloudstream3.databinding.ActivityMainBinding import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding import com.lagradost.cloudstream3.mvvm.Resource -import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observeNullable @@ -832,6 +834,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { focusOutline.get()?.isVisible = false } } + /*private val scrollListener = object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + current = current.copy(x = current.x + dx, y = current.y + dy) + setTargetPosition(current) + } + }*/ private fun setTargetPosition(target: FocusTarget) { focusOutline.get()?.apply { @@ -874,7 +883,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (!exactlyTheSame) { lastView?.removeOnLayoutChangeListener(layoutListener) lastView?.removeOnAttachStateChangeListener(attachListener) - (lastView?.parent as? RecyclerView)?.removeOnLayoutChangeListener(layoutListener) + (lastView?.parent as? RecyclerView)?.apply { + removeOnLayoutChangeListener(layoutListener) + //removeOnScrollListener(scrollListener) + } } val wasGone = focusOutline.isGone @@ -952,7 +964,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { focusOutline.isVisible = false } if (!exactlyTheSame) { - (newFocus.parent as? RecyclerView)?.addOnLayoutChangeListener(layoutListener) + (newFocus.parent as? RecyclerView)?.apply { + addOnLayoutChangeListener(layoutListener) + //addOnScrollListener(scrollListener) + } newFocus.addOnLayoutChangeListener(layoutListener) newFocus.addOnAttachStateChangeListener(attachListener) } @@ -970,8 +985,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { ) // if they are the same within then snap, aka scrolling - val deltaMin = 50.toPx - if (start.width == end.width && start.height == end.height && (start.x - end.x).absoluteValue < deltaMin && (start.y - end.y).absoluteValue < deltaMin) { + val deltaMinX = min(end.width / 2, 60.toPx) + val deltaMinY = min(end.height / 2, 60.toPx) + if (start.width == end.width && start.height == end.height && (start.x - end.x).absoluteValue < deltaMinX && (start.y - end.y).absoluteValue < deltaMinY) { animator?.cancel() last = start current = end @@ -1000,7 +1016,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // animate between a and b animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply { startDelay = 0 - duration = 100 + duration = 200 addUpdateListener { animation -> val animatedValue = animation.animatedValue as Float val target = FocusTarget.lerp(last, current, minOf(animatedValue, 1.0f)) @@ -1095,7 +1111,29 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> // println("refocus $oldFocus -> $newFocus") + try { + val r = Rect(0,0,0,0) + newFocus.getDrawingRect(r) + val x = r.centerX() + val y = r.centerY() + val dx = 0 //screenWidth / 2 + val dy = screenHeight / 2 + val r2 = Rect(x-dx,y-dy,x+dx,y+dy) + newFocus.requestRectangleOnScreen(r2, false) + // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) + } catch (_ : Throwable) { } TvFocus.updateFocusView(newFocus) + /*var focus = newFocus + + while(focus != null) { + if(focus is ScrollingView && focus.canScrollVertically()) { + focus.scrollBy() + } + when(focus.parent) { + is View -> focus = newFocus + else -> break + } + }*/ } newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index 6dabb5b7..e698191d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -38,6 +38,8 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener +import com.lagradost.cloudstream3.CommonActivity.screenHeight +import com.lagradost.cloudstream3.CommonActivity.screenWidth import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding @@ -126,19 +128,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { protected var useTrueSystemBrightness = true private val fullscreenNotch = true //TODO SETTING - protected val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics - - // screenWidth and screenHeight does always - // refer to the screen while in landscape mode - protected val screenWidth: Int - get() { - return max(displayMetrics.widthPixels, displayMetrics.heightPixels) - } - protected val screenHeight: Int - get() { - return min(displayMetrics.widthPixels, displayMetrics.heightPixels) - } - private var statusBarHeight: Int? = null private var navigationBarHeight: Int? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index 5208e4a5..c30e70e5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -9,6 +9,8 @@ import android.view.ViewGroup import android.widget.FrameLayout import androidx.core.view.isGone import androidx.core.view.isVisible +import com.lagradost.cloudstream3.CommonActivity.screenHeight +import com.lagradost.cloudstream3.CommonActivity.screenWidth import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.PlayerEventSource diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 1fde999c..4d236d78 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -128,7 +128,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit - - @@ -411,7 +409,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:padding="5dp" android:requiresFadingEdge="vertical" android:textColor="?attr/textColor" - android:textSize="12sp" + android:textSize="16sp" tools:text="Ryan Quicksave Romano is an eccentric adventurer with a strange power: he can create a save-point in time and redo his life whenever he dies. Arriving in New Rome, the glitzy capital of sin of a rebuilding Europe, he finds the city torn between mega-corporations, sponsored heroes, superpowered criminals, and true monsters. It's a time of chaos, where potions can grant the power to rule the world and dangers lurk everywhere. " /> - + android:layout_height="match_parent"> + + + + Date: Thu, 14 Sep 2023 10:53:35 +0000 Subject: [PATCH 143/156] More robust player release (#601) --- .../ui/player/AbstractPlayerFragment.kt | 1 + .../cloudstream3/ui/player/CS3IPlayer.kt | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 4316bbc6..8388e58f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -517,6 +517,7 @@ abstract class AbstractPlayerFragment( canEnterPipMode = false mMediaSession?.release() mMediaSession = null + playerView?.player = null SubtitlesFragment.applyStyleEvent -= ::onSubStyleChanged keepScreenOn(false) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index fe4e3423..331cfb73 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -51,6 +51,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle @@ -85,6 +86,12 @@ const val toleranceAfterUs = 300_000L class CS3IPlayer : IPlayer { private var isPlaying = false private var exoPlayer: ExoPlayer? = null + set(value) { + // If the old value is not null then the player has not been properly released. + debugAssert({ field != null && value != null }, { "Previous player instance should be released!" }) + field = value + } + var cacheSize = 0L var simpleCacheSize = 0L var videoBufferMs = 0L @@ -682,13 +689,13 @@ class CS3IPlayer : IPlayer { metadataRendererOutput ).map { if (it is TextRenderer) { - currentTextRenderer = CustomTextRenderer( + val currentTextRenderer = CustomTextRenderer( subtitleOffset, textRendererOutput, eventHandler.looper, CustomSubtitleDecoderFactory() - ) - currentTextRenderer!! + ).also { this.currentTextRenderer = it } + currentTextRenderer } else it }.toTypedArray() } @@ -1323,7 +1330,7 @@ class CS3IPlayer : IPlayer { override fun reloadPlayer(context: Context) { Log.i(TAG, "reloadPlayer") - exoPlayer?.release() + releasePlayer(false) currentLink?.let { loadOnlinePlayer(context, it) } ?: currentDownloadedFile?.let { From 2bed79b1f18e7b7ae145377865f427a781197e11 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Thu, 14 Sep 2023 17:53:54 +0700 Subject: [PATCH 144/156] Update Gofile.kt (#600) --- .../main/java/com/lagradost/cloudstream3/extractors/Gofile.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt index d76b0e11..eaf9c65f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt @@ -19,7 +19,7 @@ open class Gofile : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z]+)").find(url)?.groupValues?.get(1) + val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z-]+)").find(url)?.groupValues?.get(1) val token = app.get("$mainApi/createAccount").parsedSafe()?.data?.get("token") val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let { Regex("websiteToken\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1) @@ -59,4 +59,4 @@ open class Gofile : ExtractorApi() { @JsonProperty("data") val data: Data? = null, ) -} \ No newline at end of file +} From 6957a8f95dc7144c5b6c454930950d533c276f95 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 14 Sep 2023 20:30:44 +0200 Subject: [PATCH 145/156] bump --- app/build.gradle.kts | 4 ++-- app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f52d6e5e..66ba16c6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,8 +58,8 @@ android { minSdk = 21 targetSdk = 33 - versionCode = 59 - versionName = "4.1.8" + versionCode = 60 + versionName = "4.1.9" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 0175e0d0..5b674c4c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -179,6 +179,13 @@ object APIHolder { private var trackerCache: HashMap = hashMapOf() + /** backwards compatibility, use getTracker4 instead */ + suspend fun getTracker( + titles: List, + types: Set?, + year: Int?, + ): Tracker? = getTracker(titles, types, year, false) + /** * Get anime tracker information based on title, year and type. * Both titles are attempted to be matched with both Romaji and English title. @@ -192,7 +199,7 @@ object APIHolder { titles: List, types: Set?, year: Int?, - lessAccurate: Boolean = false + lessAccurate: Boolean ): Tracker? { return try { require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" } From 6e89ed9d81453db71d7797df07d18d0ddb7aaa99 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Fri, 15 Sep 2023 16:39:00 +0200 Subject: [PATCH 146/156] Many fixes --- .../lagradost/cloudstream3/MainActivity.kt | 13 +- ...RecyclerView.kt => CustomRecyclerViews.kt} | 39 ++- .../ui/result/ResultFragmentTv.kt | 4 +- .../ui/result/ResultViewModel2.kt | 3 +- app/src/main/res/drawable/episodes_shadow.xml | 6 +- .../main/res/layout/fragment_result_tv.xml | 239 ++++++++++-------- 6 files changed, 184 insertions(+), 120 deletions(-) rename app/src/main/java/com/lagradost/cloudstream3/ui/{AutofitRecyclerView.kt => CustomRecyclerViews.kt} (79%) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a07ae2c2..c57b6c0f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -539,6 +539,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val isTrueTv = isTrueTvSettings() navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + + // Hide downloads on TV + navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv + navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv } } @@ -1112,16 +1116,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> // println("refocus $oldFocus -> $newFocus") try { - val r = Rect(0,0,0,0) + val r = Rect(0, 0, 0, 0) newFocus.getDrawingRect(r) val x = r.centerX() val y = r.centerY() val dx = 0 //screenWidth / 2 val dy = screenHeight / 2 - val r2 = Rect(x-dx,y-dy,x+dx,y+dy) + val r2 = Rect(x - dx, y - dy, x + dx, y + dy) newFocus.requestRectangleOnScreen(r2, false) - // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) - } catch (_ : Throwable) { } + // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) + } catch (_: Throwable) { + } TvFocus.updateFocusView(newFocus) /*var focus = newFocus diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt similarity index 79% rename from app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt rename to app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt index 28ced48c..1a9549e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui import android.content.Context import android.util.AttributeSet import android.view.View +import androidx.core.view.children import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs @@ -70,8 +71,8 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : val orientation = this.orientation // fixes arabic by inverting left and right layout focus - val correctDirection = if(this.isLayoutRTL) { - when(direction) { + val correctDirection = if (this.isLayoutRTL) { + when (direction) { View.FOCUS_RIGHT -> View.FOCUS_LEFT View.FOCUS_LEFT -> View.FOCUS_RIGHT else -> direction @@ -83,12 +84,15 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : View.FOCUS_DOWN -> { return spanCount } + View.FOCUS_UP -> { return -spanCount } + View.FOCUS_RIGHT -> { return 1 } + View.FOCUS_LEFT -> { return -1 } @@ -98,12 +102,15 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : View.FOCUS_DOWN -> { return 1 } + View.FOCUS_UP -> { return -1 } + View.FOCUS_RIGHT -> { return spanCount } + View.FOCUS_LEFT -> { return -spanCount } @@ -155,4 +162,32 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att layoutManager = manager } +} + +/** + * Recyclerview wherein the max item width or height is set by the biggest view to prevent inconsistent view sizes. + */ +class MaxRecyclerView(ctx: Context, attrs: AttributeSet) : RecyclerView(ctx, attrs) { + private var biggestObserved: Int = 0 + private val orientation = LayoutManager.getProperties(context, attrs, 0, 0).orientation + private val isHorizontal = orientation == HORIZONTAL + private fun View.updateMaxSize() { + if (isHorizontal) { + this.minimumHeight = biggestObserved + } else { + this.minimumWidth = biggestObserved + } + } + + override fun onChildAttachedToWindow(child: View) { + child.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + val observed = if (isHorizontal) child.measuredHeight else child.measuredWidth + if (observed > biggestObserved) { + biggestObserved = observed + children.forEach { it.updateMaxSize() } + } else { + child.updateMaxSize() + } + super.onChildAttachedToWindow(child) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index be3de52b..c40d995b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -177,7 +177,7 @@ class ResultFragmentTv : Fragment() { isVisible = true } - this.animate().alpha(if (turnVisible) 1.0f else 0.0f).apply { + this.animate().alpha(if (turnVisible) 0.97f else 0.0f).apply { duration = 200 interpolator = DecelerateInterpolator() setListener(object : Animator.AnimatorListener { @@ -294,9 +294,9 @@ class ResultFragmentTv : Fragment() { toggleEpisodes(true) binding?.apply { val views = listOf( + resultDubSelection, resultSeasonSelection, resultRangeSelection, - resultDubSelection, resultEpisodes, resultPlayTrailer, ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index b398b54e..6acf476a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -518,7 +518,8 @@ class ResultViewModel2 : ViewModel() { val episodeNumber = episodes[currentIndex].episode if (episodeNumber < currentMin) { currentMin = episodeNumber - } else if (episodeNumber > currentMax) { + } + if (episodeNumber > currentMax) { currentMax = episodeNumber } ++currentIndex diff --git a/app/src/main/res/drawable/episodes_shadow.xml b/app/src/main/res/drawable/episodes_shadow.xml index b4cdd382..a77cbf25 100644 --- a/app/src/main/res/drawable/episodes_shadow.xml +++ b/app/src/main/res/drawable/episodes_shadow.xml @@ -1,6 +1,8 @@ + android:centerColor="?attr/primaryBlackBackground" + android:centerX="0.2" + android:endColor="?attr/primaryBlackBackground" + android:startColor="@color/transparent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 4d236d78..a143fbda 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -535,129 +535,150 @@ https://developer.android.com/design/ui/tv/samples/jet-fit - - - - - - - - - - - - - + tools:visibility="visible"> - + - style="@style/Widget.AppCompat.ProgressBar" - android:layout_gravity="center" - android:layout_width="50dp" - android:layout_height="50dp" />--> + + - - - - + android:focusableInTouchMode="false" + android:importantForAccessibility="no" + android:src="@drawable/episodes_shadow" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/shadow_space_2" + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + + + + + + + + + - Date: Fri, 15 Sep 2023 16:41:13 +0200 Subject: [PATCH 147/156] Revert targetSdk --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7bcae0f4..66ba16c6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -56,7 +56,7 @@ android { defaultConfig { applicationId = "com.lagradost.cloudstream3" minSdk = 21 - targetSdk = 29 + targetSdk = 33 versionCode = 60 versionName = "4.1.9" From 65c927496d95f82e59cc268103f1aaa3524a3d8f Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Fri, 15 Sep 2023 17:51:15 +0200 Subject: [PATCH 148/156] Make account homepage persistent --- .../lagradost/cloudstream3/MainActivity.kt | 12 ++++++- .../cloudstream3/ui/home/HomeFragment.kt | 3 +- .../cloudstream3/ui/home/HomeViewModel.kt | 17 ++++++---- .../ui/settings/SettingsProviders.kt | 4 +-- .../ui/setup/SetupFragmentMedia.kt | 4 +-- .../cloudstream3/utils/DataStoreHelper.kt | 33 ++++++++++++++++--- 6 files changed, 55 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index c57b6c0f..7e29e727 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -128,6 +128,7 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.setKey +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.IOnBackPressed @@ -305,6 +306,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // kinda shitty solution, but cant com main->home otherwise for popups val bookmarksUpdatedEvent = Event() + /** + * Used by data store helper to fully reload home when switching accounts + */ + val reloadHomeEvent = Event() /** @@ -1187,7 +1192,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } else if (lastError == null) { ioSafe { - getKey(USER_SELECTED_HOMEPAGE_API)?.let { homeApi -> + DataStoreHelper.currentHomePage?.let { homeApi -> mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi)) } ?: run { mainPluginsLoadedEvent.invoke(false) @@ -1548,6 +1553,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { migrateResumeWatching() } + getKey(USER_SELECTED_HOMEPAGE_API)?.let { homepage -> + DataStoreHelper.currentHomePage = homepage + removeKey(USER_SELECTED_HOMEPAGE_API) + } + try { if (getKey(HAS_DONE_SETUP_KEY, false) != true) { navController.navigate(R.id.navigation_setup_language) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index b84c619e..0797e9a0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -69,7 +69,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import java.util.* @@ -669,7 +668,7 @@ class HomeFragment : Fragment() { } homeViewModel.reloadStored() - homeViewModel.loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false) + homeViewModel.loadAndCancel(DataStoreHelper.currentHomePage, false) //loadHomePage(false) // nice profile pic on homepage diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index b27223ec..13d34b59 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -49,7 +49,6 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import com.lagradost.cloudstream3.utils.VideoDownloadHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -426,23 +425,29 @@ class HomeViewModel : ViewModel() { } private fun afterPluginsLoaded(forceReload: Boolean) { - loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), forceReload) + loadAndCancel(DataStoreHelper.currentHomePage, forceReload) } private fun afterMainPluginsLoaded(unused: Boolean = false) { - loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false) + loadAndCancel(DataStoreHelper.currentHomePage, false) + } + + private fun reloadHome(unused: Boolean = false) { + loadAndCancel(DataStoreHelper.currentHomePage, true) } init { MainActivity.bookmarksUpdatedEvent += ::bookmarksUpdated MainActivity.afterPluginsLoadedEvent += ::afterPluginsLoaded MainActivity.mainPluginsLoadedEvent += ::afterMainPluginsLoaded + MainActivity.reloadHomeEvent += ::reloadHome } override fun onCleared() { MainActivity.bookmarksUpdatedEvent -= ::bookmarksUpdated MainActivity.afterPluginsLoadedEvent -= ::afterPluginsLoaded MainActivity.mainPluginsLoadedEvent -= ::afterMainPluginsLoaded + MainActivity.reloadHomeEvent -= ::reloadHome super.onCleared() } @@ -495,7 +500,7 @@ class HomeViewModel : ViewModel() { val api = getApiFromNameNull(preferredApiName) if (preferredApiName == noneApi.name) { // just set to random - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) + if (fromUI) DataStoreHelper.currentHomePage = noneApi.name loadAndCancel(noneApi) } else if (preferredApiName == randomApi.name) { // randomize the api, if none exist like if not loaded or not installed @@ -506,7 +511,7 @@ class HomeViewModel : ViewModel() { } else { val apiRandom = validAPIs.random() loadAndCancel(apiRandom) - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name) + if (fromUI) DataStoreHelper.currentHomePage = apiRandom.name } } else if (api == null) { // API is not found aka not loaded or removed, post the loading @@ -520,7 +525,7 @@ class HomeViewModel : ViewModel() { } } else { // if the api is found, then set it to it and save key - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, api.name) + if (fromUI) DataStoreHelper.currentHomePage = api.name loadAndCancel(api) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index 0bef5e9a..7e57fc5b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -14,7 +14,7 @@ import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard @@ -96,7 +96,7 @@ class SettingsProviders : PreferenceFragmentCompat() { this.getString(R.string.prefer_media_type_key), selectedList.map { it.toString() }.toMutableSet() ).apply() - removeKey(USER_SELECTED_HOMEPAGE_API) + DataStoreHelper.currentHomePage = null //(context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt index 6916cafe..f9197213 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt @@ -15,8 +15,8 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API class SetupFragmentMedia : Fragment() { @@ -77,7 +77,7 @@ class SetupFragmentMedia : Fragment() { .apply() // Regenerate set homepage - removeKey(USER_SELECTED_HOMEPAGE_API) + DataStoreHelper.currentHomePage = null } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 2eb2ab01..7bce1b6c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -77,10 +77,28 @@ object DataStoreHelper { var selectedKeyIndex by PreferenceDelegate("$TAG/account_key_index", 0) val currentAccount: String get() = selectedKeyIndex.toString() - private fun setAccount(account: Account) { + /** + * Get or set the current account homepage. + * Setting this does not automatically reload the homepage. + */ + var currentHomePage: String? + get() = getKey("$currentAccount/$USER_SELECTED_HOMEPAGE_API") + set(value) { + val key = "$currentAccount/$USER_SELECTED_HOMEPAGE_API" + if (value == null) { + removeKey(key) + } else { + setKey(key, value) + } + } + + private fun setAccount(account: Account, refreshHomePage: Boolean) { selectedKeyIndex = account.keyIndex showToast(account.name) MainActivity.bookmarksUpdatedEvent(true) + if (refreshHomePage) { + MainActivity.reloadHomeEvent(true) + } } private fun editAccount(context: Context, account: Account, isNewAccount: Boolean) { @@ -112,7 +130,7 @@ object DataStoreHelper { accounts = currentAccounts.toTypedArray() // update UI - setAccount(getDefaultAccount(context)) + setAccount(getDefaultAccount(context), true) MainActivity.bookmarksUpdatedEvent(true) dialog?.dismissSafe() } @@ -161,8 +179,13 @@ object DataStoreHelper { currentAccounts.add(currentEditAccount) } + // Save the current homepage for new accounts + val currentHomePage = DataStoreHelper.currentHomePage + // set the new default account as well as add the key for the new account - setAccount(currentEditAccount) + setAccount(currentEditAccount, false) + DataStoreHelper.currentHomePage = currentHomePage + accounts = currentAccounts.toTypedArray() dialog.dismissSafe() @@ -204,7 +227,7 @@ object DataStoreHelper { ) binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter( selectCallBack = { account -> - setAccount(account) + setAccount(account, true) builder.dismissSafe() }, addAccountCallback = { @@ -353,7 +376,7 @@ object DataStoreHelper { removeKeys(folder2) } - fun deleteBookmarkedData(id : Int?) { + fun deleteBookmarkedData(id: Int?) { if (id == null) return removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString()) removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) From 24977a8d628a3418051e9bbbead8d4a88f7df035 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Fri, 15 Sep 2023 20:47:59 +0000 Subject: [PATCH 149/156] Potential fix for crash loops (#608) --- .../com/lagradost/cloudstream3/MainActivity.kt | 12 ++++++++---- .../cloudstream3/plugins/PluginManager.kt | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a07ae2c2..82a52a2a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1092,15 +1092,19 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { updateTv() // backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting? - try { + normalSafeApiCall { val appVer = BuildConfig.VERSION_NAME val lastAppAutoBackup: String = getKey("VERSION_NAME") ?: "" if (appVer != lastAppAutoBackup) { setKey("VERSION_NAME", BuildConfig.VERSION_NAME) - backup() + normalSafeApiCall { + backup() + } + normalSafeApiCall { + // Recompile oat on new version + PluginManager.deleteAllOatFiles(this) + } } - } catch (t: Throwable) { - logError(t) } // just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index 87b0ba3b..5bb96ed1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -137,6 +137,20 @@ object PluginManager { } } + /** + * Deletes all generated oat files which will force Android to recompile the dex extensions. + * This might fix unrecoverable SIGSEGV exceptions when old oat files are loaded in a new app update. + */ + fun deleteAllOatFiles(context: Context) { + File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}").listFiles()?.forEach { repo -> + repo.listFiles { file -> file.name == "oat" && file.isDirectory }?.forEach { file -> + val success = file.deleteRecursively() + Log.i(TAG, "Deleted oat directory: ${file.absolutePath} Success=$success") + } + } + } + + fun getPluginsOnline(): Array { return getKey(PLUGINS_KEY) ?: emptyArray() } From 627c1bb223a3cc4b8a917807e91100211f30a859 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Fri, 15 Sep 2023 22:30:34 +0000 Subject: [PATCH 150/156] Many UI fixes (#606) * Lower targetSdk to get all installed packages * Update sdk version * Let's not be too radical * Many fixes * Revert targetSdk * Make account homepage persistent --- .../lagradost/cloudstream3/MainActivity.kt | 25 +- ...RecyclerView.kt => CustomRecyclerViews.kt} | 39 ++- .../cloudstream3/ui/home/HomeFragment.kt | 3 +- .../cloudstream3/ui/home/HomeViewModel.kt | 17 +- .../ui/result/ResultFragmentTv.kt | 4 +- .../ui/result/ResultViewModel2.kt | 3 +- .../ui/settings/SettingsProviders.kt | 4 +- .../ui/setup/SetupFragmentMedia.kt | 4 +- .../cloudstream3/utils/DataStoreHelper.kt | 33 ++- app/src/main/res/drawable/episodes_shadow.xml | 6 +- .../main/res/layout/fragment_result_tv.xml | 239 ++++++++++-------- 11 files changed, 239 insertions(+), 138 deletions(-) rename app/src/main/java/com/lagradost/cloudstream3/ui/{AutofitRecyclerView.kt => CustomRecyclerViews.kt} (79%) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 82a52a2a..263a40f0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -128,6 +128,7 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.setKey +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.IOnBackPressed @@ -305,6 +306,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // kinda shitty solution, but cant com main->home otherwise for popups val bookmarksUpdatedEvent = Event() + /** + * Used by data store helper to fully reload home when switching accounts + */ + val reloadHomeEvent = Event() /** @@ -539,6 +544,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val isTrueTv = isTrueTvSettings() navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + + // Hide downloads on TV + navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv + navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv } } @@ -1116,16 +1125,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> // println("refocus $oldFocus -> $newFocus") try { - val r = Rect(0,0,0,0) + val r = Rect(0, 0, 0, 0) newFocus.getDrawingRect(r) val x = r.centerX() val y = r.centerY() val dx = 0 //screenWidth / 2 val dy = screenHeight / 2 - val r2 = Rect(x-dx,y-dy,x+dx,y+dy) + val r2 = Rect(x - dx, y - dy, x + dx, y + dy) newFocus.requestRectangleOnScreen(r2, false) - // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) - } catch (_ : Throwable) { } + // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) + } catch (_: Throwable) { + } TvFocus.updateFocusView(newFocus) /*var focus = newFocus @@ -1186,7 +1196,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } else if (lastError == null) { ioSafe { - getKey(USER_SELECTED_HOMEPAGE_API)?.let { homeApi -> + DataStoreHelper.currentHomePage?.let { homeApi -> mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi)) } ?: run { mainPluginsLoadedEvent.invoke(false) @@ -1547,6 +1557,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { migrateResumeWatching() } + getKey(USER_SELECTED_HOMEPAGE_API)?.let { homepage -> + DataStoreHelper.currentHomePage = homepage + removeKey(USER_SELECTED_HOMEPAGE_API) + } + try { if (getKey(HAS_DONE_SETUP_KEY, false) != true) { navController.navigate(R.id.navigation_setup_language) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt similarity index 79% rename from app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt rename to app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt index 28ced48c..1a9549e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui import android.content.Context import android.util.AttributeSet import android.view.View +import androidx.core.view.children import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs @@ -70,8 +71,8 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : val orientation = this.orientation // fixes arabic by inverting left and right layout focus - val correctDirection = if(this.isLayoutRTL) { - when(direction) { + val correctDirection = if (this.isLayoutRTL) { + when (direction) { View.FOCUS_RIGHT -> View.FOCUS_LEFT View.FOCUS_LEFT -> View.FOCUS_RIGHT else -> direction @@ -83,12 +84,15 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : View.FOCUS_DOWN -> { return spanCount } + View.FOCUS_UP -> { return -spanCount } + View.FOCUS_RIGHT -> { return 1 } + View.FOCUS_LEFT -> { return -1 } @@ -98,12 +102,15 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : View.FOCUS_DOWN -> { return 1 } + View.FOCUS_UP -> { return -1 } + View.FOCUS_RIGHT -> { return spanCount } + View.FOCUS_LEFT -> { return -spanCount } @@ -155,4 +162,32 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att layoutManager = manager } +} + +/** + * Recyclerview wherein the max item width or height is set by the biggest view to prevent inconsistent view sizes. + */ +class MaxRecyclerView(ctx: Context, attrs: AttributeSet) : RecyclerView(ctx, attrs) { + private var biggestObserved: Int = 0 + private val orientation = LayoutManager.getProperties(context, attrs, 0, 0).orientation + private val isHorizontal = orientation == HORIZONTAL + private fun View.updateMaxSize() { + if (isHorizontal) { + this.minimumHeight = biggestObserved + } else { + this.minimumWidth = biggestObserved + } + } + + override fun onChildAttachedToWindow(child: View) { + child.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + val observed = if (isHorizontal) child.measuredHeight else child.measuredWidth + if (observed > biggestObserved) { + biggestObserved = observed + children.forEach { it.updateMaxSize() } + } else { + child.updateMaxSize() + } + super.onChildAttachedToWindow(child) + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index b84c619e..0797e9a0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -69,7 +69,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import java.util.* @@ -669,7 +668,7 @@ class HomeFragment : Fragment() { } homeViewModel.reloadStored() - homeViewModel.loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false) + homeViewModel.loadAndCancel(DataStoreHelper.currentHomePage, false) //loadHomePage(false) // nice profile pic on homepage diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index b27223ec..13d34b59 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -49,7 +49,6 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import com.lagradost.cloudstream3.utils.VideoDownloadHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -426,23 +425,29 @@ class HomeViewModel : ViewModel() { } private fun afterPluginsLoaded(forceReload: Boolean) { - loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), forceReload) + loadAndCancel(DataStoreHelper.currentHomePage, forceReload) } private fun afterMainPluginsLoaded(unused: Boolean = false) { - loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false) + loadAndCancel(DataStoreHelper.currentHomePage, false) + } + + private fun reloadHome(unused: Boolean = false) { + loadAndCancel(DataStoreHelper.currentHomePage, true) } init { MainActivity.bookmarksUpdatedEvent += ::bookmarksUpdated MainActivity.afterPluginsLoadedEvent += ::afterPluginsLoaded MainActivity.mainPluginsLoadedEvent += ::afterMainPluginsLoaded + MainActivity.reloadHomeEvent += ::reloadHome } override fun onCleared() { MainActivity.bookmarksUpdatedEvent -= ::bookmarksUpdated MainActivity.afterPluginsLoadedEvent -= ::afterPluginsLoaded MainActivity.mainPluginsLoadedEvent -= ::afterMainPluginsLoaded + MainActivity.reloadHomeEvent -= ::reloadHome super.onCleared() } @@ -495,7 +500,7 @@ class HomeViewModel : ViewModel() { val api = getApiFromNameNull(preferredApiName) if (preferredApiName == noneApi.name) { // just set to random - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) + if (fromUI) DataStoreHelper.currentHomePage = noneApi.name loadAndCancel(noneApi) } else if (preferredApiName == randomApi.name) { // randomize the api, if none exist like if not loaded or not installed @@ -506,7 +511,7 @@ class HomeViewModel : ViewModel() { } else { val apiRandom = validAPIs.random() loadAndCancel(apiRandom) - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name) + if (fromUI) DataStoreHelper.currentHomePage = apiRandom.name } } else if (api == null) { // API is not found aka not loaded or removed, post the loading @@ -520,7 +525,7 @@ class HomeViewModel : ViewModel() { } } else { // if the api is found, then set it to it and save key - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, api.name) + if (fromUI) DataStoreHelper.currentHomePage = api.name loadAndCancel(api) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index be3de52b..c40d995b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -177,7 +177,7 @@ class ResultFragmentTv : Fragment() { isVisible = true } - this.animate().alpha(if (turnVisible) 1.0f else 0.0f).apply { + this.animate().alpha(if (turnVisible) 0.97f else 0.0f).apply { duration = 200 interpolator = DecelerateInterpolator() setListener(object : Animator.AnimatorListener { @@ -294,9 +294,9 @@ class ResultFragmentTv : Fragment() { toggleEpisodes(true) binding?.apply { val views = listOf( + resultDubSelection, resultSeasonSelection, resultRangeSelection, - resultDubSelection, resultEpisodes, resultPlayTrailer, ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index b398b54e..6acf476a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -518,7 +518,8 @@ class ResultViewModel2 : ViewModel() { val episodeNumber = episodes[currentIndex].episode if (episodeNumber < currentMin) { currentMin = episodeNumber - } else if (episodeNumber > currentMax) { + } + if (episodeNumber > currentMax) { currentMax = episodeNumber } ++currentIndex diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index 0bef5e9a..7e57fc5b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -14,7 +14,7 @@ import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard @@ -96,7 +96,7 @@ class SettingsProviders : PreferenceFragmentCompat() { this.getString(R.string.prefer_media_type_key), selectedList.map { it.toString() }.toMutableSet() ).apply() - removeKey(USER_SELECTED_HOMEPAGE_API) + DataStoreHelper.currentHomePage = null //(context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt index 6916cafe..f9197213 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt @@ -15,8 +15,8 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API class SetupFragmentMedia : Fragment() { @@ -77,7 +77,7 @@ class SetupFragmentMedia : Fragment() { .apply() // Regenerate set homepage - removeKey(USER_SELECTED_HOMEPAGE_API) + DataStoreHelper.currentHomePage = null } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 2eb2ab01..7bce1b6c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -77,10 +77,28 @@ object DataStoreHelper { var selectedKeyIndex by PreferenceDelegate("$TAG/account_key_index", 0) val currentAccount: String get() = selectedKeyIndex.toString() - private fun setAccount(account: Account) { + /** + * Get or set the current account homepage. + * Setting this does not automatically reload the homepage. + */ + var currentHomePage: String? + get() = getKey("$currentAccount/$USER_SELECTED_HOMEPAGE_API") + set(value) { + val key = "$currentAccount/$USER_SELECTED_HOMEPAGE_API" + if (value == null) { + removeKey(key) + } else { + setKey(key, value) + } + } + + private fun setAccount(account: Account, refreshHomePage: Boolean) { selectedKeyIndex = account.keyIndex showToast(account.name) MainActivity.bookmarksUpdatedEvent(true) + if (refreshHomePage) { + MainActivity.reloadHomeEvent(true) + } } private fun editAccount(context: Context, account: Account, isNewAccount: Boolean) { @@ -112,7 +130,7 @@ object DataStoreHelper { accounts = currentAccounts.toTypedArray() // update UI - setAccount(getDefaultAccount(context)) + setAccount(getDefaultAccount(context), true) MainActivity.bookmarksUpdatedEvent(true) dialog?.dismissSafe() } @@ -161,8 +179,13 @@ object DataStoreHelper { currentAccounts.add(currentEditAccount) } + // Save the current homepage for new accounts + val currentHomePage = DataStoreHelper.currentHomePage + // set the new default account as well as add the key for the new account - setAccount(currentEditAccount) + setAccount(currentEditAccount, false) + DataStoreHelper.currentHomePage = currentHomePage + accounts = currentAccounts.toTypedArray() dialog.dismissSafe() @@ -204,7 +227,7 @@ object DataStoreHelper { ) binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter( selectCallBack = { account -> - setAccount(account) + setAccount(account, true) builder.dismissSafe() }, addAccountCallback = { @@ -353,7 +376,7 @@ object DataStoreHelper { removeKeys(folder2) } - fun deleteBookmarkedData(id : Int?) { + fun deleteBookmarkedData(id: Int?) { if (id == null) return removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString()) removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) diff --git a/app/src/main/res/drawable/episodes_shadow.xml b/app/src/main/res/drawable/episodes_shadow.xml index b4cdd382..a77cbf25 100644 --- a/app/src/main/res/drawable/episodes_shadow.xml +++ b/app/src/main/res/drawable/episodes_shadow.xml @@ -1,6 +1,8 @@ + android:centerColor="?attr/primaryBlackBackground" + android:centerX="0.2" + android:endColor="?attr/primaryBlackBackground" + android:startColor="@color/transparent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 4d236d78..a143fbda 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -535,129 +535,150 @@ https://developer.android.com/design/ui/tv/samples/jet-fit - - - - - - - - - - - - - + tools:visibility="visible"> - + - style="@style/Widget.AppCompat.ProgressBar" - android:layout_gravity="center" - android:layout_width="50dp" - android:layout_height="50dp" />--> + + - - - - + android:focusableInTouchMode="false" + android:importantForAccessibility="no" + android:src="@drawable/episodes_shadow" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/shadow_space_2" + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + + + + + + + + + - Date: Sun, 17 Sep 2023 20:35:01 +0200 Subject: [PATCH 151/156] fix --- .../ui/player/PlayerGeneratorViewModel.kt | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 42659f8d..3179cb9f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.ui.result.ResultEpisode @@ -15,6 +16,7 @@ import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorUri import kotlinx.coroutines.Job +import kotlinx.coroutines.launch class PlayerGeneratorViewModel : ViewModel() { companion object { @@ -38,6 +40,11 @@ class PlayerGeneratorViewModel : ViewModel() { private val _currentSubtitleYear = MutableLiveData(null) val currentSubtitleYear: LiveData = _currentSubtitleYear + /** + * Save the Episode ID to prevent starting multiple link loading Jobs when preloading links. + */ + private var currentLoadingEpisodeId: Int? = null + fun setSubtitleYear(year: Int?) { _currentSubtitleYear.postValue(year) } @@ -72,18 +79,32 @@ class PlayerGeneratorViewModel : ViewModel() { } fun preLoadNextLinks() { + val id = getId() + // Do not preload if already loading + if (id == currentLoadingEpisodeId) return + Log.i(TAG, "preLoadNextLinks") currentJob?.cancel() - currentJob = viewModelScope.launchSafe { - if (generator?.hasCache == true && generator?.hasNext() == true) { - safeApiCall { - generator?.generateLinks( - type = LoadType.InApp, - clearCache = false, - callback = {}, - subtitleCallback = {}, - offset = 1 - ) + currentLoadingEpisodeId = id + + currentJob = viewModelScope.launch { + try { + if (generator?.hasCache == true && generator?.hasNext() == true) { + safeApiCall { + generator?.generateLinks( + type = LoadType.InApp, + clearCache = false, + callback = {}, + subtitleCallback = {}, + offset = 1 + ) + } + } + } catch (t: Throwable) { + logError(t) + } finally { + if (currentLoadingEpisodeId == id) { + currentLoadingEpisodeId = null } } } @@ -162,14 +183,14 @@ class PlayerGeneratorViewModel : ViewModel() { // load more data _loadingLinks.postValue(Resource.Loading()) val loadingState = safeApiCall { - generator?.generateLinks(type = type,clearCache = clearCache, callback = { + generator?.generateLinks(type = type, clearCache = clearCache, callback = { currentLinks.add(it) // Clone to prevent ConcurrentModificationException normalSafeApiCall { // Extra normalSafeApiCall since .toSet() iterates. _currentLinks.postValue(currentLinks.toSet()) } - }, subtitleCallback = { + }, subtitleCallback = { currentSubs.add(it) normalSafeApiCall { _currentSubs.postValue(currentSubs.toSet()) From 0d2a19b350021427922d7419f485ef1c46550366 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 17 Sep 2023 20:38:59 +0200 Subject: [PATCH 152/156] bump --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 66ba16c6..ca99894d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,8 +58,8 @@ android { minSdk = 21 targetSdk = 33 - versionCode = 60 - versionName = "4.1.9" + versionCode = 61 + versionName = "4.1.10" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") From 2ae5b6cefbaa85fffaa12838a87d5623e15e73ca Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 18 Sep 2023 22:28:26 +0200 Subject: [PATCH 153/156] fixed the fucking updater :skull: --- app/build.gradle.kts | 4 ++-- .../lagradost/cloudstream3/utils/InAppUpdater.kt | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ca99894d..0f815f8b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,8 +58,8 @@ android { minSdk = 21 targetSdk = 33 - versionCode = 61 - versionName = "4.1.10" + versionCode = 62 + versionName = "4.2.0" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index b2c4aa5c..0dce0b2a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -109,18 +109,19 @@ class InAppUpdater { releases.sortedWith(compareBy { versionRegex.find(it.name)?.groupValues?.get(2) }).toList().lastOrNull()*/ - val found = + val foundList = response.filter { rel -> !rel.prerelease }.sortedWith(compareBy { release -> - release.assets.filter { it.content_type == "application/vnd.android.package-archive" } - .getOrNull(0)?.name?.let { it1 -> + release.assets.firstOrNull { it.content_type == "application/vnd.android.package-archive" }?.name?.let { it1 -> versionRegex.find( it1 - )?.groupValues?.get(2) + )?.groupValues?.let { + it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt() + } } - }).toList().lastOrNull() - + }).toList() + val found = foundList.lastOrNull() val foundAsset = found?.assets?.getOrNull(0) val currentVersion = packageName?.let { packageManager.getPackageInfo( From 15333123cd298357baf85fa2c5f044ada60481d6 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Mon, 18 Sep 2023 21:22:39 +0000 Subject: [PATCH 154/156] TV UI fixes (#612) * TV UI fixes --- .../cloudstream3/ui/result/ActorAdaptor.kt | 26 +++++++++++--- .../ui/result/ResultFragmentTv.kt | 35 +++++++++++++------ .../ui/settings/SettingsFragment.kt | 5 +++ .../main/res/layout/fragment_search_tv.xml | 2 +- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index 531cb5d2..7b743388 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -3,6 +3,8 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.IdRes +import androidx.annotation.LayoutRes import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -12,7 +14,10 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.CastItemBinding import com.lagradost.cloudstream3.utils.UIHelper.setImage -class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerView.Adapter() { +class ActorAdaptor( + private var nextFocusUpId: Int? = null, + private val focusCallback: (View?) -> Unit = {} +) : RecyclerView.Adapter() { data class ActorMetaData( var isInverted: Boolean, val actor: ActorData, @@ -22,7 +27,8 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return CardViewHolder( - CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), focusCallback + CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), + focusCallback ) } @@ -64,10 +70,10 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV } } - private class CardViewHolder + private inner class CardViewHolder constructor( val binding: CastItemBinding, - private val focusCallback : (View?) -> Unit = {} + private val focusCallback: (View?) -> Unit = {} ) : RecyclerView.ViewHolder(binding.root) { @@ -78,8 +84,18 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV Pair(actor.voiceActor?.image, actor.actor.image) } + // Fix tv focus escaping the recyclerview + if (position == 0) { + itemView.nextFocusLeftId = R.id.result_cast_items + } else if ((position - 1) == itemCount) { + itemView.nextFocusRightId = R.id.result_cast_items + } + nextFocusUpId?.let { + itemView.nextFocusUpId = it + } + itemView.setOnFocusChangeListener { v, hasFocus -> - if(hasFocus) { + if (hasFocus) { focusCallback(v) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index c40d995b..4503cb88 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -114,10 +114,20 @@ class ResultFragmentTv : Fragment() { } } - private fun hasNoFocus(): Boolean { - val focus = activity?.currentFocus - if (focus == null || !focus.isVisible) return true - return focus == binding?.resultRoot +// private fun hasNoFocus(): Boolean { +// val focus = activity?.currentFocus +// if (focus == null || !focus.isVisible) return true +// return focus == binding?.resultRoot +// } + + /** + * Force focus any play button. + * Note that this will steal any focus if the episode loading is too slow (unlikely). + */ + private fun focusPlayButton() { + binding?.resultPlayMovie?.requestFocus() + binding?.resultPlaySeries?.requestFocus() + binding?.resultResumeSeries?.requestFocus() } private fun setRecommendations(rec: List?, validApiName: String?) { @@ -413,7 +423,13 @@ class ResultFragmentTv : Fragment() { setHorizontal() } - resultCastItems.adapter = ActorAdaptor { + val aboveCast = listOf( + binding?.resultEpisodesShow, + binding?.resultBookmarkButton, + ).firstOrNull { + it?.isVisible == true + } + resultCastItems.adapter = ActorAdaptor(aboveCast?.id) { toggleEpisodes(false) } } @@ -454,9 +470,7 @@ class ResultFragmentTv : Fragment() { resultPlaySeries.isVisible = false resultResumeSeries.isVisible = true - if (hasNoFocus()) { - resultResumeSeries.requestFocus() - } + focusPlayButton() resultResumeSeries.text = if (resume.isMovie) context?.getString(R.string.play_movie_button) else context?.getNameFull( @@ -539,9 +553,7 @@ class ResultFragmentTv : Fragment() { ) return@setOnLongClickListener true } - if (hasNoFocus()) { - resultPlayMovie.requestFocus() - } + focusPlayButton() } } } @@ -663,6 +675,7 @@ class ResultFragmentTv : Fragment() { ) return@setOnLongClickListener true } + focusPlayButton() } /* diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index e53fa91a..4895b0d2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -205,6 +205,11 @@ class SettingsFragment : Fragment() { } } } + + // Default focus on TV + if (isTrueTv) { + settingsGeneral.requestFocus() + } } } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search_tv.xml b/app/src/main/res/layout/fragment_search_tv.xml index 4c4af404..5fec8c6a 100644 --- a/app/src/main/res/layout/fragment_search_tv.xml +++ b/app/src/main/res/layout/fragment_search_tv.xml @@ -99,7 +99,7 @@ android:id="@+id/search_autofit_results" android:layout_width="match_parent" android:layout_height="match_parent" - + android:layout_marginStart="@dimen/navbar_width" android:background="?attr/primaryBlackBackground" android:clipToPadding="false" android:descendantFocusability="afterDescendants" From 527a6388a96a4fa961e1544e6b54acdf6a6f5ee5 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 21 Sep 2023 22:46:23 +0200 Subject: [PATCH 155/156] small fix --- .../lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index 4503cb88..7c784a43 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -648,6 +648,9 @@ class ResultFragmentTv : Fragment() { .show() } } + + // Used to request focus the first time the episodes are loaded. + var hasLoadedEpisodesOnce = false observeNullable(viewModel.episodes) { episodes -> binding?.apply { resultEpisodes.isVisible = episodes is Resource.Success @@ -675,7 +678,10 @@ class ResultFragmentTv : Fragment() { ) return@setOnLongClickListener true } - focusPlayButton() + if (!hasLoadedEpisodesOnce) { + hasLoadedEpisodesOnce = true + focusPlayButton() + } } /* From d4fff7cee67d8f7b5e610caf98c5d1816cabac83 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Thu, 21 Sep 2023 20:50:31 +0000 Subject: [PATCH 156/156] Add homepage search on TV (#618) * Add search button on homepage for TV --- .../lagradost/cloudstream3/MainActivity.kt | 1 + .../ui/home/HomeParentItemAdapterPreview.kt | 5 ++++ .../ui/quicksearch/QuickSearchFragment.kt | 5 ++++ app/src/main/res/layout/fragment_home.xml | 16 +++++++++++++ .../main/res/layout/fragment_home_head_tv.xml | 20 ++++++++++++++-- app/src/main/res/layout/fragment_home_tv.xml | 24 +++++++++++++++++-- 6 files changed, 67 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 263a40f0..627893c3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -497,6 +497,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { R.id.navigation_results_phone, R.id.navigation_results_tv, R.id.navigation_player, + R.id.navigation_quick_search, ).contains(destination.id) binding?.navHostFragment?.apply { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 1d8e1399..d7956f39 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -461,6 +461,11 @@ class HomeParentItemAdapterPreview( } } + homePreviewSearchButton.setOnClickListener { _ -> + // Open blank screen. + viewModel.queryTextSubmit("") + } + // This makes the hidden next buttons only available when on the info button // Otherwise you might be able to go to the next item without being at the info button homePreviewInfoBtt.setOnFocusChangeListener { _, hasFocus -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt index 89a09ae2..53c7c2fa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt @@ -33,6 +33,7 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchViewModel +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar @@ -269,6 +270,10 @@ class QuickSearchFragment : Fragment() { activity?.popCurrentPage() } + if (isTrueTvSettings()) { + binding?.quickSearch?.requestFocus() + } + arguments?.getString(AUTOSEARCH_KEY)?.let { binding?.quickSearch?.setQuery(it, true) arguments?.remove(AUTOSEARCH_KEY) diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index ac660ccd..36cb5f42 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -114,6 +114,22 @@ android:nextFocusRight="@id/home_switch_account" /> + + + + + android:nextFocusRight="@id/home_preview_search_button" + android:nextFocusDown="@id/home_preview_play_btt" /> + +