mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-26 09:45:21 +02:00
Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44eacc2a47 | |||
| 8135d61398 | |||
| 66208f8265 | |||
| f52251e23a | |||
| dbea93efe5 | |||
| 3bf0740bd1 | |||
| fa7f1b11f3 | |||
| ff914bbdf4 | |||
| b822078d4b | |||
| 290d2ceb50 | |||
| 8ec9025990 | |||
| c4cf856dcd | |||
| 38bb4e25d3 | |||
| 0de996d91c | |||
| 1f38c9b27d | |||
| 234f31b02d | |||
| 00e40e8cd6 | |||
| 0bc6a43dc1 | |||
| e7e0157fbc | |||
| 4cae1a41a5 | |||
| 4fa61e7f52 | |||
| f02ac796f5 | |||
| 22146a6bdc | |||
| 5285eae01d | |||
| c47ca369e4 | |||
| f0b1f62bb1 | |||
| f7aa6d006e | |||
| 6b67cd549f | |||
| fc6bf85822 | |||
| fbd9345cf8 | |||
| 63137b4c4d | |||
| e28dc7a3a6 | |||
| 6e14acc685 | |||
| ba64153f1d | |||
| 72c04e7556 | |||
| 54f37ee5b2 | |||
| 4fbb325313 | |||
| e1d3b95f73 | |||
| 8f7b4b8257 | |||
| 9d906025ea | |||
| d7f4dd65e8 | |||
| 599b119e62 | |||
| 41176464db | |||
| dd0ad19fb9 | |||
| 430625d2fb | |||
| 796cd1a776 | |||
| baa26af0c0 | |||
| ea0c27936e | |||
| 4aade35d19 | |||
| 251a5701af | |||
| 2da3116111 | |||
| 4c82fa1a4a | |||
| 7eef6eece2 | |||
| 570f32e980 | |||
| 16a0351125 | |||
| 2fa9005806 | |||
| 25527997fa | |||
| 4655d8369d | |||
| aeaaace3a4 | |||
| e6997004ff | |||
| 5e1896b7f2 | |||
| 88ca90c13a | |||
| f8ee340499 | |||
| 93f5260e20 | |||
| 34ba44ffa4 | |||
| b3a3e459a4 | |||
| f234564952 | |||
| ffa5795cc9 | |||
| 4f50c51356 | |||
| 9e9c8a0bec | |||
| 1349358d7c | |||
| 9c50f15be7 | |||
| 31e771daca | |||
| 66ce156dea | |||
| db6756bc78 | |||
| cab2581476 | |||
| 4c0be35020 | |||
| 7114201c08 | |||
| d8aecd325b | |||
| 1d18c13817 | |||
| f65eb0cd53 | |||
| 206c3884e9 | |||
| 35f9173980 | |||
| 48ab77eadc | |||
| f486513105 | |||
| f338adf033 | |||
| 74be667114 | |||
| b5a1fc92dc | |||
| 9cec1a8c49 | |||
| d4afba929b | |||
| 70939cbac6 | |||
| a3aa61df6d | |||
| e13ab5cb40 | |||
| d059947925 | |||
| d6c4b730de | |||
| 8241863170 | |||
| 31a758e4f3 | |||
| ca971a0e77 | |||
| a45a0f9a8a |
+1
-1
@@ -19,7 +19,7 @@ Thank you for your interest in contributing! This document outlines how you can
|
|||||||
|
|
||||||
### License
|
### License
|
||||||
|
|
||||||
The official plugins for this project are licensed under GPLv3. Any contributions you make will also fall under the GPLv3 license.
|
The official plugins for this project are licensed under AGPL. Any contributions you make will also fall under the AGPL license.
|
||||||
|
|
||||||
### How to Contribute
|
### How to Contribute
|
||||||
|
|
||||||
|
|||||||
+29
-13
@@ -1,13 +1,14 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import com.futo.platformplayer.encryption.EncryptionProvider
|
import com.futo.platformplayer.encryption.GEncryptionProviderV0
|
||||||
|
import com.futo.platformplayer.encryption.GEncryptionProviderV1
|
||||||
import junit.framework.TestCase.assertEquals
|
import junit.framework.TestCase.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class EncryptionProviderTests {
|
class GEncryptionProviderTests {
|
||||||
@Test
|
@Test
|
||||||
fun testEncryptDecrypt() {
|
fun testEncryptDecryptV1() {
|
||||||
val encryptionProvider = EncryptionProvider.instance
|
val encryptionProvider = GEncryptionProviderV1.instance
|
||||||
val plaintext = "This is a test string."
|
val plaintext = "This is a test string."
|
||||||
|
|
||||||
// Encrypt the plaintext
|
// Encrypt the plaintext
|
||||||
@@ -22,8 +23,8 @@ class EncryptionProviderTests {
|
|||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testEncryptDecryptBytes() {
|
fun testEncryptDecryptBytesV1() {
|
||||||
val encryptionProvider = EncryptionProvider.instance
|
val encryptionProvider = GEncryptionProviderV1.instance
|
||||||
val bytes = "This is a test string.".toByteArray();
|
val bytes = "This is a test string.".toByteArray();
|
||||||
|
|
||||||
// Encrypt the plaintext
|
// Encrypt the plaintext
|
||||||
@@ -36,21 +37,36 @@ class EncryptionProviderTests {
|
|||||||
assertArrayEquals(bytes, decrypted);
|
assertArrayEquals(bytes, decrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testEncryptDecryptBytesPassword() {
|
fun testEncryptDecryptV0() {
|
||||||
val encryptionProvider = EncryptionProvider.instance
|
val encryptionProvider = GEncryptionProviderV0.instance
|
||||||
val bytes = "This is a test string.".toByteArray();
|
val plaintext = "This is a test string."
|
||||||
val password = "1234".padStart(32, '9');
|
|
||||||
|
|
||||||
// Encrypt the plaintext
|
// Encrypt the plaintext
|
||||||
val ciphertext = encryptionProvider.encrypt(bytes, password)
|
val ciphertext = encryptionProvider.encrypt(plaintext)
|
||||||
|
|
||||||
// Decrypt the ciphertext
|
// Decrypt the ciphertext
|
||||||
val decrypted = encryptionProvider.decrypt(ciphertext, password)
|
val decrypted = encryptionProvider.decrypt(ciphertext)
|
||||||
|
|
||||||
|
// The decrypted string should be equal to the original plaintext
|
||||||
|
assertEquals(plaintext, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEncryptDecryptBytesV0() {
|
||||||
|
val encryptionProvider = GEncryptionProviderV0.instance
|
||||||
|
val bytes = "This is a test string.".toByteArray();
|
||||||
|
|
||||||
|
// Encrypt the plaintext
|
||||||
|
val ciphertext = encryptionProvider.encrypt(bytes)
|
||||||
|
|
||||||
|
// Decrypt the ciphertext
|
||||||
|
val decrypted = encryptionProvider.decrypt(ciphertext)
|
||||||
|
|
||||||
// The decrypted string should be equal to the original plaintext
|
// The decrypted string should be equal to the original plaintext
|
||||||
assertArrayEquals(bytes, decrypted);
|
assertArrayEquals(bytes, decrypted);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assertArrayEquals(a: ByteArray, b: ByteArray) {
|
private fun assertArrayEquals(a: ByteArray, b: ByteArray) {
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0
|
||||||
|
import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV1
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class GPasswordEncryptionProviderTests {
|
||||||
|
@Test
|
||||||
|
fun testEncryptDecryptBytesPasswordV1() {
|
||||||
|
val encryptionProvider = GPasswordEncryptionProviderV1();
|
||||||
|
val bytes = "This is a test string.".toByteArray();
|
||||||
|
|
||||||
|
// Encrypt the plaintext
|
||||||
|
val ciphertext = encryptionProvider.encrypt(bytes, "1234")
|
||||||
|
|
||||||
|
// Decrypt the ciphertext
|
||||||
|
val decrypted = encryptionProvider.decrypt(ciphertext, "1234")
|
||||||
|
|
||||||
|
// The decrypted string should be equal to the original plaintext
|
||||||
|
assertArrayEquals(bytes, decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEncryptDecryptBytesPasswordV0() {
|
||||||
|
val encryptionProvider = GPasswordEncryptionProviderV0("1234".padStart(32, '9'));
|
||||||
|
val bytes = "This is a test string.".toByteArray();
|
||||||
|
|
||||||
|
// Encrypt the plaintext
|
||||||
|
val ciphertext = encryptionProvider.encrypt(bytes)
|
||||||
|
|
||||||
|
// Decrypt the ciphertext
|
||||||
|
val decrypted = encryptionProvider.decrypt(ciphertext)
|
||||||
|
|
||||||
|
// The decrypted string should be equal to the original plaintext
|
||||||
|
assertArrayEquals(bytes, decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertArrayEquals(a: ByteArray, b: ByteArray) {
|
||||||
|
assertEquals(a.size, b.size);
|
||||||
|
for(i in 0 until a.size) {
|
||||||
|
assertEquals(a[i], b[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
android:enabled="true" />
|
android:enabled="true" />
|
||||||
|
|
||||||
<receiver android:name=".receivers.MediaControlReceiver" />
|
<receiver android:name=".receivers.MediaControlReceiver" />
|
||||||
|
<receiver android:name=".receivers.AudioNoisyReceiver" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.MainActivity"
|
android:name=".activities.MainActivity"
|
||||||
@@ -91,6 +92,26 @@
|
|||||||
<data android:host="*" />
|
<data android:host="*" />
|
||||||
<data android:scheme="file" />
|
<data android:scheme="file" />
|
||||||
|
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:host="*" />
|
||||||
|
<data android:scheme="content" />
|
||||||
|
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:host="*" />
|
||||||
|
<data android:scheme="file" />
|
||||||
|
|
||||||
<data android:mimeType="application/zip" />
|
<data android:mimeType="application/zip" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
@@ -182,9 +203,8 @@
|
|||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceOptionsActivity$QRCaptureActivity"
|
android:name=".activities.QRCaptureActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ let Type = {
|
|||||||
RAW: 0,
|
RAW: 0,
|
||||||
HTML: 1,
|
HTML: 1,
|
||||||
MARKUP: 2
|
MARKUP: 2
|
||||||
|
},
|
||||||
|
Chapter: {
|
||||||
|
NORMAL: 0,
|
||||||
|
|
||||||
|
SKIPPABLE: 5,
|
||||||
|
SKIP: 6
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -153,13 +159,27 @@ class FilterCapability {
|
|||||||
|
|
||||||
|
|
||||||
class PlatformAuthorLink {
|
class PlatformAuthorLink {
|
||||||
constructor(id, name, url, thumbnail, subscribers) {
|
constructor(id, name, url, thumbnail, subscribers, membershipUrl) {
|
||||||
this.id = id ?? PlatformID(); //PlatformID
|
this.id = id ?? PlatformID(); //PlatformID
|
||||||
this.name = name ?? ""; //string
|
this.name = name ?? ""; //string
|
||||||
this.url = url ?? ""; //string
|
this.url = url ?? ""; //string
|
||||||
this.thumbnail = thumbnail; //string
|
this.thumbnail = thumbnail; //string
|
||||||
if(subscribers)
|
if(subscribers)
|
||||||
this.subscribers = subscribers;
|
this.subscribers = subscribers;
|
||||||
|
if(membershipUrl)
|
||||||
|
this.membershipUrl = membershipUrl ?? null; //string (for backcompat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class PlatformAuthorMembershipLink {
|
||||||
|
constructor(id, name, url, thumbnail, subscribers, membershipUrl) {
|
||||||
|
this.id = id ?? PlatformID(); //PlatformID
|
||||||
|
this.name = name ?? ""; //string
|
||||||
|
this.url = url ?? ""; //string
|
||||||
|
this.thumbnail = thumbnail; //string
|
||||||
|
if(subscribers)
|
||||||
|
this.subscribers = subscribers;
|
||||||
|
if(membershipUrl)
|
||||||
|
this.membershipUrl = membershipUrl ?? null; //string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class PlatformContent {
|
class PlatformContent {
|
||||||
|
|||||||
@@ -185,6 +185,25 @@ fun OffsetDateTime.toHumanNowDiffString(abs: Boolean = false) : String {
|
|||||||
|
|
||||||
return "${value} ${unit}";
|
return "${value} ${unit}";
|
||||||
};
|
};
|
||||||
|
fun Int.toHumanTimeIndicator(abs: Boolean = false) : String {
|
||||||
|
var value = this;
|
||||||
|
|
||||||
|
var unit = "s";
|
||||||
|
|
||||||
|
if(abs) value = abs(value);
|
||||||
|
if(value >= secondsInHour) {
|
||||||
|
value = (this / secondsInHour).toInt();
|
||||||
|
if(abs) value = abs(value);
|
||||||
|
unit = "hr" + (if(value > 1) "s" else "");
|
||||||
|
}
|
||||||
|
else if(value >= secondsInMinute) {
|
||||||
|
value = (this / secondsInMinute).toInt();
|
||||||
|
if(abs) value = abs(value);
|
||||||
|
unit = "min";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "${value}${unit}";
|
||||||
|
}
|
||||||
|
|
||||||
fun Long.toHumanTime(isMs: Boolean): String {
|
fun Long.toHumanTime(isMs: Boolean): String {
|
||||||
var scaler = 1;
|
var scaler = 1;
|
||||||
|
|||||||
@@ -35,4 +35,8 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
|
|||||||
|
|
||||||
fun Protocol.Claim.resolveChannelUrl(): String? {
|
fun Protocol.Claim.resolveChannelUrl(): String? {
|
||||||
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
||||||
|
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
class HorizontalSpaceItemDecoration(private val startSpace: Int, private val betweenSpace: Int, private val endSpace: Int) : RecyclerView.ItemDecoration() {
|
||||||
|
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
outRect.left = betweenSpace
|
||||||
|
|
||||||
|
val position = parent.getChildAdapterPosition(view)
|
||||||
|
if (position == 0) {
|
||||||
|
outRect.left = startSpace
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (position == state.itemCount - 1) {
|
||||||
|
outRect.right = endSpace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,7 @@ import android.content.ActivityNotFoundException
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import androidx.core.content.ContextCompat.startActivity
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.activities.*
|
import com.futo.platformplayer.activities.*
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
@@ -22,6 +20,7 @@ import com.futo.platformplayer.views.FeedStyle
|
|||||||
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
|
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
|
||||||
import com.futo.platformplayer.views.fields.FormField
|
import com.futo.platformplayer.views.fields.FormField
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
|
import com.futo.platformplayer.views.fields.FormFieldButton
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -29,6 +28,7 @@ import kotlinx.serialization.*
|
|||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
||||||
@@ -44,9 +44,10 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
val onTabsChanged = Event0();
|
val onTabsChanged = Event0();
|
||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
"Manage Polycentric identity", FieldForm.BUTTON,
|
R.string.manage_polycentric_identity, FieldForm.BUTTON,
|
||||||
"Manage your Polycentric identity", -3
|
R.string.manage_your_polycentric_identity, -5
|
||||||
)
|
)
|
||||||
|
@FormFieldButton(R.drawable.ic_person)
|
||||||
fun managePolycentricIdentity() {
|
fun managePolycentricIdentity() {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
if (StatePolycentric.instance.processHandle != null) {
|
if (StatePolycentric.instance.processHandle != null) {
|
||||||
@@ -58,22 +59,38 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
"Open FAQ", FieldForm.BUTTON,
|
R.string.show_faq, FieldForm.BUTTON,
|
||||||
"Get answers to common questions", -2
|
R.string.get_answers_to_common_questions, -4
|
||||||
)
|
)
|
||||||
|
@FormFieldButton(R.drawable.ic_quiz)
|
||||||
fun openFAQ() {
|
fun openFAQ() {
|
||||||
try {
|
try {
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://grayjay.app/faq.html"))
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Settings.URL_FAQ))
|
||||||
|
SettingsActivity.getActivity()?.startActivity(browserIntent);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
//Ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@FormField(
|
||||||
|
R.string.show_issues, FieldForm.BUTTON,
|
||||||
|
R.string.a_list_of_user_reported_and_self_reported_issues, -3
|
||||||
|
)
|
||||||
|
@FormFieldButton(R.drawable.ic_data_alert)
|
||||||
|
fun openIssues() {
|
||||||
|
try {
|
||||||
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/futo-org/grayjay-android/issues"))
|
||||||
SettingsActivity.getActivity()?.startActivity(browserIntent);
|
SettingsActivity.getActivity()?.startActivity(browserIntent);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
@FormField(
|
@FormField(
|
||||||
"Submit feedback", FieldForm.BUTTON,
|
R.string.submit_feedback, FieldForm.BUTTON,
|
||||||
"Give feedback on the application", -1
|
R.string.give_feedback_on_the_application, -1
|
||||||
)
|
)
|
||||||
|
@FormFieldButton(R.drawable.ic_bug)
|
||||||
fun submitFeedback() {
|
fun submitFeedback() {
|
||||||
try {
|
try {
|
||||||
val i = Intent(Intent.ACTION_VIEW);
|
val i = Intent(Intent.ACTION_VIEW);
|
||||||
@@ -87,12 +104,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
"Manage Tabs", FieldForm.BUTTON,
|
R.string.manage_tabs, FieldForm.BUTTON,
|
||||||
"Change tabs visible on the home screen", -1
|
R.string.change_tabs_visible_on_the_home_screen, -2
|
||||||
)
|
)
|
||||||
|
@FormFieldButton(R.drawable.ic_tabs)
|
||||||
fun manageTabs() {
|
fun manageTabs() {
|
||||||
try {
|
try {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
@@ -103,11 +121,39 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Home", "group", "Configure how your Home tab works and feels", 1)
|
|
||||||
|
|
||||||
|
@FormField(R.string.language, "group", -1, 0)
|
||||||
|
var language = LanguageSettings();
|
||||||
|
@Serializable
|
||||||
|
class LanguageSettings {
|
||||||
|
@FormField(R.string.app_language, FieldForm.DROPDOWN, R.string.may_require_restart, 5, "app_language")
|
||||||
|
@DropdownFieldOptionsId(R.array.app_languages)
|
||||||
|
var appLanguage: Int = 0;
|
||||||
|
|
||||||
|
fun getAppLanguageLocaleString(): String? {
|
||||||
|
return when(appLanguage) {
|
||||||
|
0 -> null
|
||||||
|
1 -> "en";
|
||||||
|
2 -> "de";
|
||||||
|
3 -> "es";
|
||||||
|
4 -> "pt";
|
||||||
|
5 -> "fr"
|
||||||
|
6 -> "ja";
|
||||||
|
7 -> "ko";
|
||||||
|
8 -> "zh";
|
||||||
|
9 -> "ru";
|
||||||
|
10 -> "ar";
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.home, "group", R.string.configure_how_your_home_tab_works_and_feels, 1)
|
||||||
var home = HomeSettings();
|
var home = HomeSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class HomeSettings {
|
class HomeSettings {
|
||||||
@FormField("Feed Style", FieldForm.DROPDOWN, "", 5)
|
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5)
|
||||||
@DropdownFieldOptionsId(R.array.feed_style)
|
@DropdownFieldOptionsId(R.array.feed_style)
|
||||||
var homeFeedStyle: Int = 1;
|
var homeFeedStyle: Int = 1;
|
||||||
|
|
||||||
@@ -117,21 +163,39 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
else
|
else
|
||||||
return FeedStyle.THUMBNAIL;
|
return FeedStyle.THUMBNAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||||
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.clear_hidden, FieldForm.BUTTON, R.string.clear_hidden_description, 7)
|
||||||
|
@FormFieldButton(R.drawable.ic_visibility_off)
|
||||||
|
fun clearHidden() {
|
||||||
|
StateMeta.instance.removeAllHiddenCreators();
|
||||||
|
StateMeta.instance.removeAllHiddenVideos();
|
||||||
|
SettingsActivity.getActivity()?.let {
|
||||||
|
UIDialogs.toast(it, "Creators and videos should show up again");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Search", "group", "", 2)
|
@FormField(R.string.search, "group", -1, 2)
|
||||||
var search = SearchSettings();
|
var search = SearchSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class SearchSettings {
|
class SearchSettings {
|
||||||
@FormField("Search History", FieldForm.TOGGLE, "", 4)
|
@FormField(R.string.search_history, FieldForm.TOGGLE, R.string.may_require_restart, 3)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var searchHistory: Boolean = true;
|
var searchHistory: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
@FormField("Feed Style", FieldForm.DROPDOWN, "", 5)
|
@FormField(R.string.feed_style, FieldForm.DROPDOWN, -1, 4)
|
||||||
@DropdownFieldOptionsId(R.array.feed_style)
|
@DropdownFieldOptionsId(R.array.feed_style)
|
||||||
var searchFeedStyle: Int = 1;
|
var searchFeedStyle: Int = 1;
|
||||||
|
|
||||||
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
|
||||||
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun getSearchFeedStyle(): FeedStyle {
|
fun getSearchFeedStyle(): FeedStyle {
|
||||||
if(searchFeedStyle == 0)
|
if(searchFeedStyle == 0)
|
||||||
@@ -141,11 +205,11 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Subscriptions", "group", "Configure how your Subscriptions works and feels", 3)
|
@FormField(R.string.subscriptions, "group", R.string.configure_how_your_subscriptions_works_and_feels, 3)
|
||||||
var subscriptions = SubscriptionsSettings();
|
var subscriptions = SubscriptionsSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class SubscriptionsSettings {
|
class SubscriptionsSettings {
|
||||||
@FormField("Feed Style", FieldForm.DROPDOWN, "", 5)
|
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 4)
|
||||||
@DropdownFieldOptionsId(R.array.feed_style)
|
@DropdownFieldOptionsId(R.array.feed_style)
|
||||||
var subscriptionsFeedStyle: Int = 1;
|
var subscriptionsFeedStyle: Int = 1;
|
||||||
|
|
||||||
@@ -156,11 +220,17 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return FeedStyle.THUMBNAIL;
|
return FeedStyle.THUMBNAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Fetch on app boot", FieldForm.TOGGLE, "Shortly after opening the app, start fetching subscriptions.", 6)
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
|
||||||
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 6)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var fetchOnAppBoot: Boolean = true;
|
var fetchOnAppBoot: Boolean = true;
|
||||||
|
|
||||||
@FormField("Background Update", FieldForm.DROPDOWN, "Experimental background update for subscriptions cache (requires restart)", 7)
|
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 6)
|
||||||
|
var fetchOnTabOpen: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 7)
|
||||||
@DropdownFieldOptionsId(R.array.background_interval)
|
@DropdownFieldOptionsId(R.array.background_interval)
|
||||||
var subscriptionsBackgroundUpdateInterval: Int = 0;
|
var subscriptionsBackgroundUpdateInterval: Int = 0;
|
||||||
|
|
||||||
@@ -176,26 +246,36 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@FormField("Subscription Concurrency", FieldForm.DROPDOWN, "Specify how many threads are used to fetch channels (requires restart)", 8)
|
@FormField(R.string.subscription_concurrency, FieldForm.DROPDOWN, R.string.specify_how_many_threads_are_used_to_fetch_channels, 8)
|
||||||
@DropdownFieldOptionsId(R.array.thread_count)
|
@DropdownFieldOptionsId(R.array.thread_count)
|
||||||
var subscriptionConcurrency: Int = 3;
|
var subscriptionConcurrency: Int = 3;
|
||||||
|
|
||||||
fun getSubscriptionsConcurrency() : Int {
|
fun getSubscriptionsConcurrency() : Int {
|
||||||
return threadIndexToCount(subscriptionConcurrency);
|
return threadIndexToCount(subscriptionConcurrency);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 9)
|
||||||
|
var showWatchMetrics: Boolean = false;
|
||||||
|
|
||||||
|
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 10)
|
||||||
|
var allowPlaytimeTracking: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 11)
|
||||||
|
var alwaysReloadFromCache: Boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Player", "group", "Change behavior of the player", 4)
|
@FormField(R.string.player, "group", R.string.change_behavior_of_the_player, 4)
|
||||||
var playback = PlaybackSettings();
|
var playback = PlaybackSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class PlaybackSettings {
|
class PlaybackSettings {
|
||||||
@FormField("Primary Language", FieldForm.DROPDOWN, "", 0)
|
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, 0)
|
||||||
@DropdownFieldOptionsId(R.array.languages)
|
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||||
var primaryLanguage: Int = 0;
|
var primaryLanguage: Int = 0;
|
||||||
|
|
||||||
fun getPrimaryLanguage(context: Context) = context.resources.getStringArray(R.array.languages)[primaryLanguage];
|
fun getPrimaryLanguage(context: Context) = context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
||||||
|
|
||||||
@FormField("Default Playback Speed", FieldForm.DROPDOWN, "", 1)
|
@FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 1)
|
||||||
@DropdownFieldOptionsId(R.array.playback_speeds)
|
@DropdownFieldOptionsId(R.array.playback_speeds)
|
||||||
var defaultPlaybackSpeed: Int = 3;
|
var defaultPlaybackSpeed: Int = 3;
|
||||||
fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
|
fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
|
||||||
@@ -211,29 +291,29 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
else -> 1.0f;
|
else -> 1.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
@FormField("Preferred Quality", FieldForm.DROPDOWN, "", 2)
|
@FormField(R.string.preferred_quality, FieldForm.DROPDOWN, -1, 2)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
var preferredQuality: Int = 0;
|
var preferredQuality: Int = 0;
|
||||||
|
|
||||||
@FormField("Preferred Metered Quality", FieldForm.DROPDOWN, "", 2)
|
@FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, -1, 2)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
var preferredMeteredQuality: Int = 0;
|
var preferredMeteredQuality: Int = 0;
|
||||||
fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
|
fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
|
||||||
fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
|
fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
|
||||||
fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();
|
fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();
|
||||||
|
|
||||||
@FormField("Preferred Preview Quality", FieldForm.DROPDOWN, "", 3)
|
@FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, -1, 3)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
var preferredPreviewQuality: Int = 5;
|
var preferredPreviewQuality: Int = 5;
|
||||||
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
||||||
|
|
||||||
@FormField("Auto-Rotate", FieldForm.DROPDOWN, "", 4)
|
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 4)
|
||||||
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
|
||||||
var autoRotate: Int = 2;
|
var autoRotate: Int = 2;
|
||||||
|
|
||||||
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
|
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
|
||||||
|
|
||||||
@FormField("Auto-Rotate Dead Zone", FieldForm.DROPDOWN, "This prevents the device from rotating within the given amount of degrees.", 5)
|
@FormField(R.string.auto_rotate_dead_zone, FieldForm.DROPDOWN, R.string.this_prevents_the_device_from_rotating_within_the_given_amount_of_degrees, 5)
|
||||||
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
|
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
|
||||||
var autoRotateDeadZone: Int = 0;
|
var autoRotateDeadZone: Int = 0;
|
||||||
|
|
||||||
@@ -241,21 +321,17 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return autoRotateDeadZone * 5;
|
return autoRotateDeadZone * 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Background Behavior", FieldForm.DROPDOWN, "", 6)
|
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
|
||||||
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
||||||
var backgroundPlay: Int = 2;
|
var backgroundPlay: Int = 2;
|
||||||
|
|
||||||
fun isBackgroundContinue() = backgroundPlay == 1;
|
fun isBackgroundContinue() = backgroundPlay == 1;
|
||||||
fun isBackgroundPictureInPicture() = backgroundPlay == 2;
|
fun isBackgroundPictureInPicture() = backgroundPlay == 2;
|
||||||
|
|
||||||
@FormField("Resume After Preview", FieldForm.DROPDOWN, "When watching a video in preview mode, resume at the position when opening the video", 7)
|
@FormField(R.string.resume_after_preview, FieldForm.DROPDOWN, R.string.when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code, 7)
|
||||||
@DropdownFieldOptionsId(R.array.resume_after_preview)
|
@DropdownFieldOptionsId(R.array.resume_after_preview)
|
||||||
var resumeAfterPreview: Int = 1;
|
var resumeAfterPreview: Int = 1;
|
||||||
|
|
||||||
|
|
||||||
@FormField("Live Chat Webview", FieldForm.TOGGLE, "Use the live chat web window when available over native implementation.", 8)
|
|
||||||
var useLiveChatWindow: Boolean = true;
|
|
||||||
|
|
||||||
fun shouldResumePreview(previewedPosition: Long): Boolean{
|
fun shouldResumePreview(previewedPosition: Long): Boolean{
|
||||||
if(resumeAfterPreview == 2)
|
if(resumeAfterPreview == 2)
|
||||||
return true;
|
return true;
|
||||||
@@ -263,14 +339,22 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 8)
|
||||||
|
var useLiveChatWindow: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 8)
|
||||||
|
var backgroundSwitchToAudio: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Downloads", "group", "Configure downloading of videos", 5)
|
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 5)
|
||||||
var downloads = Downloads();
|
var downloads = Downloads();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Downloads {
|
class Downloads {
|
||||||
|
|
||||||
@FormField("Download when", FieldForm.DROPDOWN, "Configure when videos should be downloaded", 0)
|
@FormField(R.string.download_when, FieldForm.DROPDOWN, R.string.configure_when_videos_should_be_downloaded, 0)
|
||||||
@DropdownFieldOptionsId(R.array.when_download)
|
@DropdownFieldOptionsId(R.array.when_download)
|
||||||
var whenDownload: Int = 0;
|
var whenDownload: Int = 0;
|
||||||
|
|
||||||
@@ -283,21 +367,21 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Default Video Quality", FieldForm.DROPDOWN, "", 2)
|
@FormField(R.string.default_video_quality, FieldForm.DROPDOWN, -1, 2)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_video_download)
|
@DropdownFieldOptionsId(R.array.preferred_video_download)
|
||||||
var preferredVideoQuality: Int = 4;
|
var preferredVideoQuality: Int = 4;
|
||||||
fun getDefaultVideoQualityPixels(): Int = preferedQualityToPixels(preferredVideoQuality);
|
fun getDefaultVideoQualityPixels(): Int = preferedQualityToPixels(preferredVideoQuality);
|
||||||
|
|
||||||
@FormField("Default Audio Quality", FieldForm.DROPDOWN, "", 3)
|
@FormField(R.string.default_audio_quality, FieldForm.DROPDOWN, -1, 3)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_audio_download)
|
@DropdownFieldOptionsId(R.array.preferred_audio_download)
|
||||||
var preferredAudioQuality: Int = 1;
|
var preferredAudioQuality: Int = 1;
|
||||||
fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;
|
fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;
|
||||||
|
|
||||||
@FormField("ByteRange Download", FieldForm.TOGGLE, "Attempt to utilize byte ranges, this can be combined with concurrency to bypass throttling", 4)
|
@FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var byteRangeDownload: Boolean = true;
|
var byteRangeDownload: Boolean = true;
|
||||||
|
|
||||||
@FormField("ByteRange Concurrency", FieldForm.DROPDOWN, "Number of concurrent threads to multiply download speeds from throttled sources", 5)
|
@FormField(R.string.byte_range_concurrency, FieldForm.DROPDOWN, R.string.number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources, 5)
|
||||||
@DropdownFieldOptionsId(R.array.thread_count)
|
@DropdownFieldOptionsId(R.array.thread_count)
|
||||||
var byteRangeConcurrency: Int = 3;
|
var byteRangeConcurrency: Int = 3;
|
||||||
fun getByteRangeThreadCount(): Int {
|
fun getByteRangeThreadCount(): Int {
|
||||||
@@ -305,20 +389,20 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Browsing", "group", "Configure browsing behavior", 6)
|
@FormField(R.string.browsing, "group", R.string.configure_browsing_behavior, 6)
|
||||||
var browsing = Browsing();
|
var browsing = Browsing();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Browsing {
|
class Browsing {
|
||||||
@FormField("Enable Video Cache", FieldForm.TOGGLE, "A cache to quickly load previously fetched videos", 0)
|
@FormField(R.string.enable_video_cache, FieldForm.TOGGLE, R.string.cache_to_quickly_load_previously_fetched_videos, 0)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var videoCache: Boolean = true;
|
var videoCache: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Casting", "group", "Configure casting", 7)
|
@FormField(R.string.casting, "group", R.string.configure_casting, 7)
|
||||||
var casting = Casting();
|
var casting = Casting();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Casting {
|
class Casting {
|
||||||
@FormField("Enabled", FieldForm.TOGGLE, "Enable casting", 0)
|
@FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enable_casting, 0)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var enabled: Boolean = true;
|
var enabled: Boolean = true;
|
||||||
|
|
||||||
@@ -340,24 +424,24 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@FormField("Logging", FieldForm.GROUP, "", 8)
|
@FormField(R.string.logging, FieldForm.GROUP, -1, 8)
|
||||||
var logging = Logging();
|
var logging = Logging();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Logging {
|
class Logging {
|
||||||
@FormField("Log Level", FieldForm.DROPDOWN, "", 0)
|
@FormField(R.string.log_level, FieldForm.DROPDOWN, -1, 0)
|
||||||
@DropdownFieldOptionsId(R.array.log_levels)
|
@DropdownFieldOptionsId(R.array.log_levels)
|
||||||
var logLevel: Int = 0;
|
var logLevel: Int = 0;
|
||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
"Submit logs", FieldForm.BUTTON,
|
R.string.submit_logs, FieldForm.BUTTON,
|
||||||
"Submit logs to help us narrow down issues", 1
|
R.string.submit_logs_to_help_us_narrow_down_issues, 1
|
||||||
)
|
)
|
||||||
fun submitLogs() {
|
fun submitLogs() {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
if (!Logger.submitLogs()) {
|
if (!Logger.submitLogs()) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, "Please enable logging to submit logs") }
|
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.please_enable_logging_to_submit_logs)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -369,40 +453,40 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
@FormField("Announcement", FieldForm.GROUP, "", 10)
|
@FormField(R.string.announcement, FieldForm.GROUP, -1, 10)
|
||||||
var announcementSettings = AnnouncementSettings();
|
var announcementSettings = AnnouncementSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class AnnouncementSettings {
|
class AnnouncementSettings {
|
||||||
@FormField(
|
@FormField(
|
||||||
"Reset announcements", FieldForm.BUTTON,
|
R.string.reset_announcements, FieldForm.BUTTON,
|
||||||
"Reset hidden announcements", 1
|
R.string.reset_hidden_announcements, 1
|
||||||
)
|
)
|
||||||
fun resetAnnouncements() {
|
fun resetAnnouncements() {
|
||||||
StateAnnouncement.instance.resetAnnouncements();
|
StateAnnouncement.instance.resetAnnouncements();
|
||||||
UIDialogs.toast("Announcements reset.");
|
SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.announcements_reset)); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Plugins", FieldForm.GROUP, "", 11)
|
@FormField(R.string.plugins, FieldForm.GROUP, -1, 11)
|
||||||
@Transient
|
@Transient
|
||||||
var plugins = Plugins();
|
var plugins = Plugins();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Plugins {
|
class Plugins {
|
||||||
|
|
||||||
@FormField("Clear Cookies on Logout", FieldForm.TOGGLE, "Clears cookies when you log out, allowing you to change account.", 0)
|
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
|
||||||
var clearCookiesOnLogout: Boolean = true;
|
var clearCookiesOnLogout: Boolean = true;
|
||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
"Clear Cookies", FieldForm.BUTTON,
|
R.string.clear_cookies, FieldForm.BUTTON,
|
||||||
"Clears in-app browser cookies, especially useful for fully logging out of plugins.", 1
|
R.string.clears_in_app_browser_cookies, 1
|
||||||
)
|
)
|
||||||
fun clearCookies() {
|
fun clearCookies() {
|
||||||
val cookieManager: CookieManager = CookieManager.getInstance();
|
val cookieManager: CookieManager = CookieManager.getInstance();
|
||||||
cookieManager.removeAllCookies(null);
|
cookieManager.removeAllCookies(null);
|
||||||
}
|
}
|
||||||
@FormField(
|
@FormField(
|
||||||
"Reinstall Embedded Plugins", FieldForm.BUTTON,
|
R.string.reinstall_embedded_plugins, FieldForm.BUTTON,
|
||||||
"Also removes any data related plugin like login or settings (may not clear browser cache)", 1
|
R.string.also_removes_any_data_related_plugin_like_login_or_settings, 1
|
||||||
)
|
)
|
||||||
fun reinstallEmbedded() {
|
fun reinstallEmbedded() {
|
||||||
StateApp.instance.scopeOrNull!!.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull!!.launch(Dispatchers.IO) {
|
||||||
@@ -411,7 +495,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
StateApp.instance.contextOrNull?.let {
|
StateApp.instance.contextOrNull?.let {
|
||||||
UIDialogs.toast(it, "Embedded plugins reinstalled, a reboot is recommended");
|
UIDialogs.toast(it, it.getString(R.string.embedded_plugins_reinstalled_a_reboot_is_recommended));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
@@ -426,7 +510,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@FormField("External Storage", FieldForm.GROUP, "", 12)
|
@FormField(R.string.external_storage, FieldForm.GROUP, -1, 12)
|
||||||
var storage = Storage();
|
var storage = Storage();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Storage {
|
class Storage {
|
||||||
@@ -438,13 +522,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun isStorageMainValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageGeneralUri());
|
fun isStorageMainValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageGeneralUri());
|
||||||
fun isStorageDownloadValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageDownloadUri());
|
fun isStorageDownloadValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageDownloadUri());
|
||||||
|
|
||||||
@FormField("Change external General directory", FieldForm.BUTTON, "Change the external directory for general files, used for persistent files like auto-backup", 3)
|
@FormField(R.string.change_external_general_directory, FieldForm.BUTTON, R.string.change_the_external_directory_for_general_files, 3)
|
||||||
fun changeStorageGeneral() {
|
fun changeStorageGeneral() {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
StateApp.instance.changeExternalGeneralDirectory(it);
|
StateApp.instance.changeExternalGeneralDirectory(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@FormField("Change external Downloads directory", FieldForm.BUTTON, "Change the external storage for download files, used for exported download files", 4)
|
@FormField(R.string.change_external_downloads_directory, FieldForm.BUTTON, R.string.change_the_external_storage_for_download_files, 4)
|
||||||
fun changeStorageDownload() {
|
fun changeStorageDownload() {
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
StateApp.instance.changeExternalDownloadDirectory(it);
|
StateApp.instance.changeExternalDownloadDirectory(it);
|
||||||
@@ -453,19 +537,19 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@FormField("Auto Update", "group", "Configure the auto updater", 12)
|
@FormField(R.string.auto_update, "group", R.string.configure_the_auto_updater, 12)
|
||||||
var autoUpdate = AutoUpdate();
|
var autoUpdate = AutoUpdate();
|
||||||
@Serializable
|
@Serializable
|
||||||
class AutoUpdate {
|
class AutoUpdate {
|
||||||
@FormField("Check", FieldForm.DROPDOWN, "", 0)
|
@FormField(R.string.check, FieldForm.DROPDOWN, -1, 0)
|
||||||
@DropdownFieldOptionsId(R.array.auto_update_when_array)
|
@DropdownFieldOptionsId(R.array.auto_update_when_array)
|
||||||
var check: Int = 0;
|
var check: Int = 0;
|
||||||
|
|
||||||
@FormField("Background download", FieldForm.DROPDOWN, "Configure if background download should be used", 1)
|
@FormField(R.string.background_download, FieldForm.DROPDOWN, R.string.configure_if_background_download_should_be_used, 1)
|
||||||
@DropdownFieldOptionsId(R.array.background_download)
|
@DropdownFieldOptionsId(R.array.background_download)
|
||||||
var backgroundDownload: Int = 0;
|
var backgroundDownload: Int = 0;
|
||||||
|
|
||||||
@FormField("Download when", FieldForm.DROPDOWN, "Configure when updates should be downloaded", 2)
|
@FormField(R.string.download_when, FieldForm.DROPDOWN, R.string.configure_when_updates_should_be_downloaded, 2)
|
||||||
@DropdownFieldOptionsId(R.array.when_download)
|
@DropdownFieldOptionsId(R.array.when_download)
|
||||||
var whenDownload: Int = 0;
|
var whenDownload: Int = 0;
|
||||||
|
|
||||||
@@ -483,8 +567,8 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
"Manual check", FieldForm.BUTTON,
|
R.string.manual_check, FieldForm.BUTTON,
|
||||||
"Manually check for updates", 3
|
R.string.manually_check_for_updates, 3
|
||||||
)
|
)
|
||||||
fun manualCheck() {
|
fun manualCheck() {
|
||||||
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||||
@@ -496,19 +580,20 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
try {
|
try {
|
||||||
it.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${it.packageName}")))
|
it.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${it.packageName}")))
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
UIDialogs.toast(it, "Failed to show store.");
|
UIDialogs.toast(it, it.getString(R.string.failed_to_show_store));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
"View changelog", FieldForm.BUTTON,
|
R.string.view_changelog, FieldForm.BUTTON,
|
||||||
"Review the current and past changelogs", 4
|
R.string.review_the_current_and_past_changelogs, 4
|
||||||
)
|
)
|
||||||
fun viewChangelog() {
|
fun viewChangelog() {
|
||||||
UIDialogs.toast("Retrieving changelog");
|
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
|
UIDialogs.toast(it.getString(R.string.retrieving_changelog));
|
||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val version = StateUpdate.instance.downloadVersionCode(ManagedHttpClient()) ?: return@launch;
|
val version = StateUpdate.instance.downloadVersionCode(ManagedHttpClient()) ?: return@launch;
|
||||||
@@ -525,8 +610,8 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
"Remove Cached Version", FieldForm.BUTTON,
|
R.string.remove_cached_version, FieldForm.BUTTON,
|
||||||
"Remove the last downloaded version", 5
|
R.string.remove_the_last_downloaded_version, 5
|
||||||
)
|
)
|
||||||
fun removeCachedVersion() {
|
fun removeCachedVersion() {
|
||||||
StateApp.withContext {
|
StateApp.withContext {
|
||||||
@@ -543,7 +628,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Backup", FieldForm.GROUP, "", 13)
|
@FormField(R.string.backup, FieldForm.GROUP, -1, 13)
|
||||||
var backup = Backup();
|
var backup = Backup();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Backup {
|
class Backup {
|
||||||
@@ -553,58 +638,75 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var autoBackupPassword: String? = null;
|
var autoBackupPassword: String? = null;
|
||||||
fun shouldAutomaticBackup() = autoBackupPassword != null;
|
fun shouldAutomaticBackup() = autoBackupPassword != null;
|
||||||
|
|
||||||
@FormField("Automatic Backup", FieldForm.READONLYTEXT, "", 0)
|
@FormField(R.string.automatic_backup, FieldForm.READONLYTEXT, -1, 0)
|
||||||
val automaticBackupText get() = if(!shouldAutomaticBackup()) "None" else "Every Day";
|
val automaticBackupText get() = if(!shouldAutomaticBackup()) "None" else "Every Day";
|
||||||
|
|
||||||
@FormField("Set Automatic Backup", FieldForm.BUTTON, "Configure daily backup in case of catastrophic failure. (Written to the external Grayjay directory)", 1)
|
@FormField(R.string.set_automatic_backup, FieldForm.BUTTON, R.string.configure_daily_backup_in_case_of_catastrophic_failure, 1)
|
||||||
fun configureAutomaticBackup() {
|
fun configureAutomaticBackup() {
|
||||||
UIDialogs.showAutomaticBackupDialog(SettingsActivity.getActivity()!!, autoBackupPassword != null) {
|
UIDialogs.showAutomaticBackupDialog(SettingsActivity.getActivity()!!, autoBackupPassword != null) {
|
||||||
SettingsActivity.getActivity()?.reloadSettings();
|
SettingsActivity.getActivity()?.reloadSettings();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@FormField("Restore Automatic Backup", FieldForm.BUTTON, "Restore a previous automatic backup", 2)
|
@FormField(R.string.restore_automatic_backup, FieldForm.BUTTON, R.string.restore_a_previous_automatic_backup, 2)
|
||||||
fun restoreAutomaticBackup() {
|
fun restoreAutomaticBackup() {
|
||||||
val activity = SettingsActivity.getActivity()!!
|
val activity = SettingsActivity.getActivity()!!
|
||||||
|
|
||||||
if(!StateBackup.hasAutomaticBackup())
|
if(!StateBackup.hasAutomaticBackup())
|
||||||
UIDialogs.toast(activity, "You don't have any automatic backups", false);
|
UIDialogs.toast(activity, activity.getString(R.string.you_don_t_have_any_automatic_backups), false);
|
||||||
else
|
else
|
||||||
UIDialogs.showAutomaticRestoreDialog(activity, activity.lifecycleScope);
|
UIDialogs.showAutomaticRestoreDialog(activity, activity.lifecycleScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@FormField("Export Data", FieldForm.BUTTON, "Creates a zip file with your data which can be imported by opening it with Grayjay", 3)
|
@FormField(R.string.export_data, FieldForm.BUTTON, R.string.creates_a_zip_file_with_your_data_which_can_be_imported_by_opening_it_with_grayjay, 3)
|
||||||
fun export() {
|
fun export() {
|
||||||
StateBackup.startExternalBackup();
|
StateBackup.startExternalBackup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, 4)
|
||||||
|
fun import() {
|
||||||
|
val act = SettingsActivity.getActivity() ?: return;
|
||||||
|
StateApp.instance.requestFileReadAccess(act, null) {
|
||||||
|
if(it != null && it.exists()) {
|
||||||
|
val name = it.name;
|
||||||
|
val contents = it.readBytes(act);
|
||||||
|
if(contents != null) {
|
||||||
|
if(name != null && name.endsWith(".zip", true))
|
||||||
|
StateBackup.importZipBytes(act, act.lifecycleScope, contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Payment", FieldForm.GROUP, "", 14)
|
@FormField(R.string.payment, FieldForm.GROUP, -1, 14)
|
||||||
var payment = Payment();
|
var payment = Payment();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Payment {
|
class Payment {
|
||||||
@FormField("Payment Status", FieldForm.READONLYTEXT, "", 1)
|
@FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1)
|
||||||
val paymentStatus: String get() = if (StatePayment.instance.hasPaid) "Paid" else "Not Paid";
|
val paymentStatus: String get() = SettingsActivity.getActivity()?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown";
|
||||||
|
|
||||||
@FormField("Clear Payment", FieldForm.BUTTON, "Deletes license keys from app", 2)
|
@FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
|
||||||
fun clearPayment() {
|
fun clearPayment() {
|
||||||
StatePayment.instance.clearLicenses();
|
StatePayment.instance.clearLicenses();
|
||||||
SettingsActivity.getActivity()?.let {
|
SettingsActivity.getActivity()?.let {
|
||||||
UIDialogs.toast(it, "Licenses cleared, might require app restart");
|
UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
|
||||||
it.reloadSettings();
|
it.reloadSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Info", FieldForm.GROUP, "", 15)
|
@FormField(R.string.info, FieldForm.GROUP, -1, 15)
|
||||||
var info = Info();
|
var info = Info();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Info {
|
class Info {
|
||||||
@FormField("Version Code", FieldForm.READONLYTEXT, "", 1, "code")
|
@FormField(R.string.version_code, FieldForm.READONLYTEXT, -1, 1, "code")
|
||||||
var versionCode = BuildConfig.VERSION_CODE;
|
var versionCode = BuildConfig.VERSION_CODE;
|
||||||
@FormField("Version Name", FieldForm.READONLYTEXT, "", 2)
|
@FormField(R.string.version_name, FieldForm.READONLYTEXT, -1, 2)
|
||||||
var versionName = BuildConfig.VERSION_NAME;
|
var versionName = BuildConfig.VERSION_NAME;
|
||||||
@FormField("Version Type", FieldForm.READONLYTEXT, "", 3)
|
@FormField(R.string.version_type, FieldForm.READONLYTEXT, -1, 3)
|
||||||
var versionType = BuildConfig.BUILD_TYPE;
|
var versionType = BuildConfig.BUILD_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,6 +717,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "Settings";
|
private const val TAG = "Settings";
|
||||||
|
const val URL_FAQ = "https://grayjay.app/faq.html";
|
||||||
|
|
||||||
private var _isFirst = true;
|
private var _isFirst = true;
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,24 @@ package com.futo.platformplayer
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
|
import androidx.work.Constraints
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.NetworkType
|
||||||
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
|
import androidx.work.PeriodicWorkRequest
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.background.BackgroundWorker
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
||||||
@@ -17,6 +27,7 @@ import com.futo.platformplayer.states.StateAnnouncement
|
|||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
@@ -27,28 +38,30 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.stream.IntStream.range
|
import java.util.stream.IntStream.range
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
@Serializable()
|
@Serializable()
|
||||||
class SettingsDev : FragmentedStorageFileJson() {
|
class SettingsDev : FragmentedStorageFileJson() {
|
||||||
|
|
||||||
@FormField("Developer Mode", FieldForm.TOGGLE, "", 0)
|
@FormField(R.string.developer_mode, FieldForm.TOGGLE, -1, 0)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var developerMode: Boolean = false;
|
var developerMode: Boolean = false;
|
||||||
|
|
||||||
@FormField("Development Server", FieldForm.GROUP,
|
@FormField(R.string.development_server, FieldForm.GROUP,
|
||||||
"Settings related to development server, be careful as it may open your phone to security vulnerabilities", 1)
|
R.string.settings_related_to_development_server_be_careful_as_it_may_open_your_phone_to_security_vulnerabilities, 1)
|
||||||
val devServerSettings: DeveloperServerFields = DeveloperServerFields();
|
val devServerSettings: DeveloperServerFields = DeveloperServerFields();
|
||||||
@Serializable
|
@Serializable
|
||||||
class DeveloperServerFields {
|
class DeveloperServerFields {
|
||||||
|
|
||||||
@FormField("Start Server on boot", FieldForm.TOGGLE, "", 0)
|
@FormField(R.string.start_server_on_boot, FieldForm.TOGGLE, -1, 0)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var devServerOnBoot: Boolean = false;
|
var devServerOnBoot: Boolean = false;
|
||||||
|
|
||||||
@FormField("Start Server", FieldForm.BUTTON,
|
@FormField(R.string.start_server, FieldForm.BUTTON,
|
||||||
"Starts a DevServer on port 11337, may expose vulnerabilities.", 1)
|
R.string.starts_a_devServer_on_port_11337_may_expose_vulnerabilities, 1)
|
||||||
fun startServer() {
|
fun startServer() {
|
||||||
StateDeveloper.instance.runServer();
|
StateDeveloper.instance.runServer();
|
||||||
StateApp.instance.contextOrNull?.let {
|
StateApp.instance.contextOrNull?.let {
|
||||||
@@ -57,45 +70,57 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Experimental", FieldForm.GROUP,
|
@FormField(R.string.experimental, FieldForm.GROUP,
|
||||||
"Settings related to development server, be careful as it may open your phone to security vulnerabilities", 2)
|
R.string.settings_related_to_development_server_be_careful_as_it_may_open_your_phone_to_security_vulnerabilities, 2)
|
||||||
val experimentalSettings: ExperimentalFields = ExperimentalFields();
|
val experimentalSettings: ExperimentalFields = ExperimentalFields();
|
||||||
@Serializable
|
@Serializable
|
||||||
class ExperimentalFields {
|
class ExperimentalFields {
|
||||||
|
|
||||||
@FormField("Background Subscription Testing", FieldForm.TOGGLE, "", 0)
|
@FormField(R.string.background_subscription_testing, FieldForm.TOGGLE, -1, 0)
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var backgroundSubscriptionFetching: Boolean = false;
|
var backgroundSubscriptionFetching: Boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Crash Me", FieldForm.BUTTON,
|
@FormField(R.string.crash_me, FieldForm.BUTTON,
|
||||||
"Crashes the application on purpose", 2)
|
R.string.crashes_the_application_on_purpose, 2)
|
||||||
fun crashMe() {
|
fun crashMe() {
|
||||||
throw java.lang.IllegalStateException("This is an uncaught exception triggered on purpose!");
|
throw java.lang.IllegalStateException("This is an uncaught exception triggered on purpose!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Delete Announcements", FieldForm.BUTTON,
|
@FormField(R.string.delete_announcements, FieldForm.BUTTON,
|
||||||
"Delete all announcements", 2)
|
R.string.delete_all_announcements, 2)
|
||||||
fun deleteAnnouncements() {
|
fun deleteAnnouncements() {
|
||||||
StateAnnouncement.instance.deleteAllAnnouncements();
|
StateAnnouncement.instance.deleteAllAnnouncements();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Clear Cookies", FieldForm.BUTTON,
|
@FormField(R.string.clear_cookies, FieldForm.BUTTON,
|
||||||
"Clear all cook from the CookieManager", 2)
|
R.string.clear_all_cookies_from_the_cookieManager, 2)
|
||||||
fun clearCookies() {
|
fun clearCookies() {
|
||||||
val cookieManager: CookieManager = CookieManager.getInstance()
|
val cookieManager: CookieManager = CookieManager.getInstance()
|
||||||
cookieManager.removeAllCookies(null);
|
cookieManager.removeAllCookies(null);
|
||||||
}
|
}
|
||||||
|
@FormField(R.string.test_background_worker, FieldForm.BUTTON,
|
||||||
|
R.string.test_background_worker_description, 3)
|
||||||
|
fun triggerBackgroundUpdate() {
|
||||||
|
val act = SettingsActivity.getActivity()!!;
|
||||||
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
|
||||||
|
|
||||||
|
val wm = WorkManager.getInstance(act);
|
||||||
|
val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
|
||||||
|
.setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build())
|
||||||
|
.build();
|
||||||
|
wm.enqueue(req);
|
||||||
|
}
|
||||||
|
|
||||||
@Contextual
|
@Contextual
|
||||||
@Transient
|
@Transient
|
||||||
@FormField("V8 Benchmarks", FieldForm.GROUP,
|
@FormField(R.string.v8_benchmarks, FieldForm.GROUP,
|
||||||
"Various benchmarks using the integrated V8 engine", 3)
|
R.string.various_benchmarks_using_the_integrated_v8_engine, 4)
|
||||||
val v8Benchmarks: V8Benchmarks = V8Benchmarks();
|
val v8Benchmarks: V8Benchmarks = V8Benchmarks();
|
||||||
class V8Benchmarks {
|
class V8Benchmarks {
|
||||||
@FormField(
|
@FormField(
|
||||||
"Test V8 Creation speed", FieldForm.BUTTON,
|
R.string.test_v8_creation_speed, FieldForm.BUTTON,
|
||||||
"Tests V8 creation times and running", 1
|
R.string.tests_v8_creation_times_and_running, 1
|
||||||
)
|
)
|
||||||
fun testV8Creation() {
|
fun testV8Creation() {
|
||||||
var plugin: V8Plugin? = null;
|
var plugin: V8Plugin? = null;
|
||||||
@@ -137,8 +162,8 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
"Test V8 Communication speed", FieldForm.BUTTON,
|
R.string.test_v8_communication_speed, FieldForm.BUTTON,
|
||||||
"Tests V8 communication speeds", 2
|
R.string.tests_v8_communication_speeds, 4
|
||||||
)
|
)
|
||||||
fun testV8RunSpeeds() {
|
fun testV8RunSpeeds() {
|
||||||
var plugin: V8Plugin? = null;
|
var plugin: V8Plugin? = null;
|
||||||
@@ -182,12 +207,12 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@Contextual
|
@Contextual
|
||||||
@Transient
|
@Transient
|
||||||
@FormField("V8 Script Testing", FieldForm.GROUP, "Various tests against a custom source", 4)
|
@FormField(R.string.v8_script_testing, FieldForm.GROUP, R.string.various_tests_against_a_custom_source, 4)
|
||||||
val v8ScriptTests: V8ScriptTests = V8ScriptTests();
|
val v8ScriptTests: V8ScriptTests = V8ScriptTests();
|
||||||
class V8ScriptTests {
|
class V8ScriptTests {
|
||||||
@Contextual
|
@Contextual
|
||||||
private var _currentPlugin : JSClient? = null;
|
private var _currentPlugin : JSClient? = null;
|
||||||
@FormField("Inject", FieldForm.BUTTON, "Injects a test source config (local) into V8", 1)
|
@FormField(R.string.inject, FieldForm.BUTTON, R.string.injects_a_test_source_config_local_into_v8, 1)
|
||||||
fun testV8Init() {
|
fun testV8Init() {
|
||||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -203,7 +228,7 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@FormField("getHome", FieldForm.BUTTON, "Attempts to fetch 2 pages from getHome", 2)
|
@FormField(R.string.getHome, FieldForm.BUTTON, R.string.attempts_to_fetch_2_pages_from_getHome, 2)
|
||||||
fun testV8Home() {
|
fun testV8Home() {
|
||||||
runTestPlugin(_currentPlugin) {
|
runTestPlugin(_currentPlugin) {
|
||||||
var home: IPager<IPlatformContent>? = null;
|
var home: IPager<IPlatformContent>? = null;
|
||||||
@@ -269,27 +294,36 @@ class SettingsDev : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@Contextual
|
@Contextual
|
||||||
@Transient
|
@Transient
|
||||||
@FormField("Other", FieldForm.GROUP, "Others...", 5)
|
@FormField(R.string.other, FieldForm.GROUP, R.string.others_ellipsis, 5)
|
||||||
val otherTests: OtherTests = OtherTests();
|
val otherTests: OtherTests = OtherTests();
|
||||||
class OtherTests {
|
class OtherTests {
|
||||||
@FormField("Clear Downloads", FieldForm.BUTTON, "Deletes all ongoing downloads", 1)
|
@FormField(R.string.unsubscribe_all, FieldForm.BUTTON, R.string.removes_all_subscriptions, -1)
|
||||||
|
fun unsubscribeAll() {
|
||||||
|
val toUnsub = StateSubscriptions.instance.getSubscriptions();
|
||||||
|
UIDialogs.toast("Started unsubbing.. (${toUnsub.size})")
|
||||||
|
toUnsub.forEach {
|
||||||
|
StateSubscriptions.instance.removeSubscription(it.channel.url);
|
||||||
|
};
|
||||||
|
UIDialogs.toast("Finished unsubbing.. (${toUnsub.size})")
|
||||||
|
}
|
||||||
|
@FormField(R.string.clear_downloads, FieldForm.BUTTON, R.string.deletes_all_ongoing_downloads, 1)
|
||||||
fun clearDownloads() {
|
fun clearDownloads() {
|
||||||
StateDownloads.instance.getDownloading().forEach {
|
StateDownloads.instance.getDownloading().forEach {
|
||||||
StateDownloads.instance.removeDownload(it);
|
StateDownloads.instance.removeDownload(it);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@FormField("Clear All Downloaded", FieldForm.BUTTON, "Deletes all downloaded videos and related files", 2)
|
@FormField(R.string.clear_all_downloaded, FieldForm.BUTTON, R.string.deletes_all_downloaded_videos_and_related_files, 2)
|
||||||
fun clearDownloaded() {
|
fun clearDownloaded() {
|
||||||
StateDownloads.instance.getDownloadedVideos().forEach {
|
StateDownloads.instance.getDownloadedVideos().forEach {
|
||||||
StateDownloads.instance.deleteCachedVideo(it.id);
|
StateDownloads.instance.deleteCachedVideo(it.id);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@FormField("Delete Unresolved", FieldForm.BUTTON, "Deletes all unresolved source files", 3)
|
@FormField(R.string.delete_unresolved, FieldForm.BUTTON, R.string.deletes_all_unresolved_source_files, 3)
|
||||||
fun cleanupDownloads() {
|
fun cleanupDownloads() {
|
||||||
StateDownloads.instance.cleanupDownloads();
|
StateDownloads.instance.cleanupDownloads();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Fill storage till error", FieldForm.BUTTON, "Writes to disk till no space is left", 4)
|
@FormField(R.string.fill_storage_till_error, FieldForm.BUTTON, R.string.writes_to_disk_till_no_space_is_left, 4)
|
||||||
fun fillStorage(context: Context, scope: CoroutineScope?) {
|
fun fillStorage(context: Context, scope: CoroutineScope?) {
|
||||||
val gigabuffer = ByteArray(1024 * 1024 * 128);
|
val gigabuffer = ByteArray(1024 * 1024 * 128);
|
||||||
var count: Long = 0;
|
var count: Long = 0;
|
||||||
|
|||||||
@@ -100,12 +100,12 @@ class UIDialogs {
|
|||||||
dialog.show();
|
dialog.show();
|
||||||
};
|
};
|
||||||
if(StateBackup.hasAutomaticBackup() && !skipRestoreCheck)
|
if(StateBackup.hasAutomaticBackup() && !skipRestoreCheck)
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_move_up, "An old backup is available", "Would you like to restore this backup?", null, 0,
|
UIDialogs.showDialog(context, R.drawable.ic_move_up, context.getString(R.string.an_old_backup_is_available), context.getString(R.string.would_you_like_to_restore_this_backup), null, 0,
|
||||||
UIDialogs.Action("Cancel", {}), //To nothing
|
UIDialogs.Action(context.getString(R.string.cancel), {}), //To nothing
|
||||||
UIDialogs.Action("Override", {
|
UIDialogs.Action(context.getString(R.string.override), {
|
||||||
dialogAction();
|
dialogAction();
|
||||||
}, UIDialogs.ActionStyle.DANGEROUS),
|
}, UIDialogs.ActionStyle.DANGEROUS),
|
||||||
UIDialogs.Action("Restore", {
|
UIDialogs.Action(context.getString(R.string.restore), {
|
||||||
UIDialogs.showAutomaticRestoreDialog(context, StateApp.instance.scope);
|
UIDialogs.showAutomaticRestoreDialog(context, StateApp.instance.scope);
|
||||||
}, UIDialogs.ActionStyle.PRIMARY));
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
else {
|
else {
|
||||||
@@ -211,10 +211,10 @@ class UIDialogs {
|
|||||||
(if(ex != null ) "${ex.message}" else ""),
|
(if(ex != null ) "${ex.message}" else ""),
|
||||||
if(ex is PluginException) ex.code else null,
|
if(ex is PluginException) ex.code else null,
|
||||||
0,
|
0,
|
||||||
UIDialogs.Action("Retry", {
|
UIDialogs.Action(context.getString(R.string.retry), {
|
||||||
retryAction?.invoke();
|
retryAction?.invoke();
|
||||||
}, UIDialogs.ActionStyle.PRIMARY),
|
}, UIDialogs.ActionStyle.PRIMARY),
|
||||||
UIDialogs.Action("Close", {
|
UIDialogs.Action(context.getString(R.string.close), {
|
||||||
closeAction?.invoke()
|
closeAction?.invoke()
|
||||||
}, UIDialogs.ActionStyle.NONE)
|
}, UIDialogs.ActionStyle.NONE)
|
||||||
);
|
);
|
||||||
@@ -226,15 +226,15 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun showDataRetryDialog(context: Context, reason: String? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null) {
|
fun showDataRetryDialog(context: Context, reason: String? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null) {
|
||||||
val retryButtonAction = Action("Retry", retryAction ?: {}, ActionStyle.PRIMARY)
|
val retryButtonAction = Action(context.getString(R.string.retry), retryAction ?: {}, ActionStyle.PRIMARY)
|
||||||
val closeButtonAction = Action("Close", closeAction ?: {}, ActionStyle.ACCENT)
|
val closeButtonAction = Action(context.getString(R.string.close), closeAction ?: {}, ActionStyle.ACCENT)
|
||||||
showDialog(context, R.drawable.ic_no_internet_86dp, "Data Retry", reason, null, 0, closeButtonAction, retryButtonAction)
|
showDialog(context, R.drawable.ic_no_internet_86dp, context.getString(R.string.data_retry), reason, null, 0, closeButtonAction, retryButtonAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null) {
|
fun showConfirmationDialog(context: Context, text: String, action: () -> Unit, cancelAction: (() -> Unit)? = null) {
|
||||||
val confirmButtonAction = Action("Confirm", action, ActionStyle.PRIMARY)
|
val confirmButtonAction = Action(context.getString(R.string.confirm), action, ActionStyle.PRIMARY)
|
||||||
val cancelButtonAction = Action("Cancel", cancelAction ?: {}, ActionStyle.ACCENT)
|
val cancelButtonAction = Action(context.getString(R.string.cancel), cancelAction ?: {}, ActionStyle.ACCENT)
|
||||||
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||||
@@ -16,6 +20,7 @@ import com.futo.platformplayer.downloads.VideoLocal
|
|||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.states.*
|
import com.futo.platformplayer.states.*
|
||||||
import com.futo.platformplayer.views.Loader
|
import com.futo.platformplayer.views.Loader
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
||||||
@@ -45,6 +50,66 @@ class UISlideOverlays {
|
|||||||
menu.show();
|
menu.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) {
|
||||||
|
val items = arrayListOf<View>();
|
||||||
|
var menu: SlideUpMenuOverlay? = null;
|
||||||
|
|
||||||
|
val originalNotif = subscription.doNotifications;
|
||||||
|
val originalLive = subscription.doFetchLive;
|
||||||
|
val originalStream = subscription.doFetchStreams;
|
||||||
|
val originalVideo = subscription.doFetchVideos;
|
||||||
|
val originalPosts = subscription.doFetchPosts;
|
||||||
|
|
||||||
|
items.addAll(listOf(
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
||||||
|
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||||
|
}, false),
|
||||||
|
SlideUpMenuGroup(container.context, "Fetch Settings",
|
||||||
|
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||||
|
-1, listOf()),
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_live_tv, "Livestreams", "Check for livestreams", "fetchLive", {
|
||||||
|
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
||||||
|
}, false),
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_play, "Streams", "Check for finished streams", "fetchStreams", {
|
||||||
|
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchLive;
|
||||||
|
}, false),
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_play, "Videos", "Check for videos", "fetchVideos", {
|
||||||
|
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchLive;
|
||||||
|
}, false),
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_chat, "Posts", "Check for posts", "fetchPosts", {
|
||||||
|
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchLive;
|
||||||
|
}, false)));
|
||||||
|
|
||||||
|
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
|
||||||
|
|
||||||
|
if(subscription.doNotifications)
|
||||||
|
menu.selectOption(null, "notifications", true, true);
|
||||||
|
if(subscription.doFetchLive)
|
||||||
|
menu.selectOption(null, "fetchLive", true, true);
|
||||||
|
if(subscription.doFetchStreams)
|
||||||
|
menu.selectOption(null, "fetchStreams", true, true);
|
||||||
|
if(subscription.doFetchVideos)
|
||||||
|
menu.selectOption(null, "fetchVideos", true, true);
|
||||||
|
if(subscription.doFetchPosts)
|
||||||
|
menu.selectOption(null, "fetchPosts", true, true);
|
||||||
|
|
||||||
|
menu.onOK.subscribe {
|
||||||
|
subscription.save();
|
||||||
|
menu.hide(true);
|
||||||
|
};
|
||||||
|
menu.onCancel.subscribe {
|
||||||
|
subscription.doNotifications = originalNotif;
|
||||||
|
subscription.doFetchLive = originalLive;
|
||||||
|
subscription.doFetchStreams = originalStream;
|
||||||
|
subscription.doFetchVideos = originalVideo;
|
||||||
|
subscription.doFetchPosts = originalPosts;
|
||||||
|
};
|
||||||
|
|
||||||
|
menu.setOk("Save");
|
||||||
|
|
||||||
|
menu.show();
|
||||||
|
}
|
||||||
|
|
||||||
fun showDownloadVideoOverlay(video: IPlatformVideoDetails, container: ViewGroup, contentResolver: ContentResolver? = null): SlideUpMenuOverlay? {
|
fun showDownloadVideoOverlay(video: IPlatformVideoDetails, container: ViewGroup, contentResolver: ContentResolver? = null): SlideUpMenuOverlay? {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
var menu: SlideUpMenuOverlay? = null;
|
var menu: SlideUpMenuOverlay? = null;
|
||||||
@@ -64,22 +129,22 @@ class UISlideOverlays {
|
|||||||
val subtitleSources = video.subtitles;
|
val subtitleSources = video.subtitles;
|
||||||
|
|
||||||
if(videoSources.size == 0 && (audioSources?.size ?: 0) == 0) {
|
if(videoSources.size == 0 && (audioSources?.size ?: 0) == 0) {
|
||||||
UIDialogs.toast("No downloads available", false);
|
UIDialogs.toast(container.context.getString(R.string.no_downloads_available), false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!VideoHelper.isDownloadable(video)) {
|
if(!VideoHelper.isDownloadable(video)) {
|
||||||
Logger.i(TAG, "Attempted to open downloads without valid sources for [${video.name}]: ${video.url}");
|
Logger.i(TAG, "Attempted to open downloads without valid sources for [${video.name}]: ${video.url}");
|
||||||
UIDialogs.toast( "No downloadable sources (yet)");
|
UIDialogs.toast( container.context.getString(R.string.no_downloadable_sources_yet));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, "Video", videoSources,
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
||||||
listOf(listOf(SlideUpMenuItem(container.context, R.drawable.ic_movie, "None", "Audio Only", "none", {
|
listOf(listOf(SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.none), container.context.getString(R.string.audio_only), "none", {
|
||||||
selectedVideo = null;
|
selectedVideo = null;
|
||||||
menu?.selectOption(videoSources, "none");
|
menu?.selectOption(videoSources, "none");
|
||||||
if(selectedAudio != null || !requiresAudio)
|
if(selectedAudio != null || !requiresAudio)
|
||||||
menu?.setOk("Download");
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
}, false)) +
|
}, false)) +
|
||||||
videoSources
|
videoSources
|
||||||
.filter { it.isDownloadable() }
|
.filter { it.isDownloadable() }
|
||||||
@@ -88,7 +153,7 @@ class UISlideOverlays {
|
|||||||
selectedVideo = it as IVideoUrlSource;
|
selectedVideo = it as IVideoUrlSource;
|
||||||
menu?.selectOption(videoSources, it);
|
menu?.selectOption(videoSources, it);
|
||||||
if(selectedAudio != null || !requiresAudio)
|
if(selectedAudio != null || !requiresAudio)
|
||||||
menu?.setOk("Download");
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
}, false)
|
}, false)
|
||||||
}).flatten().toList()
|
}).flatten().toList()
|
||||||
));
|
));
|
||||||
@@ -100,13 +165,13 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
|
|
||||||
audioSources?.let { audioSources ->
|
audioSources?.let { audioSources ->
|
||||||
items.add(SlideUpMenuGroup(container.context, "Audio", audioSources, audioSources
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.audio), audioSources, audioSources
|
||||||
.filter { VideoHelper.isDownloadable(it) }
|
.filter { VideoHelper.isDownloadable(it) }
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
||||||
selectedAudio = it as IAudioUrlSource;
|
selectedAudio = it as IAudioUrlSource;
|
||||||
menu?.selectOption(audioSources, it);
|
menu?.selectOption(audioSources, it);
|
||||||
menu?.setOk("Download");
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
}, false);
|
}, false);
|
||||||
}));
|
}));
|
||||||
val asources = audioSources;
|
val asources = audioSources;
|
||||||
@@ -125,7 +190,7 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
//ContentResolver is required for subtitles..
|
//ContentResolver is required for subtitles..
|
||||||
if(contentResolver != null) {
|
if(contentResolver != null) {
|
||||||
items.add(SlideUpMenuGroup(container.context, "Subtitles", subtitleSources, subtitleSources
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
|
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
|
||||||
if (selectedSubtitle == it) {
|
if (selectedSubtitle == it) {
|
||||||
@@ -139,7 +204,7 @@ class UISlideOverlays {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
menu = SlideUpMenuOverlay(container.context, container, "Download Video", null, true, items);
|
menu = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items);
|
||||||
|
|
||||||
if(selectedVideo != null) {
|
if(selectedVideo != null) {
|
||||||
menu.selectOption(videoSources, selectedVideo);
|
menu.selectOption(videoSources, selectedVideo);
|
||||||
@@ -148,7 +213,7 @@ class UISlideOverlays {
|
|||||||
audioSources?.let { audioSources -> menu.selectOption(audioSources, selectedAudio); };
|
audioSources?.let { audioSources -> menu.selectOption(audioSources, selectedAudio); };
|
||||||
}
|
}
|
||||||
if(selectedAudio != null || (!requiresAudio && selectedVideo != null)) {
|
if(selectedAudio != null || (!requiresAudio && selectedVideo != null)) {
|
||||||
menu.setOk("Download");
|
menu.setOk(container.context.getString(R.string.download));
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.onOK.subscribe {
|
menu.onOK.subscribe {
|
||||||
@@ -185,7 +250,7 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
fun showDownloadVideoOverlay(video: IPlatformVideo, container: ViewGroup, useDetails: Boolean = false) {
|
fun showDownloadVideoOverlay(video: IPlatformVideo, container: ViewGroup, useDetails: Boolean = false) {
|
||||||
val handleUnknownDownload: ()->Unit = {
|
val handleUnknownDownload: ()->Unit = {
|
||||||
showUnknownVideoDownload("Video", container) { px, bitrate ->
|
showUnknownVideoDownload(container.context.getString(R.string.video), container) { px, bitrate ->
|
||||||
StateDownloads.instance.download(video, px, bitrate)
|
StateDownloads.instance.download(video, px, bitrate)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -195,7 +260,7 @@ class UISlideOverlays {
|
|||||||
val scope = StateApp.instance.scopeOrNull;
|
val scope = StateApp.instance.scopeOrNull;
|
||||||
|
|
||||||
if(scope != null) {
|
if(scope != null) {
|
||||||
val loader = showLoaderOverlay("Fetching video details", container);
|
val loader = showLoaderOverlay(container.context.getString(R.string.fetching_video_details), container);
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val videoDetails = StatePlatform.instance.getContentDetails(video.url, false).await();
|
val videoDetails = StatePlatform.instance.getContentDetails(video.url, false).await();
|
||||||
@@ -209,7 +274,7 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast("Failed to fetch details for download");
|
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download));
|
||||||
handleUnknownDownload();
|
handleUnknownDownload();
|
||||||
loader.hide(true);
|
loader.hide(true);
|
||||||
}
|
}
|
||||||
@@ -220,7 +285,7 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
|
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
|
||||||
showUnknownVideoDownload("Video", container) { px, bitrate ->
|
showUnknownVideoDownload(container.context.getString(R.string.video), container) { px, bitrate ->
|
||||||
StateDownloads.instance.download(playlist, px, bitrate);
|
StateDownloads.instance.download(playlist, px, bitrate);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -232,7 +297,7 @@ class UISlideOverlays {
|
|||||||
var targetBitrate: Long = 0;
|
var targetBitrate: Long = 0;
|
||||||
|
|
||||||
val resolutions = listOf(
|
val resolutions = listOf(
|
||||||
Triple<String, String, Long>("None", "None", -1),
|
Triple<String, String, Long>(container.context.getString(R.string.none), container.context.getString(R.string.none), -1),
|
||||||
Triple<String, String, Long>("480P", "720x480", 720*480),
|
Triple<String, String, Long>("480P", "720x480", 720*480),
|
||||||
Triple<String, String, Long>("720P", "1280x720", 1280*720),
|
Triple<String, String, Long>("720P", "1280x720", 1280*720),
|
||||||
Triple<String, String, Long>("1080P", "1920x1080", 1920*1080),
|
Triple<String, String, Long>("1080P", "1920x1080", 1920*1080),
|
||||||
@@ -240,23 +305,23 @@ class UISlideOverlays {
|
|||||||
Triple<String, String, Long>("2160P", "3840x2160", 3840*2160)
|
Triple<String, String, Long>("2160P", "3840x2160", 3840*2160)
|
||||||
);
|
);
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, "Target Resolution", "Video", resolutions.map {
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_resolution), "Video", resolutions.map {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.first, it.second, it.third, {
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.first, it.second, it.third, {
|
||||||
targetPxSize = it.third;
|
targetPxSize = it.third;
|
||||||
menu?.selectOption("Video", it.third);
|
menu?.selectOption("Video", it.third);
|
||||||
}, false)
|
}, false)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, "Target Bitrate", "Bitrate", listOf(
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.target_bitrate), "Bitrate", listOf(
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, "Low Bitrate", "", 1, {
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.low_bitrate), "", 1, {
|
||||||
targetBitrate = 1;
|
targetBitrate = 1;
|
||||||
menu?.selectOption("Bitrate", 1);
|
menu?.selectOption("Bitrate", 1);
|
||||||
menu?.setOk("Download");
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
}, false),
|
}, false),
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, "High Bitrate", "", 9999999, {
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, container.context.getString(R.string.high_bitrate), "", 9999999, {
|
||||||
targetBitrate = 9999999;
|
targetBitrate = 9999999;
|
||||||
menu?.selectOption("Bitrate", 9999999);
|
menu?.selectOption("Bitrate", 9999999);
|
||||||
menu?.setOk("Download");
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
}, false)
|
}, false)
|
||||||
)));
|
)));
|
||||||
|
|
||||||
@@ -277,12 +342,12 @@ class UISlideOverlays {
|
|||||||
if(Settings.instance.downloads.isHighBitrateDefault()) {
|
if(Settings.instance.downloads.isHighBitrateDefault()) {
|
||||||
targetBitrate = 9999999;
|
targetBitrate = 9999999;
|
||||||
menu.selectOption("Bitrate", 9999999);
|
menu.selectOption("Bitrate", 9999999);
|
||||||
menu.setOk("Download");
|
menu.setOk(container.context.getString(R.string.download));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
targetBitrate = 1;
|
targetBitrate = 1;
|
||||||
menu.selectOption("Bitrate", 1);
|
menu.selectOption("Bitrate", 1);
|
||||||
menu.setOk("Download");
|
menu.setOk(container.context.getString(R.string.download));
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.onOK.subscribe {
|
menu.onOK.subscribe {
|
||||||
@@ -304,14 +369,14 @@ class UISlideOverlays {
|
|||||||
return overlay;
|
return overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, onVideoHidden: (()->Unit)? = null): SlideUpMenuOverlay {
|
fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, vararg actions: SlideUpMenuItem): SlideUpMenuOverlay {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
||||||
|
|
||||||
if (lastUpdated != null) {
|
if (lastUpdated != null) {
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, "Recently Used Playlist", "recentlyusedplaylist",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} videos", "",
|
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "",
|
||||||
{
|
{
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
@@ -322,23 +387,28 @@ class UISlideOverlays {
|
|||||||
val allPlaylists = StatePlaylists.instance.getPlaylists();
|
val allPlaylists = StatePlaylists.instance.getPlaylists();
|
||||||
val queue = StatePlayer.instance.getQueue();
|
val queue = StatePlayer.instance.getQueue();
|
||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(SlideUpMenuGroup(container.context, "Actions", "actions",
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, "Hide", "Hide from Home", "hide",
|
(listOf(
|
||||||
{ StateMeta.instance.addHiddenVideo(video.url); onVideoHidden?.invoke() }),
|
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download), {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_download, "Download", "Download the video", "download",
|
showDownloadVideoOverlay(video, container, true);
|
||||||
{ showDownloadVideoOverlay(video, container, true); }, false)
|
}, false),
|
||||||
))
|
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
|
||||||
|
StateMeta.instance.addHiddenCreator(video.author.url);
|
||||||
|
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
||||||
|
}))
|
||||||
|
+ actions)
|
||||||
|
));
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, "Add To", "addto",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, "Add to Queue", "${queue.size} videos", "queue",
|
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.add_to_queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
|
||||||
{ StatePlayer.instance.addToQueue(video); }),
|
{ StatePlayer.instance.addToQueue(video); }),
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, "Add to " + StatePlayer.TYPE_WATCHLATER + "", "${watchLater.size} videos", "watch later",
|
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, "${container.context.getString(R.string.add_to)} " + StatePlayer.TYPE_WATCHLATER + "", "${watchLater.size} " + container.context.getString(R.string.videos), "watch later",
|
||||||
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); })
|
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); })
|
||||||
));
|
));
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||||
for (playlist in allPlaylists) {
|
for (playlist in allPlaylists) {
|
||||||
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, "Add to " + playlist.name + "", "${playlist.videos.size} videos", "",
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, "${container.context.getString(R.string.add_to)} " + playlist.name + "", "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
||||||
{
|
{
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
@@ -346,9 +416,9 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(playlistItems.size > 0)
|
if(playlistItems.size > 0)
|
||||||
items.add(SlideUpMenuGroup(container.context, "Playlists", "", playlistItems));
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.playlists), "", playlistItems));
|
||||||
|
|
||||||
return SlideUpMenuOverlay(container.context, container, "Video Options", null, true, items).apply { show() };
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.video_options), null, true, items).apply { show() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -360,8 +430,8 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
if (lastUpdated != null) {
|
if (lastUpdated != null) {
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, "Recently Used Playlist", "recentlyusedplaylist",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} videos", "",
|
SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, lastUpdated.name, "${lastUpdated.videos.size} " + container.context.getString(R.string.videos), "",
|
||||||
{
|
{
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
@@ -373,18 +443,18 @@ class UISlideOverlays {
|
|||||||
val queue = StatePlayer.instance.getQueue();
|
val queue = StatePlayer.instance.getQueue();
|
||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(
|
items.add(
|
||||||
SlideUpMenuGroup(container.context, "Other", "other",
|
SlideUpMenuGroup(container.context, container.context.getString(R.string.other), "other",
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, "Queue", "${queue.size} videos", "queue",
|
SlideUpMenuItem(container.context, R.drawable.ic_queue_add, container.context.getString(R.string.queue), "${queue.size} " + container.context.getString(R.string.videos), "queue",
|
||||||
{ StatePlayer.instance.addToQueue(video); }),
|
{ StatePlayer.instance.addToQueue(video); }),
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, StatePlayer.TYPE_WATCHLATER, "${watchLater.size} videos", "watch later",
|
SlideUpMenuItem(container.context, R.drawable.ic_watchlist_add, StatePlayer.TYPE_WATCHLATER, "${watchLater.size} " + container.context.getString(R.string.videos), "watch later",
|
||||||
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
{ StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_download, "Download", "Download the video", "download",
|
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download),
|
||||||
{ showDownloadVideoOverlay(video, container, true); }, false))
|
{ showDownloadVideoOverlay(video, container, true); }, false))
|
||||||
);
|
);
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||||
for (playlist in allPlaylists) {
|
for (playlist in allPlaylists) {
|
||||||
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} videos", "",
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
||||||
{
|
{
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
@@ -392,9 +462,9 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(playlistItems.size > 0)
|
if(playlistItems.size > 0)
|
||||||
items.add(SlideUpMenuGroup(container.context, "Playlists", "", playlistItems));
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.playlists), "", playlistItems));
|
||||||
|
|
||||||
return SlideUpMenuOverlay(container.context, container, "Add to", null, true, items).apply { show() };
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters {
|
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters {
|
||||||
@@ -412,8 +482,8 @@ class UISlideOverlays {
|
|||||||
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
|
.map { btn -> SlideUpMenuItem(container.context, btn.iconResource, btn.text.text.toString(), "", "", {
|
||||||
btn.handler?.invoke(btn);
|
btn.handler?.invoke(btn);
|
||||||
}, true) as View }.toTypedArray() ?: arrayOf(),
|
}, true) as View }.toTypedArray() ?: arrayOf(),
|
||||||
arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, "Change Pins", "Decide which buttons should be pinned", "", {
|
arrayOf(SlideUpMenuItem(container.context, R.drawable.ic_pin, container.context.getString(R.string.change_pins), container.context.getString(R.string.decide_which_buttons_should_be_pinned), "", {
|
||||||
showOrderOverlay(container, "Select your pins in order", (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
|
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
|
||||||
val selected = it
|
val selected = it
|
||||||
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
||||||
.filter { it != null }
|
.filter { it != null }
|
||||||
@@ -425,7 +495,7 @@ class UISlideOverlays {
|
|||||||
}, false))
|
}, false))
|
||||||
).flatten().toTypedArray();
|
).flatten().toTypedArray();
|
||||||
|
|
||||||
return SlideUpMenuOverlay(container.context, container, "More Options", null, true, *views).apply { show() };
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit) {
|
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit) {
|
||||||
@@ -433,7 +503,7 @@ class UISlideOverlays {
|
|||||||
|
|
||||||
var overlay: SlideUpMenuOverlay? = null;
|
var overlay: SlideUpMenuOverlay? = null;
|
||||||
|
|
||||||
overlay = SlideUpMenuOverlay(container.context, container, title, "Save", true,
|
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
|
||||||
options.map { SlideUpMenuItem(container.context, R.drawable.ic_move_up, it.first, "", it.second, {
|
options.map { SlideUpMenuItem(container.context, R.drawable.ic_move_up, it.first, "", it.second, {
|
||||||
if(overlay!!.selectOption(null, it.second, true, true)) {
|
if(overlay!!.selectOption(null, it.second, true, true)) {
|
||||||
if(!selection.contains(it.second))
|
if(!selection.contains(it.second))
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ fun ensureNotMainThread() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _regexUrl = Regex("https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&\\/\\/=]*)");
|
||||||
|
fun String.isHttpUrl(): Boolean {
|
||||||
|
return _regexUrl.matchEntire(this) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private val _regexHexColor = Regex("(#[a-fA-F0-9]{8})|(#[a-fA-F0-9]{6})|(#[a-fA-F0-9]{3})");
|
private val _regexHexColor = Regex("(#[a-fA-F0-9]{8})|(#[a-fA-F0-9]{6})|(#[a-fA-F0-9]{3})");
|
||||||
fun String.isHexColor(): Boolean {
|
fun String.isHexColor(): Boolean {
|
||||||
return _regexHexColor.matches(this);
|
return _regexHexColor.matches(this);
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
var url = intent?.dataString;
|
var url = intent?.dataString;
|
||||||
|
|
||||||
if(url == null)
|
if(url == null)
|
||||||
UIDialogs.showDialog(this, R.drawable.ic_error, "No valid URL provided..", null, null,
|
UIDialogs.showDialog(this, R.drawable.ic_error, getString(R.string.no_valid_url_provided), null, null,
|
||||||
0, UIDialogs.Action("Ok", { finish() }, UIDialogs.ActionStyle.PRIMARY));
|
0, UIDialogs.Action(getString(R.string.ok), { finish() }, UIDialogs.ActionStyle.PRIMARY));
|
||||||
else {
|
else {
|
||||||
if(url.startsWith("vfuto://"))
|
if(url.startsWith("vfuto://"))
|
||||||
url = "https://" + url.substring("vfuto://".length);
|
url = "https://" + url.substring("vfuto://".length);
|
||||||
@@ -129,14 +129,14 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
Logger.e(TAG, "Failed decode config", ex);
|
Logger.e(TAG, "Failed decode config", ex);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.showDialog(this@AddSourceActivity, R.drawable.ic_error,
|
UIDialogs.showDialog(this@AddSourceActivity, R.drawable.ic_error,
|
||||||
"Invalid Config Format", null, null,
|
getString(R.string.invalid_config_format), null, null,
|
||||||
0, UIDialogs.Action("Ok", { finish() }, UIDialogs.ActionStyle.PRIMARY));
|
0, UIDialogs.Action("Ok", { finish() }, UIDialogs.ActionStyle.PRIMARY));
|
||||||
};
|
};
|
||||||
return@launch;
|
return@launch;
|
||||||
} catch(ex: Exception) {
|
} catch(ex: Exception) {
|
||||||
Logger.e(TAG, "Failed fetch config", ex);
|
Logger.e(TAG, "Failed fetch config", ex);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.showGeneralErrorDialog(this@AddSourceActivity, "Failed to fetch configuration", ex);
|
UIDialogs.showGeneralErrorDialog(this@AddSourceActivity, getString(R.string.failed_to_fetch_configuration), ex);
|
||||||
};
|
};
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Logger.e(TAG, "Failed fetch script", ex);
|
Logger.e(TAG, "Failed fetch script", ex);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.showGeneralErrorDialog(this@AddSourceActivity, "Failed to fetch script", ex);
|
UIDialogs.showGeneralErrorDialog(this@AddSourceActivity, getString(R.string.failed_to_fetch_script), ex);
|
||||||
};
|
};
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
@@ -175,8 +175,8 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
_sourcePermissions.addView(
|
_sourcePermissions.addView(
|
||||||
SourceInfoView(this,
|
SourceInfoView(this,
|
||||||
R.drawable.ic_language,
|
R.drawable.ic_language,
|
||||||
"URL Access",
|
getString(R.string.url_access),
|
||||||
"The plugin will have access to the following domains",
|
getString(R.string.the_plugin_will_have_access_to_the_following_domains),
|
||||||
config.allowUrls, true)
|
config.allowUrls, true)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -184,8 +184,8 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
_sourcePermissions.addView(
|
_sourcePermissions.addView(
|
||||||
SourceInfoView(this,
|
SourceInfoView(this,
|
||||||
R.drawable.ic_code,
|
R.drawable.ic_code,
|
||||||
"Eval Access",
|
getString(R.string.eval_access),
|
||||||
"The plugin will have access to eval capability (remote injection)",
|
getString(R.string.the_plugin_will_have_access_to_eval_capability_remote_injection),
|
||||||
config.allowUrls, true)
|
config.allowUrls, true)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,13 +16,14 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
lateinit var _buttonQR: BigButton;
|
lateinit var _buttonQR: BigButton;
|
||||||
lateinit var _buttonURL: BigButton;
|
lateinit var _buttonURL: BigButton;
|
||||||
|
lateinit var _buttonPlugins: BigButton;
|
||||||
|
|
||||||
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
scanResult?.let {
|
scanResult?.let {
|
||||||
val content = it.contents
|
val content = it.contents
|
||||||
if (content == null) {
|
if (content == null) {
|
||||||
UIDialogs.toast(this, "Failed to scan QR code")
|
UIDialogs.toast(this, getString(R.string.failed_to_scan_qr_code))
|
||||||
return@let
|
return@let
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
} else if (content.startsWith("grayjay://plugin/")) {
|
} else if (content.startsWith("grayjay://plugin/")) {
|
||||||
content.substring("grayjay://plugin/".length)
|
content.substring("grayjay://plugin/".length)
|
||||||
} else {
|
} else {
|
||||||
UIDialogs.toast(this, "Not a plugin URL")
|
UIDialogs.toast(this, getString(R.string.not_a_plugin_url))
|
||||||
return@let;
|
return@let;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
_buttonQR = findViewById(R.id.option_qr);
|
_buttonQR = findViewById(R.id.option_qr);
|
||||||
_buttonURL = findViewById(R.id.option_url);
|
_buttonURL = findViewById(R.id.option_url);
|
||||||
|
_buttonPlugins = findViewById(R.id.option_plugins);
|
||||||
|
|
||||||
_buttonBack.setOnClickListener {
|
_buttonBack.setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
@@ -59,7 +61,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
_buttonQR.onClick.subscribe {
|
_buttonQR.onClick.subscribe {
|
||||||
val integrator = IntentIntegrator(this);
|
val integrator = IntentIntegrator(this);
|
||||||
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||||
integrator.setPrompt("Scan a QR Code")
|
integrator.setPrompt(getString(R.string.scan_a_qr_code))
|
||||||
integrator.setOrientationLocked(true);
|
integrator.setOrientationLocked(true);
|
||||||
integrator.setCameraId(0)
|
integrator.setCameraId(0)
|
||||||
integrator.setBeepEnabled(false)
|
integrator.setBeepEnabled(false)
|
||||||
@@ -69,12 +71,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buttonURL.onClick.subscribe {
|
_buttonURL.onClick.subscribe {
|
||||||
UIDialogs.toast(this, "Not implemented yet..");
|
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class QRCaptureActivity: CaptureActivity() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -38,8 +38,8 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
_buttonRestart = findViewById(R.id.button_restart);
|
_buttonRestart = findViewById(R.id.button_restart);
|
||||||
_buttonClose = findViewById(R.id.button_close);
|
_buttonClose = findViewById(R.id.button_close);
|
||||||
|
|
||||||
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: "Unknown Context";
|
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: getString(R.string.unknown_context);
|
||||||
val stack = intent.getStringExtra(EXTRA_STACK) ?: "Something went wrong... missing stack trace?";
|
val stack = intent.getStringExtra(EXTRA_STACK) ?: getString(R.string.something_went_wrong_missing_stack_trace);
|
||||||
|
|
||||||
val exceptionString = "Version information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE})\n" +
|
val exceptionString = "Version information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE})\n" +
|
||||||
"Device information (brand= ${Build.BRAND}, manufacturer = ${Build.MANUFACTURER}, device = ${Build.DEVICE}, version-sdk = ${Build.VERSION.SDK_INT}, version-os = ${Build.VERSION.BASE_OS})\n\n" +
|
"Device information (brand= ${Build.BRAND}, manufacturer = ${Build.MANUFACTURER}, device = ${Build.DEVICE}, version-sdk = ${Build.VERSION.SDK_INT}, version-os = ${Build.VERSION.BASE_OS})\n\n" +
|
||||||
@@ -79,13 +79,13 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun submitFile() {
|
private fun submitFile() {
|
||||||
if (_submitted) {
|
if (_submitted) {
|
||||||
Toast.makeText(this, "Logs already submitted.", Toast.LENGTH_LONG).show();
|
Toast.makeText(this, getString(R.string.logs_already_submitted), Toast.LENGTH_LONG).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
val file = _file;
|
val file = _file;
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
Toast.makeText(this, "No logs found.", Toast.LENGTH_LONG).show();
|
Toast.makeText(this, getString(R.string.no_logs_found), Toast.LENGTH_LONG).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,14 +101,14 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
try {
|
try {
|
||||||
Toast.makeText(this@ExceptionActivity, "Failed automated share, share manually?", Toast.LENGTH_LONG).show();
|
Toast.makeText(this@ExceptionActivity, getString(R.string.failed_automated_share_share_manually), Toast.LENGTH_LONG).show();
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_submitted = true;
|
_submitted = true;
|
||||||
file.delete();
|
file.delete();
|
||||||
Toast.makeText(this@ExceptionActivity, "Shared $id", Toast.LENGTH_LONG).show();
|
Toast.makeText(this@ExceptionActivity, getString(R.string.shared_id).replace("{id}", id), Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,10 +119,10 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
val i = Intent(Intent.ACTION_SEND);
|
val i = Intent(Intent.ACTION_SEND);
|
||||||
i.type = "text/plain";
|
i.type = "text/plain";
|
||||||
i.putExtra(Intent.EXTRA_EMAIL, arrayOf("grayjay@futo.org"));
|
i.putExtra(Intent.EXTRA_EMAIL, arrayOf("grayjay@futo.org"));
|
||||||
i.putExtra(Intent.EXTRA_SUBJECT, "Unhandled exception in VS");
|
i.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.unhandled_exception_in_vs));
|
||||||
i.putExtra(Intent.EXTRA_TEXT, exceptionString);
|
i.putExtra(Intent.EXTRA_TEXT, exceptionString);
|
||||||
|
|
||||||
startActivity(Intent.createChooser(i, "Send exception to developers..."));
|
startActivity(Intent.createChooser(i, getString(R.string.send_exception_to_developers)));
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored
|
//Ignored
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import android.webkit.ConsoleMessage
|
|||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import android.webkit.WebChromeClient
|
import android.webkit.WebChromeClient
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
@@ -23,6 +25,8 @@ import kotlinx.serialization.json.Json
|
|||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
class LoginActivity : AppCompatActivity() {
|
||||||
private lateinit var _webView: WebView;
|
private lateinit var _webView: WebView;
|
||||||
|
private lateinit var _textUrl: TextView;
|
||||||
|
private lateinit var _buttonClose: ImageButton;
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -30,6 +34,13 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
setContentView(R.layout.activity_login);
|
setContentView(R.layout.activity_login);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
|
_textUrl = findViewById(R.id.text_url);
|
||||||
|
_buttonClose = findViewById(R.id.button_close);
|
||||||
|
_buttonClose.setOnClickListener {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
_webView = findViewById(R.id.web_view);
|
_webView = findViewById(R.id.web_view);
|
||||||
_webView.settings.javaScriptEnabled = true;
|
_webView.settings.javaScriptEnabled = true;
|
||||||
CookieManager.getInstance().setAcceptCookie(true);
|
CookieManager.getInstance().setAcceptCookie(true);
|
||||||
@@ -60,6 +71,8 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
};
|
};
|
||||||
var isFirstLoad = true;
|
var isFirstLoad = true;
|
||||||
webViewClient.onPageLoaded.subscribe { view, url ->
|
webViewClient.onPageLoaded.subscribe { view, url ->
|
||||||
|
_textUrl.setText(url ?: "");
|
||||||
|
|
||||||
if(!isFirstLoad)
|
if(!isFirstLoad)
|
||||||
return@subscribe;
|
return@subscribe;
|
||||||
isFirstLoad = false;
|
isFirstLoad = false;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.content.pm.ActivityInfo
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.preference.PreferenceManager
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
@@ -154,6 +155,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
Logger.i(TAG, "MainActivity.attachBaseContext")
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
||||||
StateApp.instance.mainAppStarting(this);
|
StateApp.instance.mainAppStarting(this);
|
||||||
@@ -459,6 +465,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Logger.i(TAG, "View Received: " + targetData);
|
Logger.i(TAG, "View Received: " + targetData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"VIDEO" -> {
|
||||||
|
val url = intent.getStringExtra("VIDEO");
|
||||||
|
navigate(_fragVideoDetail, url);
|
||||||
|
}
|
||||||
"TAB" -> {
|
"TAB" -> {
|
||||||
when(intent.getStringExtra("TAB")){
|
when(intent.getStringExtra("TAB")){
|
||||||
"Sources" -> {
|
"Sources" -> {
|
||||||
@@ -478,13 +488,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if(targetData.startsWith("grayjay://license/")) {
|
if(targetData.startsWith("grayjay://license/")) {
|
||||||
if(StatePayment.instance.setPaymentLicenseUrl(targetData))
|
if(StatePayment.instance.setPaymentLicenseUrl(targetData))
|
||||||
{
|
{
|
||||||
UIDialogs.showDialogOk(this, R.drawable.ic_check, "Your license key has been set!\nAn app restart might be required.");
|
UIDialogs.showDialogOk(this, R.drawable.ic_check, getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required));
|
||||||
|
|
||||||
if(fragCurrent is BuyFragment)
|
if(fragCurrent is BuyFragment)
|
||||||
closeSegment(fragCurrent);
|
closeSegment(fragCurrent);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
UIDialogs.toast("Invalid license format");
|
UIDialogs.toast(getString(R.string.invalid_license_format));
|
||||||
|
|
||||||
}
|
}
|
||||||
else if(targetData.startsWith("grayjay://plugin/")) {
|
else if(targetData.startsWith("grayjay://plugin/")) {
|
||||||
@@ -493,13 +503,21 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
};
|
};
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
else if(targetData.startsWith("grayjay://video/")) {
|
||||||
|
val videoUrl = targetData.substring("grayjay://video/".length);
|
||||||
|
navigate(_fragVideoDetail, videoUrl);
|
||||||
|
}
|
||||||
|
else if(targetData.startsWith("grayjay://channel/")) {
|
||||||
|
val channelUrl = targetData.substring("grayjay://channel/".length);
|
||||||
|
navigate(_fragMainChannel, channelUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"content" -> {
|
"content" -> {
|
||||||
if(!handleContent(targetData, intent.type)) {
|
if(!handleContent(targetData, intent.type)) {
|
||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
this,
|
this,
|
||||||
R.drawable.ic_play,
|
R.drawable.ic_play,
|
||||||
"Unknown content format [${targetData}]",
|
getString(R.string.unknown_content_format) + " [${targetData}]",
|
||||||
"Ok",
|
"Ok",
|
||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
@@ -509,7 +527,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
this,
|
this,
|
||||||
R.drawable.ic_play,
|
R.drawable.ic_play,
|
||||||
"Unknown file format [${targetData}]",
|
getString(R.string.unknown_file_format) + " [${targetData}]",
|
||||||
"Ok",
|
"Ok",
|
||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
@@ -519,7 +537,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
this,
|
this,
|
||||||
R.drawable.ic_play,
|
R.drawable.ic_play,
|
||||||
"Unknown Polycentric format [${targetData}]",
|
getString(R.string.unknown_polycentric_format) + " [${targetData}]",
|
||||||
"Ok",
|
"Ok",
|
||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
@@ -529,7 +547,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
this,
|
this,
|
||||||
R.drawable.ic_play,
|
R.drawable.ic_play,
|
||||||
"Unknown url format [${targetData}]",
|
getString(R.string.unknown_url_format) + " [${targetData}]",
|
||||||
"Ok",
|
"Ok",
|
||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
@@ -538,7 +556,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
UIDialogs.showGeneralErrorDialog(this, "Failed to handle file", ex);
|
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_handle_file), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,6 +597,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
StateBackup.importZipBytes(this, lifecycleScope, data);
|
StateBackup.importZipBytes(this, lifecycleScope, data);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if(file.lowercase().endsWith(".txt") || mime == "text/plain") {
|
||||||
|
return handleUnknownText(String(data));
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
fun handleFile(file: String): Boolean {
|
fun handleFile(file: String): Boolean {
|
||||||
@@ -596,6 +617,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
StateBackup.importZipBytes(this, lifecycleScope, readSharedFile(file));
|
StateBackup.importZipBytes(this, lifecycleScope, readSharedFile(file));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if(file.lowercase().endsWith(".txt")) {
|
||||||
|
return handleUnknownText(String(readSharedFile(file)));
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
fun handleReconstruction(recon: String) {
|
fun handleReconstruction(recon: String) {
|
||||||
@@ -603,7 +627,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
val store: ManagedStore<*> = when(type) {
|
val store: ManagedStore<*> = when(type) {
|
||||||
"Playlist" -> StatePlaylists.instance.playlistStore
|
"Playlist" -> StatePlaylists.instance.playlistStore
|
||||||
else -> {
|
else -> {
|
||||||
UIDialogs.toast("Unknown reconstruction type ${type}", false);
|
UIDialogs.toast(getString(R.string.unknown_reconstruction_type) + " ${type}", false);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -621,6 +645,20 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun handleUnknownText(text: String): Boolean {
|
||||||
|
try {
|
||||||
|
if(text.startsWith("@/Subscription") || text.startsWith("Subscriptions")) {
|
||||||
|
val lines = text.split("\n").map { it.trim() }.drop(1).filter { it.isNotEmpty() };
|
||||||
|
navigate(_fragImportSubscriptions, lines);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, ex.message, ex);
|
||||||
|
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_parse_text_file), ex);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
fun handleUnknownJson(name: String?, json: String): Boolean {
|
fun handleUnknownJson(name: String?, json: String): Boolean {
|
||||||
|
|
||||||
val context = this;
|
val context = this;
|
||||||
@@ -646,7 +684,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
catch(ex: Exception) {
|
catch(ex: Exception) {
|
||||||
Logger.e(TAG, ex.message, ex);
|
Logger.e(TAG, ex.message, ex);
|
||||||
UIDialogs.showGeneralErrorDialog(context, "Failed to parse NewPipe Subscriptions", ex);
|
UIDialogs.showGeneralErrorDialog(context, getString(R.string.failed_to_parse_newpipe_subscriptions), ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -741,6 +779,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
StateSaved.instance.setVideoToOpenBlocking(null);
|
StateSaved.instance.setVideoToOpenBlocking(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <reified T> isFragmentActive(): Boolean {
|
||||||
|
return fragCurrent is T;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate takes a MainFragment, and makes them the current main visible view
|
* Navigate takes a MainFragment, and makes them the current main visible view
|
||||||
@@ -925,5 +966,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
return sourcesIntent;
|
return sourcesIntent;
|
||||||
}
|
}
|
||||||
|
fun getVideoIntent(context: Context, videoUrl: String) : Intent {
|
||||||
|
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||||
|
sourcesIntent.action = "VIDEO";
|
||||||
|
sourcesIntent.putExtra("VIDEO", videoUrl);
|
||||||
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
return sourcesIntent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension);
|
val qrCodeBitmap = generateQRCode(_exportBundle, dimension, dimension);
|
||||||
_imageQR.setImageBitmap(qrCodeBitmap);
|
_imageQR.setImageBitmap(qrCodeBitmap);
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.e(TAG, "Failed to generate QR code", e);
|
Logger.e(TAG, getString(R.string.failed_to_generate_qr_code), e);
|
||||||
_imageQR.visibility = View.INVISIBLE;
|
_imageQR.visibility = View.INVISIBLE;
|
||||||
_textQR.visibility = View.INVISIBLE;
|
_textQR.visibility = View.INVISIBLE;
|
||||||
}
|
}
|
||||||
@@ -63,12 +63,12 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
|||||||
type = "text/plain";
|
type = "text/plain";
|
||||||
putExtra(Intent.EXTRA_TEXT, _exportBundle);
|
putExtra(Intent.EXTRA_TEXT, _exportBundle);
|
||||||
}
|
}
|
||||||
startActivity(Intent.createChooser(shareIntent, "Share Text"));
|
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_text)));
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonCopy.onClick.subscribe {
|
_buttonCopy.onClick.subscribe {
|
||||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager;
|
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager;
|
||||||
val clip = ClipData.newPlainText("Copied Text", _exportBundle);
|
val clip = ClipData.newPlainText(getString(R.string.copied_text), _exportBundle);
|
||||||
clipboard.setPrimaryClip(clip);
|
clipboard.setPrimaryClip(clip);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -54,7 +54,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
try {
|
try {
|
||||||
val username = _profileName.text.toString();
|
val username = _profileName.text.toString();
|
||||||
if (username.length < 3) {
|
if (username.length < 3) {
|
||||||
UIDialogs.toast(this@PolycentricCreateProfileActivity, "Must be at least 3 characters long.");
|
UIDialogs.toast(this@PolycentricCreateProfileActivity, getString(R.string.must_be_at_least_3_characters_long));
|
||||||
return@setOnClickListener;
|
return@setOnClickListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,16 +68,18 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
processHandle.setUsername(username);
|
processHandle.setUsername(username);
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to create profile .", e);
|
Logger.e(TAG, getString(R.string.failed_to_create_profile), e);
|
||||||
return@launch;
|
return@launch;
|
||||||
} finally {
|
} finally {
|
||||||
_creating = false;
|
_creating = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Logger.i(TAG, "Started backfill");
|
||||||
processHandle.fullyBackfillServers();
|
processHandle.fullyBackfillServers();
|
||||||
|
Logger.i(TAG, "Finished backfill");
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to fully backfill servers.");
|
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class PolycentricHomeActivity : AppCompatActivity() {
|
|||||||
this.setMargins(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, resources.displayMetrics).toInt());
|
this.setMargins(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, resources.displayMetrics).toInt());
|
||||||
};
|
};
|
||||||
profileButton.withPrimaryText(systemState.username);
|
profileButton.withPrimaryText(systemState.username);
|
||||||
profileButton.withSecondaryText("Sign in to this identity");
|
profileButton.withSecondaryText(getString(R.string.sign_in_to_this_identity));
|
||||||
profileButton.onClick.subscribe {
|
profileButton.onClick.subscribe {
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
startActivity(Intent(this@PolycentricHomeActivity, PolycentricProfileActivity::class.java));
|
startActivity(Intent(this@PolycentricHomeActivity, PolycentricProfileActivity::class.java));
|
||||||
|
|||||||
+5
-9
@@ -59,7 +59,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
_buttonScanProfile.setOnClickListener {
|
_buttonScanProfile.setOnClickListener {
|
||||||
val integrator = IntentIntegrator(this)
|
val integrator = IntentIntegrator(this)
|
||||||
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||||
integrator.setPrompt("Scan a QR code")
|
integrator.setPrompt(getString(R.string.scan_a_qr_code))
|
||||||
integrator.setOrientationLocked(true);
|
integrator.setOrientationLocked(true);
|
||||||
integrator.setCameraId(0)
|
integrator.setCameraId(0)
|
||||||
integrator.setBeepEnabled(false)
|
integrator.setBeepEnabled(false)
|
||||||
@@ -70,7 +70,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
_buttonImportProfile.setOnClickListener {
|
_buttonImportProfile.setOnClickListener {
|
||||||
if (_editProfile.text.isEmpty()) {
|
if (_editProfile.text.isEmpty()) {
|
||||||
UIDialogs.toast(this, "Text field does not contain any data");
|
UIDialogs.toast(this, getString(R.string.text_field_does_not_contain_any_data));
|
||||||
return@setOnClickListener;
|
return@setOnClickListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun import(url: String) {
|
private fun import(url: String) {
|
||||||
if (!url.startsWith("polycentric://")) {
|
if (!url.startsWith("polycentric://")) {
|
||||||
UIDialogs.toast(this, "Not a valid URL");
|
UIDialogs.toast(this, getString(R.string.not_a_valid_url));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
|
val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey);
|
||||||
if (existingProcessSecret != null) {
|
if (existingProcessSecret != null) {
|
||||||
UIDialogs.toast(this, "This profile is already imported");
|
UIDialogs.toast(this, getString(R.string.this_profile_is_already_imported));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,15 +124,11 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
finish();
|
finish();
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to import profile", e);
|
Logger.w(TAG, "Failed to import profile", e);
|
||||||
UIDialogs.toast(this, "Failed to import profile: '${e.message}'");
|
UIDialogs.toast(this, getString(R.string.failed_to_import_profile) + " '${e.message}'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PolycentricImportProfileActivity";
|
private const val TAG = "PolycentricImportProfileActivity";
|
||||||
}
|
}
|
||||||
|
|
||||||
class QRCaptureActivity: CaptureActivity() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+14
-10
@@ -16,6 +16,7 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.dialogs.CommentDialog
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -28,6 +29,7 @@ import com.futo.polycentric.core.Store
|
|||||||
import com.futo.polycentric.core.Synchronization
|
import com.futo.polycentric.core.Synchronization
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
import com.futo.polycentric.core.toURLInfoDataLink
|
import com.futo.polycentric.core.toURLInfoDataLink
|
||||||
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.github.dhaval2404.imagepicker.ImagePicker
|
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -72,7 +74,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, "Failed to backfill client");
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,10 +103,10 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buttonDelete.onClick.subscribe {
|
_buttonDelete.onClick.subscribe {
|
||||||
UIDialogs.showConfirmationDialog(this, "Are you sure you want to remove this profile?", {
|
UIDialogs.showConfirmationDialog(this, getString(R.string.are_you_sure_you_want_to_remove_this_profile), {
|
||||||
val processHandle = StatePolycentric.instance.processHandle;
|
val processHandle = StatePolycentric.instance.processHandle;
|
||||||
if (processHandle == null) {
|
if (processHandle == null) {
|
||||||
UIDialogs.toast(this, "No process handle set");
|
UIDialogs.toast(this, getString(R.string.no_process_handle_set));
|
||||||
return@showConfirmationDialog;
|
return@showConfirmationDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,13 +124,13 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
var hasChanges = false;
|
var hasChanges = false;
|
||||||
val username = _editName.text.toString();
|
val username = _editName.text.toString();
|
||||||
if (username.length < 3) {
|
if (username.length < 3) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, "Name must be at least 3 characters long");
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long));
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
|
|
||||||
val processHandle = StatePolycentric.instance.processHandle;
|
val processHandle = StatePolycentric.instance.processHandle;
|
||||||
if (processHandle == null) {
|
if (processHandle == null) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, "Process handle unset");
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset));
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +145,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
val bytes = readBytesFromUri(applicationContext.contentResolver, avatarUri);
|
val bytes = readBytesFromUri(applicationContext.contentResolver, avatarUri);
|
||||||
if (bytes == null) {
|
if (bytes == null) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, "Failed to read image");
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_read_image));
|
||||||
}
|
}
|
||||||
|
|
||||||
return@launch;
|
return@launch;
|
||||||
@@ -186,14 +188,16 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
try {
|
try {
|
||||||
|
Logger.i(TAG, "Started backfill");
|
||||||
processHandle.fullyBackfillServers();
|
processHandle.fullyBackfillServers();
|
||||||
|
Logger.i(TAG, "Finished backfill");
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, "Changes have been saved");
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.changes_have_been_saved));
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to synchronize changes", e);
|
Logger.w(TAG, "Failed to synchronize changes", e);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, "Failed to synchronize changes");
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_synchronize_changes));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,7 +223,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
val avatar = systemState.avatar.selectBestImage(dp_80 * dp_80);
|
val avatar = systemState.avatar.selectBestImage(dp_80 * dp_80);
|
||||||
|
|
||||||
Glide.with(_imagePolycentric)
|
Glide.with(_imagePolycentric)
|
||||||
.load(avatar?.toURLInfoDataLink(processHandle.system.toProto(), processHandle.processSecret.process.toProto(), systemState.servers.toList()))
|
.load(avatar?.toURLInfoSystemLinkUrl(processHandle.system.toProto(), avatar.process, systemState.servers.toList()))
|
||||||
.placeholder(R.drawable.placeholder_profile)
|
.placeholder(R.drawable.placeholder_profile)
|
||||||
.crossfade()
|
.crossfade()
|
||||||
.into(_imagePolycentric)
|
.into(_imagePolycentric)
|
||||||
@@ -235,7 +239,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
} else if (resultCode == ImagePicker.RESULT_ERROR) {
|
} else if (resultCode == ImagePicker.RESULT_ERROR) {
|
||||||
UIDialogs.toast(this, ImagePicker.getError(data));
|
UIDialogs.toast(this, ImagePicker.getError(data));
|
||||||
} else {
|
} else {
|
||||||
UIDialogs.toast(this, "Image picker cancelled");
|
UIDialogs.toast(this, getString(R.string.image_picker_cancelled));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import com.journeyapps.barcodescanner.CaptureActivity
|
||||||
|
|
||||||
|
class QRCaptureActivity : CaptureActivity() {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -13,6 +14,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.Loader
|
import com.futo.platformplayer.views.Loader
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
||||||
@@ -28,6 +30,10 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
|
|
||||||
private var _isFinished = false;
|
private var _isFinished = false;
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
|
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
|
||||||
|
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||||
|
}
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_settings);
|
setContentView(R.layout.activity_settings);
|
||||||
@@ -43,6 +49,11 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
|
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
|
||||||
_form.setObjectValues();
|
_form.setObjectValues();
|
||||||
Settings.instance.save();
|
Settings.instance.save();
|
||||||
|
|
||||||
|
if(field.descriptor?.id == "app_language") {
|
||||||
|
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
|
||||||
|
StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
_buttonBack.setOnClickListener {
|
_buttonBack.setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
@@ -70,7 +81,7 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||||||
SettingsDev.instance.developerMode = true;
|
SettingsDev.instance.developerMode = true;
|
||||||
SettingsDev.instance.save();
|
SettingsDev.instance.save();
|
||||||
updateDevMode();
|
updateDevMode();
|
||||||
UIDialogs.toast(this, "You are now in developer mode");
|
UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media
|
|||||||
import androidx.collection.LruCache
|
import androidx.collection.LruCache
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
@@ -49,6 +50,7 @@ class CachedPlatformClient : IPlatformClient {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getContentChapters(url: String): List<IChapter> = _client.getContentChapters(url);
|
||||||
override fun getPlaybackTracker(url: String): IPlaybackTracker? = _client.getPlaybackTracker(url);
|
override fun getPlaybackTracker(url: String): IPlaybackTracker? = _client.getPlaybackTracker(url);
|
||||||
|
|
||||||
override fun isChannelUrl(url: String): Boolean = _client.isChannelUrl(url);
|
override fun isChannelUrl(url: String): Boolean = _client.isChannelUrl(url);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media
|
|||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
@@ -100,6 +101,8 @@ interface IPlatformClient {
|
|||||||
*/
|
*/
|
||||||
fun getContentDetails(url: String): IPlatformContentDetails;
|
fun getContentDetails(url: String): IPlatformContentDetails;
|
||||||
|
|
||||||
|
fun getContentChapters(url: String): List<IChapter>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the playback tracker for a piece of content
|
* Gets the playback tracker for a piece of content
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ data class PlatformClientCapabilities(
|
|||||||
val hasGetSearchCapabilities: Boolean = false,
|
val hasGetSearchCapabilities: Boolean = false,
|
||||||
val hasGetChannelCapabilities: Boolean = false,
|
val hasGetChannelCapabilities: Boolean = false,
|
||||||
val hasGetLiveEvents: Boolean = false,
|
val hasGetLiveEvents: Boolean = false,
|
||||||
val hasGetLiveChatWindow: Boolean = false
|
val hasGetLiveChatWindow: Boolean = false,
|
||||||
|
val hasGetContentChapters: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ import com.futo.platformplayer.getOrThrow
|
|||||||
* A link to a channel, often with its own name and thumbnail
|
* A link to a channel, often with its own name and thumbnail
|
||||||
*/
|
*/
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class PlatformAuthorLink {
|
open class PlatformAuthorLink {
|
||||||
val id: PlatformID;
|
val id: PlatformID;
|
||||||
val name: String;
|
val name: String;
|
||||||
val url: String;
|
val url: String;
|
||||||
@@ -28,6 +28,9 @@ class PlatformAuthorLink {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
|
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
|
||||||
|
if(value.has("membershipUrl"))
|
||||||
|
return PlatformAuthorMembershipLink.fromV8(config, value);
|
||||||
|
|
||||||
val context = "AuthorLink"
|
val context = "AuthorLink"
|
||||||
return PlatformAuthorLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
|
return PlatformAuthorLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
|
||||||
value.getOrThrow(config ,"name", context),
|
value.getOrThrow(config ,"name", context),
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A link to a channel, often with its own name and thumbnail
|
||||||
|
*/
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
class PlatformAuthorMembershipLink: PlatformAuthorLink {
|
||||||
|
val membershipUrl: String?;
|
||||||
|
|
||||||
|
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null, membershipUrl: String? = null): super(id, name, url, thumbnail, subscribers)
|
||||||
|
{
|
||||||
|
this.membershipUrl = membershipUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorMembershipLink {
|
||||||
|
val context = "AuthorMembershipLink"
|
||||||
|
return PlatformAuthorMembershipLink(PlatformID.fromV8(config, value.getOrThrow(config, "id", context, false)),
|
||||||
|
value.getOrThrow(config ,"name", context),
|
||||||
|
value.getOrThrow(config, "url", context),
|
||||||
|
value.getOrDefault<String>(config, "thumbnail", context, null),
|
||||||
|
if(value.has("subscribers")) value.getOrThrow(config,"subscribers", context) else null,
|
||||||
|
if(value.has("membershipUrl")) value.getOrThrow(config, "membershipUrl", context) else null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,10 @@ class Thumbnails {
|
|||||||
fun getLQThumbnail() : String? {
|
fun getLQThumbnail() : String? {
|
||||||
return sources.firstOrNull()?.url;
|
return sources.firstOrNull()?.url;
|
||||||
}
|
}
|
||||||
|
fun getMinimumThumbnail(quality: Int): String? {
|
||||||
|
return sources.firstOrNull { it.quality >= quality }?.url ?: getHQThumbnail();
|
||||||
|
}
|
||||||
|
|
||||||
fun hasMultiple() = sources.size > 1;
|
fun hasMultiple() = sources.size > 1;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.chapters
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.exceptions.UnknownPlatformException
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
|
|
||||||
|
interface IChapter {
|
||||||
|
val name: String;
|
||||||
|
val type: ChapterType;
|
||||||
|
val timeStart: Int;
|
||||||
|
val timeEnd: Int;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ChapterType(val value: Int) {
|
||||||
|
NORMAL(0),
|
||||||
|
|
||||||
|
SKIPPABLE(5),
|
||||||
|
SKIP(6);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromInt(value: Int): ChapterType
|
||||||
|
{
|
||||||
|
val result = ChapterType.values().firstOrNull { it.value == value };
|
||||||
|
if(result == null)
|
||||||
|
throw UnknownPlatformException(value.toString());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.PlatformClientCapabilities
|
|||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
@@ -91,6 +92,19 @@ open class JSClient : IPlatformClient {
|
|||||||
val enableInSearch get() = descriptor.appSettings.tabEnabled.enableSearch ?: true
|
val enableInSearch get() = descriptor.appSettings.tabEnabled.enableSearch ?: true
|
||||||
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
|
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
|
||||||
|
|
||||||
|
fun getSubscriptionRateLimit(): Int? {
|
||||||
|
val pluginRateLimit = config.subscriptionRateLimit;
|
||||||
|
val settingsRateLimit = descriptor.appSettings.rateLimit.getSubRateLimit();
|
||||||
|
if(settingsRateLimit > 0) {
|
||||||
|
if(pluginRateLimit != null)
|
||||||
|
return settingsRateLimit.coerceAtMost(pluginRateLimit);
|
||||||
|
else
|
||||||
|
return settingsRateLimit;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return pluginRateLimit;
|
||||||
|
}
|
||||||
|
|
||||||
val onDisabled = Event1<JSClient>();
|
val onDisabled = Event1<JSClient>();
|
||||||
val onCaptchaException = Event2<JSClient, ScriptCaptchaRequiredException>();
|
val onCaptchaException = Event2<JSClient, ScriptCaptchaRequiredException>();
|
||||||
|
|
||||||
@@ -181,6 +195,7 @@ open class JSClient : IPlatformClient {
|
|||||||
hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false,
|
hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false,
|
||||||
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
|
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
|
||||||
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
|
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
|
||||||
|
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -414,6 +429,17 @@ open class JSClient : IPlatformClient {
|
|||||||
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
|
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JSOptional //getContentChapters = function(url, initialData)
|
||||||
|
@JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details")
|
||||||
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
|
override fun getContentChapters(url: String): List<IChapter> = isBusyWith {
|
||||||
|
if(!capabilities.hasGetContentChapters)
|
||||||
|
return@isBusyWith listOf();
|
||||||
|
ensureEnabled();
|
||||||
|
return@isBusyWith JSChapter.fromV8(config,
|
||||||
|
plugin.executeTyped("source.getContentChapters(${Json.encodeToString(url)})"));
|
||||||
|
}
|
||||||
|
|
||||||
@JSOptional
|
@JSOptional
|
||||||
@JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url")
|
@JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url")
|
||||||
@JSDocsParameter("url", "A content url (this platform)")
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
@@ -558,7 +584,7 @@ open class JSClient : IPlatformClient {
|
|||||||
if(it.containsKey(claimType)) {
|
if(it.containsKey(claimType)) {
|
||||||
val templates = it[claimType];
|
val templates = it[claimType];
|
||||||
if(templates != null)
|
if(templates != null)
|
||||||
for(value in values.keys.sortedBy { it }) {
|
for(value in values.keys.sortedBy { if(it == config.primaryClaimFieldType) Int.MIN_VALUE else it }) {
|
||||||
if(templates.containsKey(value)) {
|
if(templates.containsKey(value)) {
|
||||||
return templates[value]!!.replace("{{CLAIMVALUE}}", values[value]!!);
|
return templates[value]!!.replace("{{CLAIMVALUE}}", values[value]!!);
|
||||||
}
|
}
|
||||||
@@ -568,6 +594,23 @@ open class JSClient : IPlatformClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resolveChannelUrlsByClaimTemplates(claimType: Int, values: Map<Int, String>): List<String> {
|
||||||
|
val urls = arrayListOf<String>();
|
||||||
|
channelClaimTemplates?.let {
|
||||||
|
if(it.containsKey(claimType)) {
|
||||||
|
val templates = it[claimType];
|
||||||
|
if(templates != null)
|
||||||
|
for(value in values.keys.sortedBy { it }) {
|
||||||
|
if(templates.containsKey(value)) {
|
||||||
|
urls.add(templates[value]!!.replace("{{CLAIMVALUE}}", values[value]!!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun <T> isBusyWith(handle: ()->T): T {
|
private fun <T> isBusyWith(handle: ()->T): T {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
import com.futo.platformplayer.encryption.EncryptionProvider
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
|
||||||
data class SourceAuth(val cookieMap: HashMap<String, HashMap<String, String>>? = null, val headers: Map<String, Map<String, String>> = mapOf()) {
|
data class SourceAuth(val cookieMap: HashMap<String, HashMap<String, String>>? = null, val headers: Map<String, Map<String, String>> = mapOf()) {
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "(headers: '$headers', cookieString: '$cookieMap')";
|
return "(headers: '$headers', cookieString: '$cookieMap')";
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toEncrypted(): String{
|
fun toEncrypted(): String{
|
||||||
return EncryptionProvider.instance.encrypt(serialize());
|
return SourceEncrypted.fromDecrypted { serialize() }.toJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun serialize(): String {
|
private fun serialize(): String {
|
||||||
@@ -25,20 +22,10 @@ data class SourceAuth(val cookieMap: HashMap<String, HashMap<String, String>>? =
|
|||||||
val TAG = "SourceAuth";
|
val TAG = "SourceAuth";
|
||||||
|
|
||||||
fun fromEncrypted(encrypted: String?): SourceAuth? {
|
fun fromEncrypted(encrypted: String?): SourceAuth? {
|
||||||
if(encrypted == null)
|
return SourceEncrypted.decryptEncrypted(encrypted) { deserialize(it) };
|
||||||
return null;
|
|
||||||
|
|
||||||
val decrypted = EncryptionProvider.instance.decrypt(encrypted);
|
|
||||||
try {
|
|
||||||
return deserialize(decrypted);
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to deserialize authentication", ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deserialize(str: String): SourceAuth {
|
private fun deserialize(str: String): SourceAuth {
|
||||||
val data = Json.decodeFromString<SerializedAuth>(str);
|
val data = Json.decodeFromString<SerializedAuth>(str);
|
||||||
return SourceAuth(data.cookieMap, data.headers);
|
return SourceAuth(data.cookieMap, data.headers);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-15
@@ -1,7 +1,5 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
import com.futo.platformplayer.encryption.EncryptionProvider
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
@@ -13,7 +11,7 @@ data class SourceCaptchaData(val cookieMap: HashMap<String, HashMap<String, Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun toEncrypted(): String{
|
fun toEncrypted(): String{
|
||||||
return EncryptionProvider.instance.encrypt(serialize());
|
return SourceEncrypted.fromDecrypted { serialize() }.toJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun serialize(): String {
|
private fun serialize(): String {
|
||||||
@@ -21,20 +19,10 @@ data class SourceCaptchaData(val cookieMap: HashMap<String, HashMap<String, Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "SourceAuth";
|
val TAG = "SourceCaptchaData";
|
||||||
|
|
||||||
fun fromEncrypted(encrypted: String?): SourceCaptchaData? {
|
fun fromEncrypted(encrypted: String?): SourceCaptchaData? {
|
||||||
if(encrypted == null)
|
return SourceEncrypted.decryptEncrypted(encrypted) { deserialize(it) };
|
||||||
return null;
|
|
||||||
|
|
||||||
val decrypted = EncryptionProvider.instance.decrypt(encrypted);
|
|
||||||
try {
|
|
||||||
return deserialize(decrypted);
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to deserialize authentication", ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deserialize(str: String): SourceCaptchaData {
|
fun deserialize(str: String): SourceCaptchaData {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
|
import com.futo.platformplayer.encryption.GEncryptionProvider
|
||||||
|
import com.futo.platformplayer.encryption.GEncryptionProviderV0
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SourceEncrypted(
|
||||||
|
val encrypted: String,
|
||||||
|
val version: Int = GEncryptionProvider.version
|
||||||
|
) {
|
||||||
|
fun toJson(): String {
|
||||||
|
return Json.encodeToString(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromDecrypted(serializer: () -> String): SourceEncrypted {
|
||||||
|
return SourceEncrypted(GEncryptionProvider.instance.encrypt(serializer()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> decryptEncrypted(encrypted: String?, deserializer: (decrypted: String) -> T): T? {
|
||||||
|
if(encrypted == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
val encryptedSourceAuth = Json.decodeFromString<SourceEncrypted>(encrypted)
|
||||||
|
if (encryptedSourceAuth.version != GEncryptionProvider.version) {
|
||||||
|
throw Exception("Invalid encryption version.");
|
||||||
|
}
|
||||||
|
|
||||||
|
val decrypted = GEncryptionProvider.instance.decrypt(encryptedSourceAuth.encrypted);
|
||||||
|
try {
|
||||||
|
return deserializer(decrypted);
|
||||||
|
} catch(ex: Throwable) {
|
||||||
|
Logger.e(SourceAuth.TAG, "Failed to deserialize SourceEncrypted<T>", ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
//Try to fall back to old mechanism, remove this eventually
|
||||||
|
if (!encrypted.contains("version")) {
|
||||||
|
val decrypted = GEncryptionProviderV0.instance.decrypt(encrypted);
|
||||||
|
try {
|
||||||
|
return deserializer(decrypted);
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
Logger.e(SourceAuth.TAG, "Failed to deserialize SourceEncrypted<T>", ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-2
@@ -41,10 +41,12 @@ class SourcePluginConfig(
|
|||||||
val constants: HashMap<String, String> = hashMapOf(),
|
val constants: HashMap<String, String> = hashMapOf(),
|
||||||
|
|
||||||
//TODO: These should be vals...but prob for serialization reasons cannot be changed.
|
//TODO: These should be vals...but prob for serialization reasons cannot be changed.
|
||||||
|
var platformUrl: String? = null,
|
||||||
var subscriptionRateLimit: Int? = null,
|
var subscriptionRateLimit: Int? = null,
|
||||||
var enableInSearch: Boolean = true,
|
var enableInSearch: Boolean = true,
|
||||||
var enableInHome: Boolean = true,
|
var enableInHome: Boolean = true,
|
||||||
var supportedClaimTypes: List<Int> = listOf()
|
var supportedClaimTypes: List<Int> = listOf(),
|
||||||
|
var primaryClaimFieldType: Int? = null
|
||||||
) : IV8PluginConfig {
|
) : IV8PluginConfig {
|
||||||
|
|
||||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||||
@@ -142,7 +144,10 @@ class SourcePluginConfig(
|
|||||||
val description: String,
|
val description: String,
|
||||||
val type: String,
|
val type: String,
|
||||||
val default: String? = null,
|
val default: String? = null,
|
||||||
val variable: String? = null
|
val variable: String? = null,
|
||||||
|
val dependency: String? = null,
|
||||||
|
val warningDialog: String? = null,
|
||||||
|
val options: List<String>? = null
|
||||||
) {
|
) {
|
||||||
@kotlinx.serialization.Transient
|
@kotlinx.serialization.Transient
|
||||||
val variableOrName: String get() = variable ?: name;
|
val variableOrName: String get() = variable ?: name;
|
||||||
|
|||||||
+28
-3
@@ -1,7 +1,9 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
|
||||||
|
import com.futo.platformplayer.views.fields.DropdownFieldOptions
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.FormField
|
import com.futo.platformplayer.views.fields.FormField
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -66,18 +68,41 @@ class SourcePluginDescriptor {
|
|||||||
@Serializable
|
@Serializable
|
||||||
class AppPluginSettings {
|
class AppPluginSettings {
|
||||||
|
|
||||||
@FormField("Visibility", "group", "Enable where this plugin's content are visible.", 2)
|
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
|
||||||
var tabEnabled = TabEnabled();
|
var tabEnabled = TabEnabled();
|
||||||
@Serializable
|
@Serializable
|
||||||
class TabEnabled {
|
class TabEnabled {
|
||||||
@FormField("Home", FieldForm.TOGGLE, "Show content in home tab", 1)
|
@FormField(R.string.home, FieldForm.TOGGLE, R.string.show_content_in_home_tab, 1)
|
||||||
var enableHome: Boolean? = null;
|
var enableHome: Boolean? = null;
|
||||||
|
|
||||||
|
|
||||||
@FormField("Search", FieldForm.TOGGLE, "Show content in search results", 2)
|
@FormField(R.string.search, FieldForm.TOGGLE, R.string.show_content_in_search_results, 2)
|
||||||
var enableSearch: Boolean? = null;
|
var enableSearch: Boolean? = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 3)
|
||||||
|
var rateLimit = RateLimit();
|
||||||
|
@Serializable
|
||||||
|
class RateLimit {
|
||||||
|
@FormField(R.string.subscriptions, FieldForm.DROPDOWN, R.string.ratelimit_sub_setting_description, 1)
|
||||||
|
@DropdownFieldOptions("Plugin defined", "25", "50", "75", "100", "125", "150", "200")
|
||||||
|
var rateLimitSubs: Int = 0;
|
||||||
|
|
||||||
|
fun getSubRateLimit(): Int {
|
||||||
|
return when(rateLimitSubs) {
|
||||||
|
0 -> -1
|
||||||
|
1 -> 25
|
||||||
|
2 -> 50
|
||||||
|
3 -> 75
|
||||||
|
4 -> 100
|
||||||
|
5 -> 125
|
||||||
|
6 -> 150
|
||||||
|
7 -> 200
|
||||||
|
else -> -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun loadDefaults(config: SourcePluginConfig) {
|
fun loadDefaults(config: SourcePluginConfig) {
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueArray
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.ChapterType
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
class JSChapter : IChapter {
|
||||||
|
override val name: String;
|
||||||
|
override val type: ChapterType;
|
||||||
|
override val timeStart: Int;
|
||||||
|
override val timeEnd: Int;
|
||||||
|
|
||||||
|
constructor(name: String, timeStart: Int, timeEnd: Int, type: ChapterType = ChapterType.NORMAL) {
|
||||||
|
this.name = name;
|
||||||
|
this.timeStart = timeStart;
|
||||||
|
this.timeEnd = timeEnd;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject): IChapter {
|
||||||
|
val context = "Chapter";
|
||||||
|
|
||||||
|
val name = obj.getOrThrow<String>(config,"name", context);
|
||||||
|
val type = ChapterType.fromInt(obj.getOrDefault<Int>(config, "type", context, ChapterType.NORMAL.value) ?: ChapterType.NORMAL.value);
|
||||||
|
val timeStart = obj.getOrThrow<Int>(config, "timeStart", context);
|
||||||
|
val timeEnd = obj.getOrThrow<Int>(config, "timeEnd", context);
|
||||||
|
|
||||||
|
return JSChapter(name, timeStart, timeEnd, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromV8(config: IV8PluginConfig, arr: V8ValueArray): List<IChapter> {
|
||||||
|
return arr.keys.mapNotNull {
|
||||||
|
val obj = arr.get<V8ValueObject>(it);
|
||||||
|
return@mapNotNull fromV8(config, obj);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,7 +75,12 @@ class DedupContentPager : IPager<IPlatformContent>, IAsyncPager<IPlatformContent
|
|||||||
return toReturn;
|
return toReturn;
|
||||||
}
|
}
|
||||||
private fun isSameItem(item: IPlatformContent, item2: IPlatformContent): Boolean {
|
private fun isSameItem(item: IPlatformContent, item2: IPlatformContent): Boolean {
|
||||||
return item.name == item2.name && (item.datetime == null || item2.datetime == null || abs(item.datetime!!.getDiffDays(item2.datetime!!)) < 2);
|
//return item == item2;
|
||||||
|
val daysAgo = Math.abs(item.datetime?.getNowDiffDays() ?: return false);
|
||||||
|
val maxDelta = Math.max(2, (daysAgo / 1.5).toInt()); //TODO: Better scaling delta
|
||||||
|
val isSame = item.name.equals(item2.name, true) && (item.datetime == null || item2.datetime == null || abs(item.datetime!!.getDiffDays(item2.datetime!!)) < maxDelta);
|
||||||
|
|
||||||
|
return isSame;
|
||||||
}
|
}
|
||||||
private fun calculateHash(item: IPlatformContent): Int {
|
private fun calculateHash(item: IPlatformContent): Int {
|
||||||
return combineHashCodes(listOf(item.name.hashCode(), item.datetime?.hashCode()));
|
return combineHashCodes(listOf(item.name.hashCode(), item.datetime?.hashCode()));
|
||||||
|
|||||||
+1
@@ -8,6 +8,7 @@ import java.util.stream.IntStream
|
|||||||
*/
|
*/
|
||||||
class MultiChronoContentPager : MultiPager<IPlatformContent> {
|
class MultiChronoContentPager : MultiPager<IPlatformContent> {
|
||||||
constructor(pagers : Array<IPager<IPlatformContent>>, allowFailure: Boolean = false, pageSize: Int = 9) : super(pagers.map { it }.toList(), allowFailure, pageSize) {}
|
constructor(pagers : Array<IPager<IPlatformContent>>, allowFailure: Boolean = false, pageSize: Int = 9) : super(pagers.map { it }.toList(), allowFailure, pageSize) {}
|
||||||
|
constructor(pagers : List<IPager<IPlatformContent>>, allowFailure: Boolean = false, pageSize: Int = 9) : super(pagers, allowFailure, pageSize) {}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun selectItemIndex(options: Array<SelectionOption<IPlatformContent>>): Int {
|
override fun selectItemIndex(options: Array<SelectionOption<IPlatformContent>>): Int {
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package com.futo.platformplayer.api.media.structures
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.util.stream.IntStream
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Content AsyncMultiPager that returns results based on a specified distribution
|
||||||
|
* Unlike its non-async counterpart, this one uses parallel nextPage requests
|
||||||
|
*/
|
||||||
|
class MultiChronoContentParallelPager : MultiParallelPager<IPlatformContent> {
|
||||||
|
|
||||||
|
constructor(pagers: List<IPager<IPlatformContent>>) : super(pagers)
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun selectItemIndex(options: Array<SelectionOption<IPlatformContent>>): Int {
|
||||||
|
if(options.size == 0)
|
||||||
|
return -1;
|
||||||
|
var bestIndex = 0;
|
||||||
|
|
||||||
|
val allResults = runBlocking { options.map { Pair(it, it.item?.await()) } };
|
||||||
|
for(i in IntStream.range(1, options.size)) {
|
||||||
|
val best = allResults[bestIndex].second;
|
||||||
|
val cur = allResults[i].second ?: continue;
|
||||||
|
if(best?.datetime == null || (cur.datetime != null && cur.datetime!! > best.datetime!!))
|
||||||
|
bestIndex = i;
|
||||||
|
}
|
||||||
|
return bestIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
+18
-18
@@ -66,25 +66,25 @@ abstract class MultiRefreshPager<T>: IRefreshPager<T>, IPager<T> {
|
|||||||
override fun getResults(): List<T> = synchronized(_pagersReusable){ _currentPager.getResults() };
|
override fun getResults(): List<T> = synchronized(_pagersReusable){ _currentPager.getResults() };
|
||||||
|
|
||||||
private fun updatePager(pagerToAdd: IPager<T>?, toReplacePager: IPager<T>? = null, error: Throwable? = null) {
|
private fun updatePager(pagerToAdd: IPager<T>?, toReplacePager: IPager<T>? = null, error: Throwable? = null) {
|
||||||
if(pagerToAdd == null) {
|
|
||||||
if(toReplacePager != null && toReplacePager is PlaceholderPager && error != null) {
|
|
||||||
val pluginId = toReplacePager.placeholderFactory.invoke().id?.pluginId ?: "";
|
|
||||||
|
|
||||||
_pagersReusable.add((PlaceholderPager(5) {
|
|
||||||
return@PlaceholderPager PlatformContentPlaceholder(pluginId, error)
|
|
||||||
} as IPager<T>).asReusable());
|
|
||||||
_currentPager = recreatePager(getCurrentSubPagers());
|
|
||||||
|
|
||||||
if(_currentPager is MultiParallelPager<*>)
|
|
||||||
runBlocking { (_currentPager as MultiParallelPager).initialize(); };
|
|
||||||
else if(_currentPager is MultiPager<*>)
|
|
||||||
(_currentPager as MultiPager).initialize()
|
|
||||||
|
|
||||||
onPagerChanged.emit(_currentPager);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
synchronized(_pagersReusable) {
|
synchronized(_pagersReusable) {
|
||||||
|
if(pagerToAdd == null) {
|
||||||
|
if(toReplacePager != null && toReplacePager is PlaceholderPager && error != null) {
|
||||||
|
val pluginId = toReplacePager.placeholderFactory.invoke().id?.pluginId ?: "";
|
||||||
|
|
||||||
|
_pagersReusable.add((PlaceholderPager(5) {
|
||||||
|
return@PlaceholderPager PlatformContentPlaceholder(pluginId, error)
|
||||||
|
} as IPager<T>).asReusable());
|
||||||
|
_currentPager = recreatePager(getCurrentSubPagers());
|
||||||
|
|
||||||
|
if(_currentPager is MultiParallelPager<*>)
|
||||||
|
runBlocking { (_currentPager as MultiParallelPager).initialize(); };
|
||||||
|
else if(_currentPager is MultiPager<*>)
|
||||||
|
(_currentPager as MultiPager).initialize()
|
||||||
|
|
||||||
|
onPagerChanged.emit(_currentPager);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
Logger.i("RefreshMultiDistributionContentPager", "Received new pager for RefreshPager")
|
Logger.i("RefreshMultiDistributionContentPager", "Received new pager for RefreshPager")
|
||||||
_pagersReusable.add(pagerToAdd.asReusable());
|
_pagersReusable.add(pagerToAdd.asReusable());
|
||||||
|
|
||||||
|
|||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
package com.futo.platformplayer.api.media.structures
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A RefreshMultiPager that simply returns all respective pagers in equal distribution, optionally inserting PlaceholderPager results as provided for their respective promised pagers
|
||||||
|
* (Eg. Pager A is completed, Pager [B,C,D] are promised/deferred. placeholderPagers [1,2,3] will map B=>1, C=>2, D=>3 until promised pagers are completed)
|
||||||
|
* Uses wrapped MultiDistributionContentAsyncPager for inidivual pagers.
|
||||||
|
*/
|
||||||
|
class RefreshChronoContentPager(pagers: List<IPager<IPlatformContent>>, pendingPagers: List<Deferred<IPager<IPlatformContent>?>>, placeholderPagers: List<IPager<IPlatformContent>>? = null)
|
||||||
|
: MultiRefreshPager<IPlatformContent>(pagers, pendingPagers, placeholderPagers) {
|
||||||
|
|
||||||
|
override fun recreatePager(pagers: List<IPager<IPlatformContent>>): IPager<IPlatformContent> {
|
||||||
|
return MultiChronoContentPager(pagers);
|
||||||
|
//return MultiChronoContentParallelPager(pagers);
|
||||||
|
//return MultiDistributionContentPager(pagers.associateWith { 1f });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ class SingleAsyncItemPager<T> {
|
|||||||
if (_currentResultPos >= _requestedPageItems.size) {
|
if (_currentResultPos >= _requestedPageItems.size) {
|
||||||
val startPos = fillDeferredUntil(_currentResultPos);
|
val startPos = fillDeferredUntil(_currentResultPos);
|
||||||
if(!_pager.hasMorePages()) {
|
if(!_pager.hasMorePages()) {
|
||||||
|
Logger.i("SingleAsyncItemPager", "end of async page reached");
|
||||||
completeRemainder { it?.complete(null) };
|
completeRemainder { it?.complete(null) };
|
||||||
}
|
}
|
||||||
if(_isRequesting)
|
if(_isRequesting)
|
||||||
|
|||||||
@@ -4,13 +4,21 @@ import android.app.NotificationChannel
|
|||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.media.MediaSession2Service.MediaNotification
|
||||||
import androidx.concurrent.futures.CallbackToFutureAdapter
|
import androidx.concurrent.futures.CallbackToFutureAdapter
|
||||||
import androidx.concurrent.futures.ResolvableFuture
|
import androidx.concurrent.futures.ResolvableFuture
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.ListenableWorker
|
import androidx.work.ListenableWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.request.target.CustomTarget
|
||||||
|
import com.bumptech.glide.request.transition.Transition
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.getNowDiffSeconds
|
import com.futo.platformplayer.getNowDiffSeconds
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
@@ -27,10 +35,10 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
class BackgroundWorker(private val appContext: Context, workerParams: WorkerParameters) :
|
class BackgroundWorker(private val appContext: Context, private val workerParams: WorkerParameters) :
|
||||||
CoroutineWorker(appContext, workerParams) {
|
CoroutineWorker(appContext, workerParams) {
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
if(StateApp.instance.isMainActive) {
|
if(StateApp.instance.isMainActive && !inputData.getBoolean("bypassMainCheck", false)) {
|
||||||
Logger.i("BackgroundWorker", "CANCELLED");
|
Logger.i("BackgroundWorker", "CANCELLED");
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
@@ -83,8 +91,11 @@ class BackgroundWorker(private val appContext: Context, workerParams: WorkerPara
|
|||||||
|
|
||||||
val newSubChanges = hashSetOf<Subscription>();
|
val newSubChanges = hashSetOf<Subscription>();
|
||||||
val newItems = mutableListOf<IPlatformContent>();
|
val newItems = mutableListOf<IPlatformContent>();
|
||||||
|
|
||||||
|
val now = OffsetDateTime.now();
|
||||||
|
val contentNotifs = mutableListOf<Pair<Subscription, IPlatformContent>>();
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(true, false,this, { progress, total ->
|
val results = StateSubscriptions.instance.getSubscriptionsFeedWithExceptions(true, false,this, { progress, total ->
|
||||||
Logger.i("BackgroundWorker", "SUBSCRIPTION PROGRESS: ${progress}/${total}");
|
Logger.i("BackgroundWorker", "SUBSCRIPTION PROGRESS: ${progress}/${total}");
|
||||||
|
|
||||||
synchronized(manager) {
|
synchronized(manager) {
|
||||||
@@ -97,21 +108,76 @@ class BackgroundWorker(private val appContext: Context, workerParams: WorkerPara
|
|||||||
}
|
}
|
||||||
}, { sub, content ->
|
}, { sub, content ->
|
||||||
synchronized(newSubChanges) {
|
synchronized(newSubChanges) {
|
||||||
if(!newSubChanges.contains(sub))
|
if(!newSubChanges.contains(sub)) {
|
||||||
newSubChanges.add(sub);
|
newSubChanges.add(sub);
|
||||||
|
if(sub.doNotifications && content.datetime?.let { it < now } == true)
|
||||||
|
contentNotifs.add(Pair(sub, content));
|
||||||
|
}
|
||||||
newItems.add(content);
|
newItems.add(content);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Only for testing notifications
|
||||||
|
val testNotifs = 0;
|
||||||
|
if(contentNotifs.size == 0 && testNotifs > 0) {
|
||||||
|
results.first.getResults().filter { it is IPlatformVideo && it.datetime?.let { it < now } == true }
|
||||||
|
.take(testNotifs).forEach {
|
||||||
|
contentNotifs.add(Pair(StateSubscriptions.instance.getSubscriptions().first(), it));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.cancel(12);
|
manager.cancel(12);
|
||||||
|
|
||||||
if(newItems.size > 0)
|
if(contentNotifs.size > 0) {
|
||||||
|
try {
|
||||||
|
val items = contentNotifs.take(5).toList()
|
||||||
|
for(i in items.indices) {
|
||||||
|
val contentNotif = items.get(i);
|
||||||
|
val thumbnail = if(contentNotif.second is IPlatformVideo) (contentNotif.second as IPlatformVideo).thumbnails.getHQThumbnail()
|
||||||
|
else null;
|
||||||
|
if(thumbnail != null)
|
||||||
|
Glide.with(appContext).asBitmap()
|
||||||
|
.load(thumbnail)
|
||||||
|
.into(object: CustomTarget<Bitmap>() {
|
||||||
|
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||||
|
notifyNewContent(manager, notificationChannel, 13 + i, contentNotif.first, contentNotif.second, resource);
|
||||||
|
}
|
||||||
|
override fun onLoadCleared(placeholder: Drawable?) {}
|
||||||
|
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||||
|
notifyNewContent(manager, notificationChannel, 13 + i, contentNotif.first, contentNotif.second, null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
else
|
||||||
|
notifyNewContent(manager, notificationChannel, 13 + i, contentNotif.first, contentNotif.second, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("BackgroundWorker", "Failed to create notif", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
manager.notify(13, NotificationCompat.Builder(appContext, notificationChannel.id)
|
manager.notify(13, NotificationCompat.Builder(appContext, notificationChannel.id)
|
||||||
.setSmallIcon(com.futo.platformplayer.R.drawable.foreground)
|
.setSmallIcon(com.futo.platformplayer.R.drawable.foreground)
|
||||||
.setContentTitle("Grayjay")
|
.setContentTitle("Grayjay")
|
||||||
.setContentText("${newItems.size} new content from ${newSubChanges.size} creators")
|
.setContentText("${newItems.size} new content from ${newSubChanges.size} creators")
|
||||||
.setSilent(true)
|
.setSilent(true)
|
||||||
.setChannelId(notificationChannel.id).build());
|
.setChannelId(notificationChannel.id).build());*/
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyNewContent(manager: NotificationManager, notificationChannel: NotificationChannel, id: Int, sub: Subscription, content: IPlatformContent, thumbnail: Bitmap? = null) {
|
||||||
|
val notifBuilder = NotificationCompat.Builder(appContext, notificationChannel.id)
|
||||||
|
.setSmallIcon(com.futo.platformplayer.R.drawable.foreground)
|
||||||
|
.setContentTitle("New by [${sub.channel.name}]")
|
||||||
|
.setContentText("${content.name}")
|
||||||
|
.setSilent(true)
|
||||||
|
.setContentIntent(PendingIntent.getActivity(this.appContext, 0, MainActivity.getVideoIntent(this.appContext, content.url),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
|
||||||
|
.setChannelId(notificationChannel.id);
|
||||||
|
if(thumbnail != null) {
|
||||||
|
//notifBuilder.setLargeIcon(thumbnail);
|
||||||
|
notifBuilder.setStyle(NotificationCompat.BigPictureStyle().bigPicture(thumbnail).bigLargeIcon(null as Bitmap?));
|
||||||
|
}
|
||||||
|
manager.notify(id, notifBuilder.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,19 +12,44 @@ import com.futo.platformplayer.serializers.PlatformContentSerializer
|
|||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import com.futo.platformplayer.toSafeFileName
|
import com.futo.platformplayer.toSafeFileName
|
||||||
import com.futo.polycentric.core.toUrl
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import kotlin.streams.toList
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
class ChannelContentCache {
|
class ChannelContentCache {
|
||||||
|
private val _targetCacheSize = 3000;
|
||||||
val _channelCacheDir = FragmentedStorage.getOrCreateDirectory("channelCache");
|
val _channelCacheDir = FragmentedStorage.getOrCreateDirectory("channelCache");
|
||||||
val _channelContents = HashMap(_channelCacheDir.listFiles()
|
val _channelContents: HashMap<String, ManagedStore<SerializedPlatformContent>>;
|
||||||
.filter { it.isDirectory }
|
init {
|
||||||
.associate { Pair(it.name, FragmentedStorage.storeJson<SerializedPlatformContent>(_channelCacheDir, it.name, PlatformContentSerializer())
|
val allFiles = _channelCacheDir.listFiles() ?: arrayOf();
|
||||||
.withoutBackup()
|
val initializeTime = measureTimeMillis {
|
||||||
.load()) });
|
_channelContents = HashMap(allFiles
|
||||||
|
.filter { it.isDirectory }
|
||||||
|
.parallelStream().map {
|
||||||
|
Pair(it.name, FragmentedStorage.storeJson(_channelCacheDir, it.name, PlatformContentSerializer())
|
||||||
|
.withoutBackup()
|
||||||
|
.load())
|
||||||
|
}.toList().associate { it })
|
||||||
|
}
|
||||||
|
val minDays = OffsetDateTime.now().minusDays(10);
|
||||||
|
val totalItems = _channelContents.map { it.value.count() }.sum();
|
||||||
|
val toTrim = totalItems - _targetCacheSize;
|
||||||
|
val trimmed: Int;
|
||||||
|
if(toTrim > 0) {
|
||||||
|
val redundantContent = _channelContents.flatMap { it.value.getItems().filter { it.datetime != null && it.datetime!!.isBefore(minDays) }.drop(9) }
|
||||||
|
.sortedBy { it.datetime!! }.take(toTrim);
|
||||||
|
for(content in redundantContent)
|
||||||
|
uncacheContent(content);
|
||||||
|
trimmed = redundantContent.size;
|
||||||
|
}
|
||||||
|
else trimmed = 0;
|
||||||
|
Logger.i(TAG, "ChannelContentCache time: ${initializeTime}ms channels: ${allFiles.size}, videos: ${totalItems}, trimmed: ${trimmed}, total: ${totalItems - trimmed}");
|
||||||
|
}
|
||||||
|
|
||||||
fun getChannelCachePager(channelUrl: String): PlatformContentPager {
|
fun getChannelCachePager(channelUrl: String): PlatformContentPager {
|
||||||
val validID = channelUrl.toSafeFileName();
|
val validID = channelUrl.toSafeFileName();
|
||||||
@@ -38,7 +63,9 @@ class ChannelContentCache {
|
|||||||
return PlatformContentPager(items, Math.min(150, items.size));
|
return PlatformContentPager(items, Math.min(150, items.size));
|
||||||
}
|
}
|
||||||
fun getSubscriptionCachePager(): DedupContentPager {
|
fun getSubscriptionCachePager(): DedupContentPager {
|
||||||
|
Logger.i(TAG, "Subscriptions CachePager get subscriptions");
|
||||||
val subs = StateSubscriptions.instance.getSubscriptions();
|
val subs = StateSubscriptions.instance.getSubscriptions();
|
||||||
|
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
|
||||||
val allUrls = subs.map {
|
val allUrls = subs.map {
|
||||||
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
|
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
|
||||||
if(!otherUrls.contains(it.channel.url))
|
if(!otherUrls.contains(it.channel.url))
|
||||||
@@ -46,6 +73,7 @@ class ChannelContentCache {
|
|||||||
else
|
else
|
||||||
return@map otherUrls;
|
return@map otherUrls;
|
||||||
}.flatten().distinct();
|
}.flatten().distinct();
|
||||||
|
Logger.i(TAG, "Subscriptions CachePager compiling");
|
||||||
val validSubIds = allUrls.map { it.toSafeFileName() }.toHashSet();
|
val validSubIds = allUrls.map { it.toSafeFileName() }.toHashSet();
|
||||||
|
|
||||||
val validStores = _channelContents
|
val validStores = _channelContents
|
||||||
@@ -55,10 +83,14 @@ class ChannelContentCache {
|
|||||||
val items = validStores.flatMap { it.getItems() }
|
val items = validStores.flatMap { it.getItems() }
|
||||||
.sortedByDescending { it.datetime };
|
.sortedByDescending { it.datetime };
|
||||||
|
|
||||||
return DedupContentPager(PlatformContentPager(items, Math.min(150, items.size)), StatePlatform.instance.getEnabledClients().map { it.id });
|
return DedupContentPager(PlatformContentPager(items, Math.min(30, items.size)), StatePlatform.instance.getEnabledClients().map { it.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cacheVideos(contents: List<IPlatformContent>): List<IPlatformContent> {
|
fun uncacheContent(content: SerializedPlatformContent) {
|
||||||
|
val store = getContentStore(content);
|
||||||
|
store?.delete(content);
|
||||||
|
}
|
||||||
|
fun cacheContents(contents: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
return contents.filter { cacheContent(it) };
|
return contents.filter { cacheContent(it) };
|
||||||
}
|
}
|
||||||
fun cacheContent(content: IPlatformContent, doUpdate: Boolean = false): Boolean {
|
fun cacheContent(content: IPlatformContent, doUpdate: Boolean = false): Boolean {
|
||||||
@@ -66,14 +98,14 @@ class ChannelContentCache {
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
val channelId = content.author.url.toSafeFileName();
|
val channelId = content.author.url.toSafeFileName();
|
||||||
val store = synchronized(_channelContents) {
|
val store = getContentStore(channelId).let {
|
||||||
var channelStore = _channelContents.get(channelId);
|
if(it == null) {
|
||||||
if(channelStore == null) {
|
Logger.i(TAG, "New Channel Cache for channel ${content.author.name}");
|
||||||
Logger.i(TAG, "New Subscription Cache for channel ${content.author.name}");
|
val store = FragmentedStorage.storeJson<SerializedPlatformContent>(_channelCacheDir, channelId, PlatformContentSerializer()).load();
|
||||||
channelStore = FragmentedStorage.storeJson<SerializedPlatformContent>(_channelCacheDir, channelId, PlatformContentSerializer()).load();
|
_channelContents.put(channelId, store);
|
||||||
_channelContents.put(channelId, channelStore);
|
return@let store;
|
||||||
}
|
}
|
||||||
return@synchronized channelStore;
|
else return@let it;
|
||||||
}
|
}
|
||||||
val serialized = SerializedPlatformContent.fromContent(content);
|
val serialized = SerializedPlatformContent.fromContent(content);
|
||||||
val existing = store.findItems { it.url == content.url };
|
val existing = store.findItems { it.url == content.url };
|
||||||
@@ -88,6 +120,17 @@ class ChannelContentCache {
|
|||||||
return existing.isEmpty();
|
return existing.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getContentStore(content: IPlatformContent): ManagedStore<SerializedPlatformContent>? {
|
||||||
|
val channelId = content.author.url.toSafeFileName();
|
||||||
|
return getContentStore(channelId);
|
||||||
|
}
|
||||||
|
private fun getContentStore(channelId: String): ManagedStore<SerializedPlatformContent>? {
|
||||||
|
return synchronized(_channelContents) {
|
||||||
|
var channelStore = _channelContents.get(channelId);
|
||||||
|
return@synchronized channelStore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "ChannelCache";
|
private val TAG = "ChannelCache";
|
||||||
|
|
||||||
@@ -95,10 +138,11 @@ class ChannelContentCache {
|
|||||||
private var _instance: ChannelContentCache? = null;
|
private var _instance: ChannelContentCache? = null;
|
||||||
val instance: ChannelContentCache get() {
|
val instance: ChannelContentCache get() {
|
||||||
synchronized(_lock) {
|
synchronized(_lock) {
|
||||||
if(_instance == null)
|
if(_instance == null) {
|
||||||
_instance = ChannelContentCache();
|
_instance = ChannelContentCache();
|
||||||
return _instance!!;
|
}
|
||||||
}
|
}
|
||||||
|
return _instance!!;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cachePagerResults(scope: CoroutineScope, pager: IPager<IPlatformContent>, onNewCacheHit: ((IPlatformContent)->Unit)? = null): IPager<IPlatformContent> {
|
fun cachePagerResults(scope: CoroutineScope, pager: IPager<IPlatformContent>, onNewCacheHit: ((IPlatformContent)->Unit)? = null): IPager<IPlatformContent> {
|
||||||
@@ -114,7 +158,7 @@ class ChannelContentCache {
|
|||||||
Logger.i(TAG, "Caching ${results.size} subscription initial results [${pager.hashCode()}]");
|
Logger.i(TAG, "Caching ${results.size} subscription initial results [${pager.hashCode()}]");
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val newCacheItems = instance.cacheVideos(results);
|
val newCacheItems = instance.cacheContents(results);
|
||||||
if(onNewCacheItem != null)
|
if(onNewCacheItem != null)
|
||||||
newCacheItems.forEach { onNewCacheItem!!(it) }
|
newCacheItems.forEach { onNewCacheItem!!(it) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -134,7 +178,7 @@ class ChannelContentCache {
|
|||||||
Logger.i(TAG, "Caching ${results.size} subscription results");
|
Logger.i(TAG, "Caching ${results.size} subscription results");
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val newCacheItems = instance.cacheVideos(results);
|
val newCacheItems = instance.cacheContents(results);
|
||||||
if(onNewCacheItem != null)
|
if(onNewCacheItem != null)
|
||||||
newCacheItems.forEach { onNewCacheItem!!(it) }
|
newCacheItems.forEach { onNewCacheItem!!(it) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
|||||||
import com.futo.platformplayer.builders.DashBuilder
|
import com.futo.platformplayer.builders.DashBuilder
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
|
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
@@ -352,16 +353,25 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (videoSource is IVideoUrlSource) {
|
if (videoSource is IVideoUrlSource)
|
||||||
ad.loadVideo("BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble());
|
ad.loadVideo("BUFFERED", videoSource.container, videoSource.getVideoUrl(), resumePosition, video.duration.toDouble());
|
||||||
} else if (audioSource is IAudioUrlSource) {
|
else if(videoSource is IHLSManifestSource)
|
||||||
|
ad.loadVideo("BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble());
|
||||||
|
else if (audioSource is IAudioUrlSource)
|
||||||
ad.loadVideo("BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble());
|
ad.loadVideo("BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble());
|
||||||
} else if (videoSource is LocalVideoSource) {
|
else if(audioSource is IHLSManifestAudioSource)
|
||||||
|
ad.loadVideo("BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble());
|
||||||
|
else if (videoSource is LocalVideoSource)
|
||||||
castLocalVideo(video, videoSource, resumePosition);
|
castLocalVideo(video, videoSource, resumePosition);
|
||||||
} else if (audioSource is LocalAudioSource) {
|
else if (audioSource is LocalAudioSource)
|
||||||
castLocalAudio(video, audioSource, resumePosition);
|
castLocalAudio(video, audioSource, resumePosition);
|
||||||
} else {
|
else {
|
||||||
throw Exception("Unhandled source type videoSource=$videoSource audioSource=$audioSource subtitleSource=$subtitleSource");
|
var str = listOf(
|
||||||
|
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
|
||||||
|
if(audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null,
|
||||||
|
if(subtitleSource != null) "Subtitles: ${subtitleSource::class.java.simpleName}" else null
|
||||||
|
).filterNotNull().joinToString(", ");
|
||||||
|
throw UnsupportedCastException(str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.graphics.Color
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
@@ -90,7 +91,9 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
Logger.i(TAG, "Started backfill");
|
||||||
processHandle.fullyBackfillServers()
|
processHandle.fullyBackfillServers()
|
||||||
|
Logger.i(TAG, "Finished backfill");
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to backfill servers.", e);
|
Logger.e(TAG, "Failed to backfill servers.", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.futo.platformplayer.encryption
|
||||||
|
|
||||||
|
class GEncryptionProvider {
|
||||||
|
companion object {
|
||||||
|
val instance: GEncryptionProviderV1 = GEncryptionProviderV1.instance;
|
||||||
|
val version = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-16
@@ -8,9 +8,8 @@ import java.security.KeyStore
|
|||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.KeyGenerator
|
import javax.crypto.KeyGenerator
|
||||||
import javax.crypto.spec.GCMParameterSpec
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class EncryptionProvider {
|
class GEncryptionProviderV0 {
|
||||||
private val _keyStore: KeyStore;
|
private val _keyStore: KeyStore;
|
||||||
private val secretKey: Key? get() = _keyStore.getKey(KEY_ALIAS, null);
|
private val secretKey: Key? get() = _keyStore.getKey(KEY_ALIAS, null);
|
||||||
|
|
||||||
@@ -25,45 +24,43 @@ class EncryptionProvider {
|
|||||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||||
.setRandomizedEncryptionRequired(false)
|
.setRandomizedEncryptionRequired(false)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
keyGenerator.generateKey();
|
keyGenerator.generateKey();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encrypt(decrypted: String, password: String? = null): String {
|
fun encrypt(decrypted: String): String {
|
||||||
val encodedBytes = encrypt(decrypted.toByteArray(), password);
|
val encodedBytes = encrypt(decrypted.toByteArray());
|
||||||
val encrypted = Base64.encodeToString(encodedBytes, Base64.DEFAULT);
|
val encrypted = Base64.encodeToString(encodedBytes, Base64.DEFAULT);
|
||||||
return encrypted;
|
return encrypted;
|
||||||
}
|
}
|
||||||
fun encrypt(decrypted: ByteArray, password: String? = null): ByteArray {
|
fun encrypt(decrypted: ByteArray): ByteArray {
|
||||||
val c: Cipher = Cipher.getInstance(AES_MODE);
|
val c: Cipher = Cipher.getInstance(AES_MODE);
|
||||||
val keyToUse = if(password == null) secretKey else SecretKeySpec(password.toByteArray(), "AES");
|
c.init(Cipher.ENCRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, FIXED_IV));
|
||||||
c.init(Cipher.ENCRYPT_MODE, keyToUse, GCMParameterSpec(128, FIXED_IV));
|
|
||||||
val encodedBytes: ByteArray = c.doFinal(decrypted);
|
val encodedBytes: ByteArray = c.doFinal(decrypted);
|
||||||
return encodedBytes;
|
return encodedBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decrypt(encrypted: String, password: String? = null): String {
|
fun decrypt(encrypted: String): String {
|
||||||
val c = Cipher.getInstance(AES_MODE);
|
val c = Cipher.getInstance(AES_MODE);
|
||||||
val keyToUse = if(password == null) secretKey else SecretKeySpec(password.toByteArray(), "AES");
|
c.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, FIXED_IV));
|
||||||
c.init(Cipher.DECRYPT_MODE, keyToUse, GCMParameterSpec(128, FIXED_IV));
|
|
||||||
val decrypted = String(c.doFinal(Base64.decode(encrypted, Base64.DEFAULT)));
|
val decrypted = String(c.doFinal(Base64.decode(encrypted, Base64.DEFAULT)));
|
||||||
return decrypted;
|
return decrypted;
|
||||||
}
|
}
|
||||||
fun decrypt(encrypted: ByteArray, password: String? = null): ByteArray {
|
fun decrypt(encrypted: ByteArray): ByteArray {
|
||||||
val c = Cipher.getInstance(AES_MODE);
|
val c = Cipher.getInstance(AES_MODE);
|
||||||
val keyToUse = if(password == null) secretKey else SecretKeySpec(password.toByteArray(), "AES");
|
c.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, FIXED_IV));
|
||||||
c.init(Cipher.DECRYPT_MODE, keyToUse, GCMParameterSpec(128, FIXED_IV));
|
|
||||||
return c.doFinal(encrypted);
|
return c.doFinal(encrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val instance: EncryptionProvider = EncryptionProvider();
|
val instance: GEncryptionProviderV0 = GEncryptionProviderV0();
|
||||||
|
|
||||||
private val FIXED_IV = byteArrayOf(12, 43, 127, 2, 99, 22, 6, 78, 24, 53, 8, 101);
|
private val FIXED_IV = byteArrayOf(12, 43, 127, 2, 99, 22, 6, 78, 24, 53, 8, 101);
|
||||||
private const val AndroidKeyStore = "AndroidKeyStore";
|
private const val AndroidKeyStore = "AndroidKeyStore";
|
||||||
private const val KEY_ALIAS = "FUTOMedia_Key";
|
private const val KEY_ALIAS = "FUTOMedia_Key";
|
||||||
private const val AES_MODE = "AES/GCM/NoPadding";
|
private const val AES_MODE = "AES/GCM/NoPadding";
|
||||||
private val TAG = "EncryptionProvider";
|
private const val TAG_LENGTH = 128
|
||||||
|
private val TAG = "GEncryptionProviderV0";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.futo.platformplayer.encryption
|
||||||
|
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import android.util.Base64
|
||||||
|
import java.security.Key
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
|
|
||||||
|
class GEncryptionProviderV1 {
|
||||||
|
private val _keyStore: KeyStore;
|
||||||
|
private val secretKey: Key? get() = _keyStore.getKey(KEY_ALIAS, null);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
_keyStore = KeyStore.getInstance(AndroidKeyStore);
|
||||||
|
_keyStore.load(null);
|
||||||
|
|
||||||
|
if (!_keyStore.containsAlias(KEY_ALIAS)) {
|
||||||
|
val keyGenerator: KeyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStore)
|
||||||
|
keyGenerator.init(KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||||
|
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||||
|
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||||
|
.setRandomizedEncryptionRequired(false)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
keyGenerator.generateKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encrypt(decrypted: String): String {
|
||||||
|
val encrypted = encrypt(decrypted.toByteArray());
|
||||||
|
val encoded = Base64.encodeToString(encrypted, Base64.DEFAULT);
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
|
fun encrypt(decrypted: ByteArray): ByteArray {
|
||||||
|
val ivBytes = generateIv()
|
||||||
|
val c: Cipher = Cipher.getInstance(AES_MODE);
|
||||||
|
c.init(Cipher.ENCRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, ivBytes));
|
||||||
|
val encodedBytes: ByteArray = c.doFinal(decrypted);
|
||||||
|
return ivBytes + encodedBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrypt(data: String): String {
|
||||||
|
val bytes = Base64.decode(data, Base64.DEFAULT)
|
||||||
|
return String(decrypt(bytes));
|
||||||
|
}
|
||||||
|
fun decrypt(bytes: ByteArray): ByteArray {
|
||||||
|
val encrypted = bytes.sliceArray(IntRange(IV_SIZE, bytes.size - 1))
|
||||||
|
val ivBytes = bytes.sliceArray(IntRange(0, IV_SIZE - 1))
|
||||||
|
|
||||||
|
val c = Cipher.getInstance(AES_MODE);
|
||||||
|
c.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, ivBytes));
|
||||||
|
return c.doFinal(encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateIv(): ByteArray {
|
||||||
|
val r = SecureRandom()
|
||||||
|
val ivBytes = ByteArray(IV_SIZE)
|
||||||
|
r.nextBytes(ivBytes)
|
||||||
|
return ivBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val instance: GEncryptionProviderV1 = GEncryptionProviderV1();
|
||||||
|
|
||||||
|
private const val AndroidKeyStore = "AndroidKeyStore";
|
||||||
|
private const val KEY_ALIAS = "FUTOMedia_Key";
|
||||||
|
private const val AES_MODE = "AES/GCM/NoPadding";
|
||||||
|
private const val IV_SIZE = 12;
|
||||||
|
private const val TAG_LENGTH = 128
|
||||||
|
private val TAG = "GEncryptionProviderV1";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.futo.platformplayer.encryption
|
||||||
|
|
||||||
|
class GPasswordEncryptionProvider {
|
||||||
|
companion object {
|
||||||
|
val version = 1;
|
||||||
|
val instance = GPasswordEncryptionProviderV1.instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.futo.platformplayer.encryption
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
class GPasswordEncryptionProviderV0 {
|
||||||
|
private val _key: SecretKeySpec;
|
||||||
|
|
||||||
|
constructor(password: String) {
|
||||||
|
_key = SecretKeySpec(password.toByteArray(), "AES");
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encrypt(decrypted: String): String {
|
||||||
|
val encodedBytes = encrypt(decrypted.toByteArray());
|
||||||
|
val encrypted = Base64.encodeToString(encodedBytes, Base64.DEFAULT);
|
||||||
|
return encrypted;
|
||||||
|
}
|
||||||
|
fun encrypt(decrypted: ByteArray): ByteArray {
|
||||||
|
val c: Cipher = Cipher.getInstance(AES_MODE);
|
||||||
|
c.init(Cipher.ENCRYPT_MODE, _key, GCMParameterSpec(TAG_LENGTH, FIXED_IV));
|
||||||
|
val encodedBytes: ByteArray = c.doFinal(decrypted);
|
||||||
|
return encodedBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrypt(encrypted: String): String {
|
||||||
|
val c = Cipher.getInstance(AES_MODE);
|
||||||
|
c.init(Cipher.DECRYPT_MODE, _key, GCMParameterSpec(TAG_LENGTH, FIXED_IV));
|
||||||
|
val decrypted = String(c.doFinal(Base64.decode(encrypted, Base64.DEFAULT)));
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
|
fun decrypt(encrypted: ByteArray): ByteArray {
|
||||||
|
val c = Cipher.getInstance(AES_MODE);
|
||||||
|
c.init(Cipher.DECRYPT_MODE, _key, GCMParameterSpec(TAG_LENGTH, FIXED_IV));
|
||||||
|
return c.doFinal(encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val FIXED_IV = byteArrayOf(12, 43, 127, 2, 99, 22, 6, 78, 24, 53, 8, 101);
|
||||||
|
private const val TAG_LENGTH = 128
|
||||||
|
private const val AES_MODE = "AES/GCM/NoPadding";
|
||||||
|
private val TAG = "GPasswordEncryptionProviderV0";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.futo.platformplayer.encryption
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.SecretKeyFactory
|
||||||
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
|
import javax.crypto.spec.PBEKeySpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
class GPasswordEncryptionProviderV1 {
|
||||||
|
fun encrypt(decrypted: String, password: String): String {
|
||||||
|
val encrypted = encrypt(decrypted.toByteArray(), password);
|
||||||
|
val encoded = Base64.encodeToString(encrypted, Base64.DEFAULT);
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encrypt(decrypted: ByteArray, password: String): ByteArray {
|
||||||
|
val saltBytes = generateSalt()
|
||||||
|
val ivBytes = generateIv()
|
||||||
|
val c: Cipher = Cipher.getInstance(AES_MODE);
|
||||||
|
val key = deriveKeyFromPassword(password, saltBytes)
|
||||||
|
|
||||||
|
c.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(TAG_LENGTH, ivBytes));
|
||||||
|
val encodedBytes: ByteArray = c.doFinal(decrypted);
|
||||||
|
return saltBytes + ivBytes + encodedBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrypt(data: String, password: String): String {
|
||||||
|
val bytes = Base64.decode(data, Base64.DEFAULT)
|
||||||
|
return String(decrypt(bytes, password));
|
||||||
|
}
|
||||||
|
fun decrypt(bytes: ByteArray, password: String): ByteArray {
|
||||||
|
val encrypted = bytes.sliceArray(IntRange(SALT_SIZE + IV_SIZE, bytes.size - 1))
|
||||||
|
val ivBytes = bytes.sliceArray(IntRange(SALT_SIZE, SALT_SIZE + IV_SIZE - 1))
|
||||||
|
val saltBytes = bytes.sliceArray(IntRange(0, SALT_SIZE - 1))
|
||||||
|
val key = deriveKeyFromPassword(password, saltBytes)
|
||||||
|
|
||||||
|
val c = Cipher.getInstance(AES_MODE);
|
||||||
|
c.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(TAG_LENGTH, ivBytes));
|
||||||
|
return c.doFinal(encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deriveKeyFromPassword(password: String, salt: ByteArray): SecretKeySpec {
|
||||||
|
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||||
|
val spec = PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_LENGTH)
|
||||||
|
val tmp = factory.generateSecret(spec)
|
||||||
|
return SecretKeySpec(tmp.encoded, "AES")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateSalt(): ByteArray {
|
||||||
|
val random = SecureRandom()
|
||||||
|
val salt = ByteArray(SALT_SIZE)
|
||||||
|
random.nextBytes(salt)
|
||||||
|
return salt
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateIv(): ByteArray {
|
||||||
|
val r = SecureRandom()
|
||||||
|
val ivBytes = ByteArray(IV_SIZE)
|
||||||
|
r.nextBytes(ivBytes)
|
||||||
|
return ivBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val instance = GPasswordEncryptionProviderV1();
|
||||||
|
private const val AES_MODE = "AES/GCM/NoPadding";
|
||||||
|
private const val IV_SIZE = 12
|
||||||
|
private const val SALT_SIZE = 16
|
||||||
|
private const val ITERATION_COUNT = 2 * 65536
|
||||||
|
private const val KEY_LENGTH = 256
|
||||||
|
private const val TAG_LENGTH = 128
|
||||||
|
private val TAG = "GPasswordEncryptionProviderV1";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.futo.platformplayer.exceptions
|
||||||
|
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
class UnsupportedCastException(msg: String) : Exception(msg) {
|
||||||
|
}
|
||||||
+2
-2
@@ -77,7 +77,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
|
|||||||
};
|
};
|
||||||
_textName?.text = channel.name;
|
_textName?.text = channel.name;
|
||||||
|
|
||||||
val metadata = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} subscribers" else "";
|
val metadata = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + (context?.getString(R.string.subscribers)?.lowercase() ?: "") else "";
|
||||||
_textMetadata?.text = metadata;
|
_textMetadata?.text = metadata;
|
||||||
_lastChannel = channel;
|
_lastChannel = channel;
|
||||||
setLinks(channel.links, channel.name);
|
setLinks(channel.links, channel.name);
|
||||||
@@ -91,7 +91,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
|
|||||||
l.removeAllViews();
|
l.removeAllViews();
|
||||||
|
|
||||||
if (links.isNotEmpty()) {
|
if (links.isNotEmpty()) {
|
||||||
_textFindOn?.text = "Find $name on";
|
_textFindOn?.text = getString(R.string.find_name_on).replace("{name}", name);
|
||||||
_textFindOn?.visibility = View.VISIBLE;
|
_textFindOn?.visibility = View.VISIBLE;
|
||||||
|
|
||||||
for (pair in links) {
|
for (pair in links) {
|
||||||
|
|||||||
+17
-8
@@ -25,6 +25,7 @@ import com.futo.platformplayer.api.media.structures.IPager
|
|||||||
import com.futo.platformplayer.api.media.structures.IRefreshPager
|
import com.futo.platformplayer.api.media.structures.IRefreshPager
|
||||||
import com.futo.platformplayer.api.media.structures.IReplacerPager
|
import com.futo.platformplayer.api.media.structures.IReplacerPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiPager
|
import com.futo.platformplayer.api.media.structures.MultiPager
|
||||||
|
import com.futo.platformplayer.cache.ChannelContentCache
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
@@ -33,6 +34,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.adapters.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.PreviewContentListAdapter
|
||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
@@ -74,9 +76,14 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
|
|
||||||
private val _taskLoadVideos = TaskHandler<IPlatformChannel, IPager<IPlatformContent>>({lifecycleScope}, {
|
private val _taskLoadVideos = TaskHandler<IPlatformChannel, IPager<IPlatformContent>>({lifecycleScope}, {
|
||||||
return@TaskHandler getContentPager(it);
|
return@TaskHandler getContentPager(it);
|
||||||
}).success {
|
}).success { livePager ->
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setPager(it);
|
|
||||||
|
val pager = if(_channel?.let { StateSubscriptions.instance.isSubscribed(it) } == true)
|
||||||
|
ChannelContentCache.cachePagerResults(lifecycleScope, livePager);
|
||||||
|
else livePager;
|
||||||
|
|
||||||
|
setPager(pager);
|
||||||
}
|
}
|
||||||
.exception<ScriptCaptchaRequiredException> { }
|
.exception<ScriptCaptchaRequiredException> { }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
@@ -248,7 +255,6 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
|
|
||||||
if(_pager is IReplacerPager<*>)
|
if(_pager is IReplacerPager<*>)
|
||||||
(_pager as IReplacerPager<*>).onReplaced.remove(this);
|
(_pager as IReplacerPager<*>).onReplaced.remove(this);
|
||||||
|
|
||||||
if(pager is IReplacerPager<*>) {
|
if(pager is IReplacerPager<*>) {
|
||||||
pager.onReplaced.subscribe(this) { oldItem, newItem ->
|
pager.onReplaced.subscribe(this) { oldItem, newItem ->
|
||||||
if(_pager != pager)
|
if(_pager != pager)
|
||||||
@@ -257,11 +263,14 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
if(_pager !is IPager<IPlatformContent>)
|
if(_pager !is IPager<IPlatformContent>)
|
||||||
return@subscribe;
|
return@subscribe;
|
||||||
|
|
||||||
val toReplaceIndex = _results.indexOfFirst { it == newItem };
|
|
||||||
if(toReplaceIndex >= 0) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
_results[toReplaceIndex] = newItem as IPlatformContent;
|
val toReplaceIndex = _results.indexOfFirst { it == oldItem };
|
||||||
_adapterResults?.let {
|
if (toReplaceIndex >= 0) {
|
||||||
it.notifyItemChanged(it.childToParentPosition(toReplaceIndex));
|
_results[toReplaceIndex] = newItem as IPlatformContent;
|
||||||
|
_adapterResults?.let {
|
||||||
|
it.notifyItemChanged(it.childToParentPosition(toReplaceIndex));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -56,7 +56,7 @@ class ChannelListFragment : Fragment, IChannelTabFragment {
|
|||||||
}.exception<ScriptCaptchaRequiredException> { }
|
}.exception<ScriptCaptchaRequiredException> { }
|
||||||
.exceptionWithParameter<Throwable> { ex, para ->
|
.exceptionWithParameter<Throwable> { ex, para ->
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", ex);
|
Logger.w(ChannelFragment.TAG, "Failed to load results.", ex);
|
||||||
UIDialogs.toast(requireContext(), "Failed to fetch\n${para}", false)
|
UIDialogs.toast(requireContext(), getString(R.string.failed_to_fetch) + "\n " + para, false)
|
||||||
loadNext();
|
loadNext();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+20
-33
@@ -1,21 +1,20 @@
|
|||||||
package com.futo.platformplayer.fragment.channel.tab
|
package com.futo.platformplayer.fragment.channel.tab
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.views.SupportView
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
||||||
private var _buttonStore: BigButton? = null;
|
private var _supportView: SupportView? = null
|
||||||
|
private var _textMonetization: TextView? = null
|
||||||
|
|
||||||
private var _lastChannel: IPlatformChannel? = null;
|
private var _lastChannel: IPlatformChannel? = null;
|
||||||
private var _lastPolycentricProfile: PolycentricProfile? = null;
|
private var _lastPolycentricProfile: PolycentricProfile? = null;
|
||||||
@@ -24,51 +23,39 @@ class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
|||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val view = inflater.inflate(R.layout.fragment_channel_monetization, container, false);
|
val view = inflater.inflate(R.layout.fragment_channel_monetization, container, false);
|
||||||
_buttonStore = view.findViewById(R.id.button_store);
|
_supportView = view.findViewById(R.id.support);
|
||||||
|
_textMonetization = view.findViewById(R.id.text_monetization);
|
||||||
_buttonStore?.onClick?.subscribe {
|
|
||||||
_lastPolycentricProfile?.systemState?.store?.let {
|
|
||||||
try {
|
|
||||||
val uri = Uri.parse(it);
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
|
||||||
intent.data = uri
|
|
||||||
startActivity(intent)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to open URI: '${it}'.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_lastChannel?.also {
|
_lastChannel?.also {
|
||||||
setChannel(it);
|
setChannel(it);
|
||||||
};
|
};
|
||||||
|
|
||||||
_lastPolycentricProfile?.also {
|
_supportView?.visibility = View.GONE;
|
||||||
setPolycentricProfile(it, animate = false);
|
_textMonetization?.visibility = View.GONE;
|
||||||
}
|
setPolycentricProfile(_lastPolycentricProfile, animate = false);
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
_buttonStore = null;
|
_supportView = null;
|
||||||
|
_textMonetization = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setChannel(channel: IPlatformChannel) {
|
override fun setChannel(channel: IPlatformChannel) {
|
||||||
_lastChannel = channel;
|
_lastChannel = channel;
|
||||||
_buttonStore?.visibility = View.GONE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?, animate: Boolean) {
|
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?, animate: Boolean) {
|
||||||
_lastPolycentricProfile = polycentricProfile;
|
_lastPolycentricProfile = polycentricProfile
|
||||||
|
if (polycentricProfile != null) {
|
||||||
if (polycentricProfile == null) {
|
_supportView?.setPolycentricProfile(polycentricProfile, animate)
|
||||||
return;
|
_supportView?.visibility = View.VISIBLE
|
||||||
}
|
_textMonetization?.visibility = View.GONE
|
||||||
|
} else {
|
||||||
if (polycentricProfile.systemState.store.isNotEmpty()) {
|
_supportView?.setPolycentricProfile(null, animate)
|
||||||
_buttonStore?.visibility = View.VISIBLE;
|
_supportView?.visibility = View.GONE
|
||||||
|
_textMonetization?.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-2
@@ -220,6 +220,13 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
buttons.removeAt(buyIndex)
|
buttons.removeAt(buyIndex)
|
||||||
buttons.add(0, button)
|
buttons.add(0, button)
|
||||||
}
|
}
|
||||||
|
//Force faq to be second
|
||||||
|
val faqIndex = buttons.indexOfFirst { b -> b.id == 97 };
|
||||||
|
if (faqIndex != -1) {
|
||||||
|
val button = buttons[faqIndex]
|
||||||
|
buttons.removeAt(faqIndex)
|
||||||
|
buttons.add(if (buttons.size == 1) 1 else 0, button)
|
||||||
|
}
|
||||||
|
|
||||||
for (data in buttons) {
|
for (data in buttons) {
|
||||||
val button = MenuButton(context, data, _fragment, true);
|
val button = MenuButton(context, data, _fragment, true);
|
||||||
@@ -245,8 +252,8 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
||||||
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
|
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
|
||||||
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
||||||
if (_buttonsVisible - 2 >= defs.size) {
|
if (_buttonsVisible - 1 >= defs.size) {
|
||||||
updateBottomMenuButtons(defs.slice(IntRange(0, defs.size - 1)).toMutableList(), false);
|
updateBottomMenuButtons(defs.toMutableList(), false);
|
||||||
} else {
|
} else {
|
||||||
updateBottomMenuButtons(defs.slice(IntRange(0, _buttonsVisible - 2)).toMutableList(), true);
|
updateBottomMenuButtons(defs.slice(IntRange(0, _buttonsVisible - 2)).toMutableList(), true);
|
||||||
updateMoreButtons(defs.slice(IntRange(_buttonsVisible - 1, defs.size - 1)).toMutableList());
|
updateMoreButtons(defs.slice(IntRange(_buttonsVisible - 1, defs.size - 1)).toMutableList());
|
||||||
@@ -289,6 +296,9 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
if (!StatePayment.instance.hasPaid) {
|
if (!StatePayment.instance.hasPaid) {
|
||||||
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
|
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
|
||||||
}
|
}
|
||||||
|
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz, R.string.faq, canToggle = false, { false }, {
|
||||||
|
it.navigate<BrowserFragment>(Settings.URL_FAQ);
|
||||||
|
}))
|
||||||
|
|
||||||
//Add conditional buttons here, when you add a conditional button, be sure to add the register and unregister events for when the button needs to be updated
|
//Add conditional buttons here, when you add a conditional button, be sure to add the register and unregister events for when the button needs to be updated
|
||||||
|
|
||||||
|
|||||||
@@ -60,12 +60,12 @@ class BuyFragment : MainFragment() {
|
|||||||
|
|
||||||
_paymentManager = PaymentManager(StatePayment.instance, fragment, _overlayPaying) { success, purchaseId, exception ->
|
_paymentManager = PaymentManager(StatePayment.instance, fragment, _overlayPaying) { success, purchaseId, exception ->
|
||||||
if(success) {
|
if(success) {
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_check, "Payment succeeded", "Thanks for your purchase, a key will be sent to your email after your payment has been received!", null, 0,
|
UIDialogs.showDialog(context, R.drawable.ic_check, context.getString(R.string.payment_succeeded), context.getString(R.string.thanks_for_your_purchase_a_key_will_be_sent_to_your_email_after_your_payment_has_been_received), null, 0,
|
||||||
UIDialogs.Action("Ok", {}, UIDialogs.ActionStyle.PRIMARY));
|
UIDialogs.Action("Ok", {}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
_fragment.close(true);
|
_fragment.close(true);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
UIDialogs.showGeneralErrorDialog(context, "Payment failed", exception);
|
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.payment_failed), exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,12 +107,12 @@ class BuyFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun paid() {
|
private fun paid() {
|
||||||
val licenseInput = SlideUpMenuTextInput(context, "License");
|
val licenseInput = SlideUpMenuTextInput(context, context.getString(R.string.license));
|
||||||
val productLicenseDialog = SlideUpMenuOverlay(context, findViewById<FrameLayout>(R.id.overlay_paid), "Enter license key", "Ok", true, licenseInput);
|
val productLicenseDialog = SlideUpMenuOverlay(context, findViewById<FrameLayout>(R.id.overlay_paid), context.getString(R.string.enter_license_key), context.getString(R.string.ok), true, licenseInput);
|
||||||
productLicenseDialog.onOK.subscribe {
|
productLicenseDialog.onOK.subscribe {
|
||||||
val licenseText = licenseInput.text;
|
val licenseText = licenseInput.text;
|
||||||
if (licenseText.isNullOrEmpty()) {
|
if (licenseText.isNullOrEmpty()) {
|
||||||
UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, "Invalid license key");
|
UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, context.getString(R.string.invalid_license_key));
|
||||||
return@subscribe;
|
return@subscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,19 +127,19 @@ class BuyFragment : MainFragment() {
|
|||||||
licenseInput.clear();
|
licenseInput.clear();
|
||||||
productLicenseDialog.hide(true);
|
productLicenseDialog.hide(true);
|
||||||
|
|
||||||
UIDialogs.showDialogOk(context, R.drawable.ic_check, "Your license key has been set!\nAn app restart might be required.");
|
UIDialogs.showDialogOk(context, R.drawable.ic_check, context.getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required));
|
||||||
_fragment.close(true);
|
_fragment.close(true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, "Invalid license key");
|
UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, context.getString(R.string.invalid_license_key));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
Logger.e("BuyFragment", "Failed to activate key", ex);
|
Logger.e("BuyFragment", "Failed to activate key", ex);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.showGeneralErrorDialog(context, "Failed to activate key", ex);
|
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_activate_key), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-56
@@ -9,6 +9,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
@@ -41,6 +42,7 @@ import com.futo.platformplayer.polycentric.PolycentricCache
|
|||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||||
import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter
|
import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter
|
||||||
@@ -100,6 +102,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
private var _viewPager: ViewPager2;
|
private var _viewPager: ViewPager2;
|
||||||
private var _tabLayoutMediator: TabLayoutMediator;
|
private var _tabLayoutMediator: TabLayoutMediator;
|
||||||
private var _buttonSubscribe: SubscribeButton;
|
private var _buttonSubscribe: SubscribeButton;
|
||||||
|
private var _buttonSubscriptionSettings: ImageButton;
|
||||||
|
|
||||||
private var _overlayContainer: FrameLayout;
|
private var _overlayContainer: FrameLayout;
|
||||||
private var _overlay_loading: LinearLayout;
|
private var _overlay_loading: LinearLayout;
|
||||||
@@ -141,7 +144,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
|
|
||||||
UIDialogs.showDialog(context,
|
UIDialogs.showDialog(context,
|
||||||
R.drawable.ic_sources,
|
R.drawable.ic_sources,
|
||||||
"No source enabled to support this channel\n(${_url})", null, null,
|
context.getString(R.string.no_source_enabled_to_support_this_channel) + "\n(${_url})", null, null,
|
||||||
0,
|
0,
|
||||||
UIDialogs.Action("Back", {
|
UIDialogs.Action("Back", {
|
||||||
fragment.close(true);
|
fragment.close(true);
|
||||||
@@ -160,10 +163,25 @@ class ChannelFragment : MainFragment() {
|
|||||||
_creatorThumbnail = findViewById(R.id.creator_thumbnail);
|
_creatorThumbnail = findViewById(R.id.creator_thumbnail);
|
||||||
_imageBanner = findViewById(R.id.image_channel_banner);
|
_imageBanner = findViewById(R.id.image_channel_banner);
|
||||||
_buttonSubscribe = findViewById(R.id.button_subscribe);
|
_buttonSubscribe = findViewById(R.id.button_subscribe);
|
||||||
|
_buttonSubscriptionSettings = findViewById(R.id.button_sub_settings);
|
||||||
_overlay_loading = findViewById(R.id.channel_loading_overlay);
|
_overlay_loading = findViewById(R.id.channel_loading_overlay);
|
||||||
_overlay_loading_spinner = findViewById(R.id.channel_loader);
|
_overlay_loading_spinner = findViewById(R.id.channel_loader);
|
||||||
_overlayContainer = findViewById(R.id.overlay_container);
|
_overlayContainer = findViewById(R.id.overlay_container);
|
||||||
|
|
||||||
|
_buttonSubscribe.onSubscribed.subscribe {
|
||||||
|
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||||
|
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
|
||||||
|
}
|
||||||
|
_buttonSubscribe.onUnSubscribed.subscribe {
|
||||||
|
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
_buttonSubscriptionSettings.setOnClickListener {
|
||||||
|
val url = channel?.url ?: _url ?: return@setOnClickListener;
|
||||||
|
val sub = StateSubscriptions.instance.getSubscription(url) ?: return@setOnClickListener;
|
||||||
|
UISlideOverlays.showSubscriptionOptionsOverlay(sub, _overlayContainer);
|
||||||
|
};
|
||||||
|
|
||||||
//TODO: Determine if this is really the only solution (isSaveEnabled=false)
|
//TODO: Determine if this is really the only solution (isSaveEnabled=false)
|
||||||
viewPager.isSaveEnabled = false;
|
viewPager.isSaveEnabled = false;
|
||||||
viewPager.registerOnPageChangeCallback(_onPageChangeCallback);
|
viewPager.registerOnPageChangeCallback(_onPageChangeCallback);
|
||||||
@@ -246,28 +264,46 @@ class ChannelFragment : MainFragment() {
|
|||||||
|
|
||||||
if (parameter is String) {
|
if (parameter is String) {
|
||||||
_buttonSubscribe.setSubscribeChannel(parameter);
|
_buttonSubscribe.setSubscribeChannel(parameter);
|
||||||
_textChannel.text = "";
|
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
|
||||||
_textChannelSub.text = "";
|
setPolycentricProfileOr(parameter) {
|
||||||
|
_textChannel.text = "";
|
||||||
|
_textChannelSub.text = "";
|
||||||
|
_creatorThumbnail.setThumbnail(null, true);
|
||||||
|
Glide.with(_imageBanner)
|
||||||
|
.clear(_imageBanner);
|
||||||
|
};
|
||||||
|
|
||||||
_url = parameter;
|
_url = parameter;
|
||||||
loadChannel();
|
loadChannel();
|
||||||
} else if (parameter is SerializedChannel) {
|
} else if (parameter is SerializedChannel) {
|
||||||
showChannel(parameter);
|
showChannel(parameter);
|
||||||
_url = parameter.url;
|
_url = parameter.url;
|
||||||
_creatorThumbnail.setThumbnail(parameter.url, false);
|
|
||||||
loadChannel();
|
loadChannel();
|
||||||
} else if (parameter is IPlatformChannel)
|
} else if (parameter is IPlatformChannel)
|
||||||
showChannel(parameter);
|
showChannel(parameter);
|
||||||
else if (parameter is PlatformAuthorLink) {
|
else if (parameter is PlatformAuthorLink) {
|
||||||
_textChannel.text = parameter.name;
|
setPolycentricProfileOr(parameter.url) {
|
||||||
_textChannelSub.text = "";
|
_textChannel.text = parameter.name;
|
||||||
_creatorThumbnail.setThumbnail(parameter.url, false);
|
_textChannelSub.text = "";
|
||||||
|
_creatorThumbnail.setThumbnail(parameter.thumbnail, true);
|
||||||
|
Glide.with(_imageBanner)
|
||||||
|
.clear(_imageBanner);
|
||||||
|
|
||||||
|
_taskLoadPolycentricProfile.run(parameter.id);
|
||||||
|
};
|
||||||
|
|
||||||
_url = parameter.url;
|
_url = parameter.url;
|
||||||
loadChannel();
|
loadChannel();
|
||||||
} else if (parameter is Subscription) {
|
} else if (parameter is Subscription) {
|
||||||
_textChannel.text = parameter.channel.name;
|
setPolycentricProfileOr(parameter.channel.url) {
|
||||||
_textChannelSub.text = "";
|
_textChannel.text = parameter.channel.name;
|
||||||
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, false);
|
_textChannelSub.text = "";
|
||||||
|
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true);
|
||||||
|
Glide.with(_imageBanner)
|
||||||
|
.clear(_imageBanner);
|
||||||
|
|
||||||
|
_taskLoadPolycentricProfile.run(parameter.channel.id);
|
||||||
|
};
|
||||||
|
|
||||||
_url = parameter.channel.url;
|
_url = parameter.channel.url;
|
||||||
loadChannel();
|
loadChannel();
|
||||||
@@ -327,19 +363,19 @@ class ChannelFragment : MainFragment() {
|
|||||||
_fragment.topBar?.onShown(channel);
|
_fragment.topBar?.onShown(channel);
|
||||||
|
|
||||||
val buttons = arrayListOf(Pair(R.drawable.ic_playlist_add) {
|
val buttons = arrayListOf(Pair(R.drawable.ic_playlist_add) {
|
||||||
UIDialogs.showConfirmationDialog(context, "Do you want to convert channel ${channel.name} to a playlist?", {
|
UIDialogs.showConfirmationDialog(context, context.getString(R.string.do_you_want_to_convert_channel_channelname_to_a_playlist).replace("{channelName}", channel.name), {
|
||||||
UIDialogs.showDialogProgress(context) {
|
UIDialogs.showDialogProgress(context) {
|
||||||
_fragment.lifecycleScope.launch(Dispatchers.IO) {
|
_fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
StatePlaylists.instance.createPlaylistFromChannel(channel) { page ->
|
StatePlaylists.instance.createPlaylistFromChannel(channel) { page ->
|
||||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
it.setText("${channel.name}\nPage ${page}");
|
it.setText("${channel.name}\n" + context.getString(R.string.page) + " $page");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch(ex: Exception) {
|
catch(ex: Exception) {
|
||||||
Logger.e(TAG, "Error", ex);
|
Logger.e(TAG, "Error", ex);
|
||||||
UIDialogs.showGeneralErrorDialog(context, "Failed to convert channel", ex);
|
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_convert_channel), ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@@ -350,24 +386,22 @@ class ChannelFragment : MainFragment() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
val plugin = StatePlatform.instance.getChannelClientOrNull(channel.url);
|
_fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
if (plugin != null && plugin.capabilities.hasSearchChannelContents) {
|
val plugin = StatePlatform.instance.getChannelClientOrNull(channel.url);
|
||||||
buttons.add(Pair(R.drawable.ic_search) {
|
withContext(Dispatchers.Main) {
|
||||||
_fragment.navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.VIDEO, channel.url));
|
if (plugin != null && plugin.capabilities.hasSearchChannelContents) {
|
||||||
});
|
buttons.add(Pair(R.drawable.ic_search) {
|
||||||
|
_fragment.navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.VIDEO, channel.url));
|
||||||
|
});
|
||||||
|
|
||||||
|
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons);
|
|
||||||
|
|
||||||
_buttonSubscribe.setSubscribeChannel(channel);
|
_buttonSubscribe.setSubscribeChannel(channel);
|
||||||
_textChannel.text = channel.name;
|
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
|
||||||
_textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} subscribers" else "";
|
_textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + context.getString(R.string.subscribers).lowercase() else "";
|
||||||
|
|
||||||
_creatorThumbnail.setThumbnail(channel.thumbnail, true);
|
|
||||||
Glide.with(_imageBanner)
|
|
||||||
.load(channel.banner)
|
|
||||||
.crossfade()
|
|
||||||
.into(_imageBanner)
|
|
||||||
|
|
||||||
//TODO: Find a better way to access the adapter fragments..
|
//TODO: Find a better way to access the adapter fragments..
|
||||||
|
|
||||||
@@ -381,51 +415,68 @@ class ChannelFragment : MainFragment() {
|
|||||||
|
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
|
|
||||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(channel.url);
|
setPolycentricProfileOr(channel.url) {
|
||||||
|
_textChannel.text = channel.name;
|
||||||
|
_creatorThumbnail.setThumbnail(channel.thumbnail, true);
|
||||||
|
Glide.with(_imageBanner)
|
||||||
|
.load(channel.banner)
|
||||||
|
.crossfade()
|
||||||
|
.into(_imageBanner);
|
||||||
|
|
||||||
|
_taskLoadPolycentricProfile.run(channel.id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setPolycentricProfileOr(url: String, or: () -> Unit) {
|
||||||
|
val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(it.url) };
|
||||||
if (cachedProfile != null) {
|
if (cachedProfile != null) {
|
||||||
setPolycentricProfile(cachedProfile, animate = false);
|
setPolycentricProfile(cachedProfile, animate = false);
|
||||||
} else {
|
} else {
|
||||||
setPolycentricProfile(null, animate = false);
|
setPolycentricProfile(null, animate = false);
|
||||||
_taskLoadPolycentricProfile.run(channel.id);
|
or();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||||
Log.i(TAG, "setPolycentricProfile(cachedPolycentricProfile = $cachedPolycentricProfile, animate = $animate)")
|
Log.i(TAG, "setPolycentricProfile(cachedPolycentricProfile = $cachedPolycentricProfile, animate = $animate)")
|
||||||
|
|
||||||
val polycentricProfile = cachedPolycentricProfile?.profile;
|
val dp_35 = 35.dp(resources)
|
||||||
if (polycentricProfile != null) {
|
val profile = cachedPolycentricProfile?.profile;
|
||||||
_fragment.topBar?.onShown(polycentricProfile);
|
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
|
||||||
|
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||||
|
|
||||||
if (polycentricProfile.systemState.username.isNotBlank())
|
if (avatar != null) {
|
||||||
_textChannel.text = polycentricProfile.systemState.username;
|
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||||
|
} else {
|
||||||
|
_creatorThumbnail.setThumbnail(channel?.thumbnail, animate);
|
||||||
|
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||||
|
}
|
||||||
|
|
||||||
val dp_35 = 35.dp(resources)
|
val banner = profile?.systemState?.banner?.selectHighestResolutionImage()
|
||||||
val avatar = polycentricProfile.systemState.avatar?.selectBestImage(dp_35 * dp_35)
|
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||||
?.let { it.toURLInfoSystemLinkUrl(polycentricProfile.system.toProto(), it.process, polycentricProfile.systemState.servers.toList()) };
|
|
||||||
|
|
||||||
if (avatar != null) {
|
if (banner != null) {
|
||||||
_creatorThumbnail.setThumbnail(avatar, true);
|
Glide.with(_imageBanner)
|
||||||
} else {
|
.load(banner)
|
||||||
_creatorThumbnail.setHarborAvailable(true, true);
|
.crossfade()
|
||||||
}
|
.into(_imageBanner);
|
||||||
|
} else {
|
||||||
|
Glide.with(_imageBanner)
|
||||||
|
.load(channel?.banner)
|
||||||
|
.crossfade()
|
||||||
|
.into(_imageBanner);
|
||||||
|
}
|
||||||
|
|
||||||
val banner = polycentricProfile.systemState.banner?.selectHighestResolutionImage()
|
if (profile != null) {
|
||||||
?.let { it.toURLInfoSystemLinkUrl(polycentricProfile.system.toProto(), it.process, polycentricProfile.systemState.servers.toList()) };
|
_fragment.topBar?.onShown(profile);
|
||||||
|
_textChannel.text = profile.systemState.username;
|
||||||
if (banner != null) {
|
|
||||||
Glide.with(_imageBanner)
|
|
||||||
.load(banner)
|
|
||||||
.crossfade()
|
|
||||||
.into(_imageBanner);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(_viewPager.adapter as ChannelViewPagerAdapter?)?.let {
|
(_viewPager.adapter as ChannelViewPagerAdapter?)?.let {
|
||||||
it.getFragment<ChannelAboutFragment>().setPolycentricProfile(polycentricProfile, animate);
|
it.getFragment<ChannelAboutFragment>().setPolycentricProfile(profile, animate);
|
||||||
it.getFragment<ChannelMonetizationFragment>().setPolycentricProfile(polycentricProfile, animate);
|
it.getFragment<ChannelMonetizationFragment>().setPolycentricProfile(profile, animate);
|
||||||
it.getFragment<ChannelListFragment>().setPolycentricProfile(polycentricProfile, animate);
|
it.getFragment<ChannelListFragment>().setPolycentricProfile(profile, animate);
|
||||||
it.getFragment<ChannelContentsFragment>().setPolycentricProfile(polycentricProfile, animate);
|
it.getFragment<ChannelContentsFragment>().setPolycentricProfile(profile, animate);
|
||||||
//TODO: Call on other tabs as needed
|
//TODO: Call on other tabs as needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-9
@@ -15,7 +15,9 @@ import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
|||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.structures.*
|
import com.futo.platformplayer.api.media.structures.*
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.video.PlayerManager
|
import com.futo.platformplayer.video.PlayerManager
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.adapters.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.PreviewContentListAdapter
|
||||||
@@ -24,6 +26,7 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
|||||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.PreviewNestedVideoViewHolder
|
import com.futo.platformplayer.views.adapters.PreviewNestedVideoViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.PreviewVideoViewHolder
|
import com.futo.platformplayer.views.adapters.PreviewVideoViewHolder
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
|
||||||
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
|
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
|
||||||
@@ -69,22 +72,31 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
//TODO: Reconstruct search video from detail if search is null
|
//TODO: Reconstruct search video from detail if search is null
|
||||||
_overlayContainer.let {
|
_overlayContainer.let {
|
||||||
if(content is IPlatformVideo)
|
if(content is IPlatformVideo)
|
||||||
UISlideOverlays.showVideoOptionsOverlay(content, it) {
|
UISlideOverlays.showVideoOptionsOverlay(content, it, SlideUpMenuItem(context, R.drawable.ic_visibility_off, context.getString(R.string.hide), context.getString(R.string.hide_from_home), "hide",
|
||||||
if (fragment is HomeFragment) {
|
{ StateMeta.instance.addHiddenVideo(content.url);
|
||||||
val removeIndex = recyclerData.results.indexOf(content);
|
if (fragment is HomeFragment) {
|
||||||
if (removeIndex >= 0) {
|
val removeIndex = recyclerData.results.indexOf(content);
|
||||||
recyclerData.results.removeAt(removeIndex);
|
if (removeIndex >= 0) {
|
||||||
recyclerData.adapter.notifyItemRemoved(recyclerData.adapter.childToParentPosition(removeIndex));
|
recyclerData.results.removeAt(removeIndex);
|
||||||
|
recyclerData.adapter.notifyItemRemoved(recyclerData.adapter.childToParentPosition(removeIndex));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
};
|
SlideUpMenuItem(context, R.drawable.ic_playlist, context.getString(R.string.play_feed_as_queue), context.getString(R.string.play_entire_feed), "playFeed",
|
||||||
|
{
|
||||||
|
val newQueue = listOf(content) + recyclerData.results
|
||||||
|
.filterIsInstance<IPlatformVideo>()
|
||||||
|
.filter { it != content };
|
||||||
|
StatePlayer.instance.setQueue(newQueue, StatePlayer.TYPE_QUEUE, "Feed Queue", true, false);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
adapter.onAddToQueueClicked.subscribe(this) {
|
adapter.onAddToQueueClicked.subscribe(this) {
|
||||||
if(it is IPlatformVideo) {
|
if(it is IPlatformVideo) {
|
||||||
StatePlayer.instance.addToQueue(it);
|
StatePlayer.instance.addToQueue(it);
|
||||||
val name = if (it.name.length > 20) (it.name.subSequence(0, 20).toString() + "...") else it.name;
|
val name = if (it.name.length > 20) (it.name.subSequence(0, 20).toString() + "...") else it.name;
|
||||||
UIDialogs.toast(context, "Queued [$name]", false);
|
UIDialogs.toast(context, context.getString(R.string.queued) + " [$name]", false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-10
@@ -17,6 +17,7 @@ import com.futo.platformplayer.api.media.structures.IPager
|
|||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
|
import com.futo.platformplayer.isHttpUrl
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -62,7 +63,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
||||||
_view?.setPreviewsEnabled(previewsEnabled);
|
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.search.previewFeedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
@@ -89,9 +90,11 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
})
|
})
|
||||||
.success { loadedResult(it); }.exception<ScriptCaptchaRequiredException> { }
|
.success { loadedResult(it); }.exception<ScriptCaptchaRequiredException> { }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
Logger.w(TAG, "Failed to load results.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPreviewsEnabled(Settings.instance.search.previewFeedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cleanup() {
|
override fun cleanup() {
|
||||||
@@ -101,14 +104,12 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
|
|
||||||
fun onShown(parameter: Any?, isBack: Boolean) {
|
fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
if(parameter is SuggestionsFragmentData) {
|
if(parameter is SuggestionsFragmentData) {
|
||||||
if(!isBack) {
|
setQuery(parameter.query, false);
|
||||||
setQuery(parameter.query, false);
|
setChannelUrl(parameter.channelUrl, false);
|
||||||
setChannelUrl(parameter.channelUrl, false);
|
|
||||||
|
|
||||||
fragment.topBar?.apply {
|
fragment.topBar?.apply {
|
||||||
if (this is SearchTopBarFragment) {
|
if (this is SearchTopBarFragment) {
|
||||||
this.setText(parameter.query);
|
this.setText(parameter.query);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,7 +146,10 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onSearch.subscribe(this) {
|
onSearch.subscribe(this) {
|
||||||
setQuery(it, true);
|
if(it.isHttpUrl())
|
||||||
|
navigate<VideoDetailFragment>(it);
|
||||||
|
else
|
||||||
|
setQuery(it, true);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-9
@@ -71,16 +71,14 @@ class CreatorSearchResultsFragment : MainFragment() {
|
|||||||
|
|
||||||
fun onShown(parameter: Any?, isBack: Boolean) {
|
fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
if(parameter is String) {
|
if(parameter is String) {
|
||||||
if(!isBack) {
|
setQuery(parameter);
|
||||||
setQuery(parameter);
|
|
||||||
|
|
||||||
fragment.topBar?.apply {
|
fragment.topBar?.apply {
|
||||||
if (this is SearchTopBarFragment) {
|
if (this is SearchTopBarFragment) {
|
||||||
setText(parameter);
|
setText(parameter);
|
||||||
onSearch.subscribe(this) {
|
onSearch.subscribe(this) {
|
||||||
setQuery(it);
|
setQuery(it);
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -6,10 +6,12 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.FrameLayout
|
||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
|
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
|
||||||
|
|
||||||
class CreatorsFragment : MainFragment() {
|
class CreatorsFragment : MainFragment() {
|
||||||
@@ -18,13 +20,16 @@ class CreatorsFragment : MainFragment() {
|
|||||||
override val hasBottomBar: Boolean get() = true;
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
private var _spinnerSortBy: Spinner? = null;
|
private var _spinnerSortBy: Spinner? = null;
|
||||||
|
private var _overlayContainer: FrameLayout? = null;
|
||||||
|
|
||||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
val view = inflater.inflate(R.layout.fragment_creators, container, false);
|
val view = inflater.inflate(R.layout.fragment_creators, container, false);
|
||||||
|
|
||||||
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
|
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
|
||||||
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
|
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
|
||||||
|
adapter.onSettings.subscribe { sub -> _overlayContainer?.let { UISlideOverlays.showSubscriptionOptionsOverlay(sub, it) } }
|
||||||
|
|
||||||
|
_overlayContainer = view.findViewById(R.id.overlay_container);
|
||||||
val spinnerSortBy: Spinner = view.findViewById(R.id.spinner_sortby);
|
val spinnerSortBy: Spinner = view.findViewById(R.id.spinner_sortby);
|
||||||
spinnerSortBy.adapter = ArrayAdapter(view.context, R.layout.spinner_item_simple, resources.getStringArray(R.array.subscriptions_sortby_array)).also {
|
spinnerSortBy.adapter = ArrayAdapter(view.context, R.layout.spinner_item_simple, resources.getStringArray(R.array.subscriptions_sortby_array)).also {
|
||||||
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
||||||
@@ -48,6 +53,7 @@ class CreatorsFragment : MainFragment() {
|
|||||||
override fun onDestroyMainView() {
|
override fun onDestroyMainView() {
|
||||||
super.onDestroyMainView();
|
super.onDestroyMainView();
|
||||||
_spinnerSortBy = null;
|
_spinnerSortBy = null;
|
||||||
|
_overlayContainer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
+4
-4
@@ -136,8 +136,8 @@ class DownloadsFragment : MainFragment() {
|
|||||||
|
|
||||||
fun reloadUI() {
|
fun reloadUI() {
|
||||||
val usage = StateDownloads.instance.getTotalUsage(true);
|
val usage = StateDownloads.instance.getTotalUsage(true);
|
||||||
_usageUsed.text = "${usage.usage.toHumanBytesSize()} Used";
|
_usageUsed.text = "${usage.usage.toHumanBytesSize()} " + context.getString(R.string.used);
|
||||||
_usageAvailable.text = "${usage.available.toHumanBytesSize()} Available";
|
_usageAvailable.text = "${usage.available.toHumanBytesSize()} " + context.getString(R.string.available);
|
||||||
_usageProgress.progress = usage.percentage.toFloat();
|
_usageProgress.progress = usage.percentage.toFloat();
|
||||||
|
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ class DownloadsFragment : MainFragment() {
|
|||||||
_listPlaylistsContainer.visibility = GONE;
|
_listPlaylistsContainer.visibility = GONE;
|
||||||
else {
|
else {
|
||||||
_listPlaylistsContainer.visibility = VISIBLE;
|
_listPlaylistsContainer.visibility = VISIBLE;
|
||||||
_listPlaylistsMeta.text = "(${playlists.size} playlists, ${playlists.sumOf { it.playlist.videos.size }} videos)";
|
_listPlaylistsMeta.text = "(${playlists.size} ${context.getString(R.string.playlists).lowercase()}, ${playlists.sumOf { it.playlist.videos.size }} ${context.getString(R.string.videos).lowercase()})";
|
||||||
|
|
||||||
_listPlaylists.removeAllViews();
|
_listPlaylists.removeAllViews();
|
||||||
for(view in playlists.map { PlaylistDownloadItem(context, it) }) {
|
for(view in playlists.map { PlaylistDownloadItem(context, it) }) {
|
||||||
@@ -176,7 +176,7 @@ class DownloadsFragment : MainFragment() {
|
|||||||
_listDownloadedHeader.visibility = GONE;
|
_listDownloadedHeader.visibility = GONE;
|
||||||
} else {
|
} else {
|
||||||
_listDownloadedHeader.visibility = VISIBLE;
|
_listDownloadedHeader.visibility = VISIBLE;
|
||||||
_listDownloadedMeta.text = "(${downloaded.size} videos)";
|
_listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()})";
|
||||||
}
|
}
|
||||||
|
|
||||||
_listDownloaded.setData(downloaded);
|
_listDownloaded.setData(downloaded);
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
|
|
||||||
_toolbarContentView = findViewById(R.id.container_toolbar_content);
|
_toolbarContentView = findViewById(R.id.container_toolbar_content);
|
||||||
|
|
||||||
|
var filteredNextPageCounter = 0;
|
||||||
_nextPageHandler = TaskHandler<TPager, List<TResult>>({fragment.lifecycleScope}, {
|
_nextPageHandler = TaskHandler<TPager, List<TResult>>({fragment.lifecycleScope}, {
|
||||||
if (it is IAsyncPager<*>)
|
if (it is IAsyncPager<*>)
|
||||||
it.nextPageAsync();
|
it.nextPageAsync();
|
||||||
@@ -141,10 +142,18 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
val filteredResults = filterResults(it);
|
val filteredResults = filterResults(it);
|
||||||
recyclerData.results.addAll(filteredResults);
|
recyclerData.results.addAll(filteredResults);
|
||||||
recyclerData.resultsUnfiltered.addAll(it);
|
recyclerData.resultsUnfiltered.addAll(it);
|
||||||
recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size);
|
if(filteredResults.isEmpty()) {
|
||||||
|
filteredNextPageCounter++
|
||||||
|
if(filteredNextPageCounter <= 4)
|
||||||
|
loadNextPage()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
filteredNextPageCounter = 0;
|
||||||
|
recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size);
|
||||||
|
}
|
||||||
}.exception<Throwable> {
|
}.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load next page.", it);
|
Logger.w(TAG, "Failed to load next page.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, "Failed to load next page", it, {
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
||||||
loadNextPage();
|
loadNextPage();
|
||||||
});
|
});
|
||||||
//UIDialogs.showDataRetryDialog(layoutInflater, it.message, { loadNextPage() });
|
//UIDialogs.showDataRetryDialog(layoutInflater, it.message, { loadNextPage() });
|
||||||
@@ -256,7 +265,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
if(jsVideoPager != null)
|
if(jsVideoPager != null)
|
||||||
UIDialogs.toast(it, "Plugin ${jsVideoPager.getPluginConfig().name} failed:\n${kv.value.message}", false);
|
UIDialogs.toast(it, context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", jsVideoPager.getPluginConfig().name).replace("{message}", kv.value.message ?: ""), false);
|
||||||
else
|
else
|
||||||
UIDialogs.toast(it, kv.value.message ?: "", false);
|
UIDialogs.toast(it, kv.value.message ?: "", false);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -333,11 +342,11 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
parentPager.onPagerError.subscribe(this) {
|
parentPager.onPagerError.subscribe(this) {
|
||||||
Logger.e(TAG, "Search pager failed: ${it.message}", it);
|
Logger.e(TAG, "Search pager failed: ${it.message}", it);
|
||||||
when (it) {
|
when (it) {
|
||||||
is PluginException -> UIDialogs.toast("Plugin [${it.config.name}] failed due to:\n${it.message}")
|
is PluginException -> UIDialogs.toast("Plugin [{pluginName}] failed due to:\n{exceptionMessage}".replace("{pluginName}", it.config.name).replace("{exceptionMessage}", it.message ?: ""))
|
||||||
is CancellationException -> {
|
is CancellationException -> {
|
||||||
//Hide cancelled toast
|
//Hide cancelled toast
|
||||||
}
|
}
|
||||||
else -> UIDialogs.toast("Plugin failed due to:\n${it.message}")
|
else -> UIDialogs.toast(context.getString(R.string.plugin_failed_due_to) + "\n${it.message}")
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-10
@@ -78,7 +78,7 @@ class HomeFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
||||||
_view?.setPreviewsEnabled(previewsEnabled);
|
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.home.previewFeedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
@@ -101,27 +101,29 @@ class HomeFragment : MainFragment() {
|
|||||||
.exception<ScriptCaptchaRequiredException> { }
|
.exception<ScriptCaptchaRequiredException> { }
|
||||||
.exception<ScriptExecutionException> {
|
.exception<ScriptExecutionException> {
|
||||||
Logger.w(ChannelFragment.TAG, "Plugin failure.", it);
|
Logger.w(ChannelFragment.TAG, "Plugin failure.", it);
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_error_pred, "Failed to get Home\nPlugin [${it.config.name}]", it.message, null, 0,
|
UIDialogs.showDialog(context, R.drawable.ic_error_pred, context.getString(R.string.failed_to_get_home_plugin) + " [${it.config.name}]", it.message, null, 0,
|
||||||
UIDialogs.Action("Ignore", {}),
|
UIDialogs.Action(context.getString(R.string.ignore), {}),
|
||||||
UIDialogs.Action("Sources", { fragment.navigate<SourcesFragment>() }, UIDialogs.ActionStyle.PRIMARY)
|
UIDialogs.Action(context.getString(R.string.sources), { fragment.navigate<SourcesFragment>() }, UIDialogs.ActionStyle.PRIMARY)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.exception<ScriptImplementationException> {
|
.exception<ScriptImplementationException> {
|
||||||
Logger.w(TAG, "Plugin failure.", it);
|
Logger.w(TAG, "Plugin failure.", it);
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_error_pred, "Failed to get Home\nPlugin [${it.config.name}]", it.message, null, 0,
|
UIDialogs.showDialog(context, R.drawable.ic_error_pred, context.getString(R.string.failed_to_get_home_plugin) + " [${it.config.name}]", it.message, null, 0,
|
||||||
UIDialogs.Action("Ignore", {}),
|
UIDialogs.Action(context.getString(R.string.ignore), {}),
|
||||||
UIDialogs.Action("Sources", { fragment.navigate<SourcesFragment>() }, UIDialogs.ActionStyle.PRIMARY)
|
UIDialogs.Action(context.getString(R.string.sources), { fragment.navigate<SourcesFragment>() }, UIDialogs.ActionStyle.PRIMARY)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load channel.", it);
|
Logger.w(TAG, "Failed to load channel.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, "Failed to get Home", it, {
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_get_home), it, {
|
||||||
loadResults()
|
loadResults()
|
||||||
}) {
|
}) {
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onShown() {
|
fun onShown() {
|
||||||
@@ -150,7 +152,7 @@ class HomeFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun filterResults(contents: List<IPlatformContent>): List<IPlatformContent> {
|
override fun filterResults(contents: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
return contents.filter { it !is IPlatformVideo || !StateMeta.instance.isVideoHidden(it.url) };
|
return contents.filter { !StateMeta.instance.isVideoHidden(it.url) && !StateMeta.instance.isCreatorHidden(it.author.url) };
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadResults() {
|
private fun loadResults() {
|
||||||
@@ -159,7 +161,7 @@ class HomeFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
private fun loadedResult(pager : IPager<IPlatformContent>) {
|
private fun loadedResult(pager : IPager<IPlatformContent>) {
|
||||||
if (pager is EmptyPager<IPlatformContent>) {
|
if (pager is EmptyPager<IPlatformContent>) {
|
||||||
StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "No home available", "No home page is available, please check if you are connected to the internet and refresh.", AnnouncementType.SESSION);
|
StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), context.getString(R.string.no_home_available), context.getString(R.string.no_home_page_is_available_please_check_if_you_are_connected_to_the_internet_and_refresh), AnnouncementType.SESSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "Got new home pager ${pager}");
|
Logger.i(TAG, "Got new home pager ${pager}");
|
||||||
|
|||||||
+4
-4
@@ -113,7 +113,7 @@ class ImportPlaylistsFragment : MainFragment() {
|
|||||||
}.exceptionWithParameter<Throwable> { ex, para ->
|
}.exceptionWithParameter<Throwable> { ex, para ->
|
||||||
//setLoading(false);
|
//setLoading(false);
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", ex);
|
Logger.w(ChannelFragment.TAG, "Failed to load results.", ex);
|
||||||
UIDialogs.toast(context, "Failed to fetch\n${para}", false)
|
UIDialogs.toast(context, context.getString(R.string.failed_to_fetch) + "\n${para}", false)
|
||||||
//UIDialogs.showDataRetryDialog(layoutInflater, { load(); });
|
//UIDialogs.showDataRetryDialog(layoutInflater, { load(); });
|
||||||
loadNext();
|
loadNext();
|
||||||
};
|
};
|
||||||
@@ -144,14 +144,14 @@ class ImportPlaylistsFragment : MainFragment() {
|
|||||||
|
|
||||||
val tb = _fragment.topBar as ImportTopBarFragment?;
|
val tb = _fragment.topBar as ImportTopBarFragment?;
|
||||||
tb?.let {
|
tb?.let {
|
||||||
it.title = "Import Playlists";
|
it.title = context.getString(R.string.import_playlists);
|
||||||
it.onImport.subscribe(this) {
|
it.onImport.subscribe(this) {
|
||||||
val playlistsToImport = _items.filter { i -> i.selected }.toList();
|
val playlistsToImport = _items.filter { i -> i.selected }.toList();
|
||||||
for (playlistToImport in playlistsToImport) {
|
for (playlistToImport in playlistsToImport) {
|
||||||
StatePlaylists.instance.createOrUpdatePlaylist(playlistToImport.playlist);
|
StatePlaylists.instance.createOrUpdatePlaylist(playlistToImport.playlist);
|
||||||
}
|
}
|
||||||
|
|
||||||
UIDialogs.toast("${playlistsToImport.size} playlists imported.");
|
UIDialogs.toast("${playlistsToImport.size} " + context.getString(R.string.playlists_imported));
|
||||||
_fragment.closeSegment();
|
_fragment.closeSegment();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,7 @@ class ImportPlaylistsFragment : MainFragment() {
|
|||||||
val itemsSelected = _items.count { i -> i.selected };
|
val itemsSelected = _items.count { i -> i.selected };
|
||||||
if (itemsSelected > 0) {
|
if (itemsSelected > 0) {
|
||||||
_textSelectDeselectAll.text = context.getString(R.string.deselect_all);
|
_textSelectDeselectAll.text = context.getString(R.string.deselect_all);
|
||||||
_textCounter.text = "$itemsSelected out of ${_items.size} selected";
|
_textCounter.text = context.getString(R.string.index_out_of_size_selected).replace("{index}", itemsSelected.toString()).replace("{size}", _items.size.toString());
|
||||||
(_fragment.topBar as ImportTopBarFragment?)?.setImportEnabled(true);
|
(_fragment.topBar as ImportTopBarFragment?)?.setImportEnabled(true);
|
||||||
} else {
|
} else {
|
||||||
_textSelectDeselectAll.text = context.getString(R.string.select_all);
|
_textSelectDeselectAll.text = context.getString(R.string.select_all);
|
||||||
|
|||||||
+22
-6
@@ -63,6 +63,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
private var _textSelectDeselectAll: TextView;
|
private var _textSelectDeselectAll: TextView;
|
||||||
private var _textNothingToImport: TextView;
|
private var _textNothingToImport: TextView;
|
||||||
private var _textCounter: TextView;
|
private var _textCounter: TextView;
|
||||||
|
private var _textLoadMore: TextView;
|
||||||
private var _adapterView: AnyAdapterView<SelectableIPlatformChannel, ImportSubscriptionViewHolder>;
|
private var _adapterView: AnyAdapterView<SelectableIPlatformChannel, ImportSubscriptionViewHolder>;
|
||||||
private var _links: List<String> = listOf();
|
private var _links: List<String> = listOf();
|
||||||
private val _items: ArrayList<SelectableIPlatformChannel> = arrayListOf();
|
private val _items: ArrayList<SelectableIPlatformChannel> = arrayListOf();
|
||||||
@@ -79,6 +80,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
_textNothingToImport = findViewById(R.id.nothing_to_import);
|
_textNothingToImport = findViewById(R.id.nothing_to_import);
|
||||||
_textSelectDeselectAll = findViewById(R.id.text_select_deselect_all);
|
_textSelectDeselectAll = findViewById(R.id.text_select_deselect_all);
|
||||||
_textCounter = findViewById(R.id.text_select_counter);
|
_textCounter = findViewById(R.id.text_select_counter);
|
||||||
|
_textLoadMore = findViewById(R.id.text_load_more);
|
||||||
_spinner = findViewById(R.id.channel_loader);
|
_spinner = findViewById(R.id.channel_loader);
|
||||||
|
|
||||||
_adapterView = findViewById<RecyclerView>(R.id.recycler_import).asAny( _items) {
|
_adapterView = findViewById<RecyclerView>(R.id.recycler_import).asAny( _items) {
|
||||||
@@ -116,10 +118,23 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
}.exceptionWithParameter<Throwable> { ex, para ->
|
}.exceptionWithParameter<Throwable> { ex, para ->
|
||||||
//setLoading(false);
|
//setLoading(false);
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", ex);
|
Logger.w(ChannelFragment.TAG, "Failed to load results.", ex);
|
||||||
UIDialogs.toast(context, "Failed to fetch\n${para}", false)
|
UIDialogs.toast(context, context.getString(R.string.failed_to_fetch) + "\n${para}", false)
|
||||||
//UIDialogs.showDataRetryDialog(layoutInflater, { load(); });
|
//UIDialogs.showDataRetryDialog(layoutInflater, { load(); });
|
||||||
loadNext();
|
loadNext();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_textLoadMore.setOnClickListener {
|
||||||
|
if (!_limitToastShown) {
|
||||||
|
return@setOnClickListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
_textLoadMore.visibility = View.GONE;
|
||||||
|
_limitToastShown = false;
|
||||||
|
_counter = 0;
|
||||||
|
load();
|
||||||
|
};
|
||||||
|
|
||||||
|
_textLoadMore.visibility = View.GONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cleanup() {
|
fun cleanup() {
|
||||||
@@ -147,14 +162,14 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
|
|
||||||
val tb = _fragment.topBar as ImportTopBarFragment?;
|
val tb = _fragment.topBar as ImportTopBarFragment?;
|
||||||
tb?.let {
|
tb?.let {
|
||||||
it.title = "Import Subscriptions";
|
it.title = context.getString(R.string.import_subscriptions);
|
||||||
it.onImport.subscribe(this) {
|
it.onImport.subscribe(this) {
|
||||||
val subscriptionsToImport = _items.filter { i -> i.selected }.toList();
|
val subscriptionsToImport = _items.filter { i -> i.selected }.toList();
|
||||||
for (subscriptionToImport in subscriptionsToImport) {
|
for (subscriptionToImport in subscriptionsToImport) {
|
||||||
StateSubscriptions.instance.addSubscription(subscriptionToImport.channel);
|
StateSubscriptions.instance.addSubscription(subscriptionToImport.channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
UIDialogs.toast("${subscriptionsToImport.size} subscriptions imported.");
|
UIDialogs.toast("${subscriptionsToImport.size} " + context.getString(R.string.subscriptions_imported));
|
||||||
_fragment.closeSegment();
|
_fragment.closeSegment();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -165,7 +180,8 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
if (_counter >= MAXIMUM_BATCH_SIZE) {
|
if (_counter >= MAXIMUM_BATCH_SIZE) {
|
||||||
if (!_limitToastShown) {
|
if (!_limitToastShown) {
|
||||||
_limitToastShown = true;
|
_limitToastShown = true;
|
||||||
UIDialogs.toast(context, "Stopped after $MAXIMUM_BATCH_SIZE to avoid rate limit, re-enter to import rest");
|
_textLoadMore.visibility = View.VISIBLE;
|
||||||
|
UIDialogs.toast(context, context.getString(R.string.stopped_after_requestcount_to_avoid_rate_limit_click_load_more_to_load_more).replace("{requestCount}", MAXIMUM_BATCH_SIZE.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -187,7 +203,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
val itemsSelected = _items.count { i -> i.selected };
|
val itemsSelected = _items.count { i -> i.selected };
|
||||||
if (itemsSelected > 0) {
|
if (itemsSelected > 0) {
|
||||||
_textSelectDeselectAll.text = context.getString(R.string.deselect_all);
|
_textSelectDeselectAll.text = context.getString(R.string.deselect_all);
|
||||||
_textCounter.text = "$itemsSelected out of ${_items.size} selected";
|
_textCounter.text = context.getString(R.string.index_out_of_size_selected).replace("{index}", itemsSelected.toString()).replace("{size}", _items.size.toString());
|
||||||
(_fragment.topBar as ImportTopBarFragment?)?.setImportEnabled(true);
|
(_fragment.topBar as ImportTopBarFragment?)?.setImportEnabled(true);
|
||||||
} else {
|
} else {
|
||||||
_textSelectDeselectAll.text = context.getString(R.string.select_all);
|
_textSelectDeselectAll.text = context.getString(R.string.select_all);
|
||||||
@@ -210,7 +226,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "ImportSubscriptionsFragment";
|
val TAG = "ImportSubscriptionsFragment";
|
||||||
private const val MAXIMUM_BATCH_SIZE = 75;
|
private const val MAXIMUM_BATCH_SIZE = 100;
|
||||||
fun newInstance() = ImportSubscriptionsFragment().apply {}
|
fun newInstance() = ImportSubscriptionsFragment().apply {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+10
-10
@@ -80,8 +80,8 @@ class PlaylistFragment : MainFragment() {
|
|||||||
constructor(fragment: PlaylistFragment, inflater: LayoutInflater) : super(inflater) {
|
constructor(fragment: PlaylistFragment, inflater: LayoutInflater) : super(inflater) {
|
||||||
_fragment = fragment;
|
_fragment = fragment;
|
||||||
|
|
||||||
val nameInput = SlideUpMenuTextInput(context, "Name");
|
val nameInput = SlideUpMenuTextInput(context, context.getString(R.string.name));
|
||||||
val editPlaylistOverlay = SlideUpMenuOverlay(context, overlayContainer, "Edit playlist", "Ok", false, nameInput);
|
val editPlaylistOverlay = SlideUpMenuOverlay(context, overlayContainer, context.getString(R.string.edit_playlist), context.getString(R.string.ok), false, nameInput);
|
||||||
|
|
||||||
_buttonDownload.visibility = View.VISIBLE;
|
_buttonDownload.visibility = View.VISIBLE;
|
||||||
editPlaylistOverlay.onOK.subscribe {
|
editPlaylistOverlay.onOK.subscribe {
|
||||||
@@ -113,14 +113,14 @@ class PlaylistFragment : MainFragment() {
|
|||||||
val playlist = _playlist ?: return@setOnShare;
|
val playlist = _playlist ?: return@setOnShare;
|
||||||
val reconstruction = StatePlaylists.instance.playlistStore.getReconstructionString(playlist);
|
val reconstruction = StatePlaylists.instance.playlistStore.getReconstructionString(playlist);
|
||||||
|
|
||||||
UISlideOverlays.showOverlay(overlayContainer, "Playlist [${playlist.name}]", null, {},
|
UISlideOverlays.showOverlay(overlayContainer, context.getString(R.string.playlist) + " [${playlist.name}]", null, {},
|
||||||
SlideUpMenuItem(context, R.drawable.ic_list, "Share as Text", "Share as a list of video urls", 1, {
|
SlideUpMenuItem(context, R.drawable.ic_list, context.getString(R.string.share_as_text), context.getString(R.string.share_as_a_list_of_video_urls), 1, {
|
||||||
_fragment.startActivity(ShareCompat.IntentBuilder(context)
|
_fragment.startActivity(ShareCompat.IntentBuilder(context)
|
||||||
.setType("text/plain")
|
.setType("text/plain")
|
||||||
.setText(reconstruction)
|
.setText(reconstruction)
|
||||||
.intent);
|
.intent);
|
||||||
}),
|
}),
|
||||||
SlideUpMenuItem(context, R.drawable.ic_move_up, "Share as Import", "Share as a import file for Grayjay", 2, {
|
SlideUpMenuItem(context, R.drawable.ic_move_up, context.getString(R.string.share_as_import), context.getString(R.string.share_as_a_import_file_for_grayjay), 2, {
|
||||||
val shareUri = StatePlaylists.instance.createPlaylistShareJsonUri(context, playlist);
|
val shareUri = StatePlaylists.instance.createPlaylistShareJsonUri(context, playlist);
|
||||||
_fragment.startActivity(ShareCompat.IntentBuilder(context)
|
_fragment.startActivity(ShareCompat.IntentBuilder(context)
|
||||||
.setType("application/json")
|
.setType("application/json")
|
||||||
@@ -146,7 +146,7 @@ class PlaylistFragment : MainFragment() {
|
|||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load playlist.", it);
|
Logger.w(TAG, "Failed to load playlist.", it);
|
||||||
val c = context ?: return@exception;
|
val c = context ?: return@exception;
|
||||||
UIDialogs.showGeneralRetryErrorDialog(c, "Failed to load playlist", it, ::fetchPlaylist);
|
UIDialogs.showGeneralRetryErrorDialog(c, context.getString(R.string.failed_to_load_playlist), it, ::fetchPlaylist);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ class PlaylistFragment : MainFragment() {
|
|||||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
|
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
|
||||||
val remotePlaylist = _remotePlaylist;
|
val remotePlaylist = _remotePlaylist;
|
||||||
if (remotePlaylist == null) {
|
if (remotePlaylist == null) {
|
||||||
UIDialogs.toast("Please wait for playlist to finish loading");
|
UIDialogs.toast(context.getString(R.string.please_wait_for_playlist_to_finish_loading));
|
||||||
return@Pair;
|
return@Pair;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ class PlaylistFragment : MainFragment() {
|
|||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
UIDialogs.toast("Playlist copied as local playlist");
|
UIDialogs.toast(context.getString(R.string.playlist_copied_as_local_playlist));
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@@ -284,7 +284,7 @@ class PlaylistFragment : MainFragment() {
|
|||||||
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
||||||
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
||||||
_buttonDownload.setOnClickListener {
|
_buttonDownload.setOnClickListener {
|
||||||
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete the downloaded videos?", {
|
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||||
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -292,7 +292,7 @@ class PlaylistFragment : MainFragment() {
|
|||||||
else if(isDownloaded) {
|
else if(isDownloaded) {
|
||||||
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
||||||
_buttonDownload.setOnClickListener {
|
_buttonDownload.setOnClickListener {
|
||||||
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete the downloaded videos?", {
|
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||||
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-9
@@ -73,16 +73,14 @@ class PlaylistSearchResultsFragment : MainFragment() {
|
|||||||
|
|
||||||
fun onShown(parameter: Any?, isBack: Boolean) {
|
fun onShown(parameter: Any?, isBack: Boolean) {
|
||||||
if(parameter is String) {
|
if(parameter is String) {
|
||||||
if(!isBack) {
|
setQuery(parameter);
|
||||||
setQuery(parameter);
|
|
||||||
|
|
||||||
fragment.topBar?.apply {
|
fragment.topBar?.apply {
|
||||||
if (this is SearchTopBarFragment) {
|
if (this is SearchTopBarFragment) {
|
||||||
setText(parameter);
|
setText(parameter);
|
||||||
onSearch.subscribe(this) {
|
onSearch.subscribe(this) {
|
||||||
setQuery(it);
|
setQuery(it);
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -92,8 +92,8 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
recyclerPlaylists.adapter = _adapterPlaylist;
|
recyclerPlaylists.adapter = _adapterPlaylist;
|
||||||
recyclerPlaylists.layoutManager = LinearLayoutManager(context);
|
recyclerPlaylists.layoutManager = LinearLayoutManager(context);
|
||||||
|
|
||||||
val nameInput = SlideUpMenuTextInput(context, "Name");
|
val nameInput = SlideUpMenuTextInput(context, context.getString(R.string.name));
|
||||||
val addPlaylistOverlay = SlideUpMenuOverlay(context, findViewById<FrameLayout>(R.id.overlay_create_playlist), "Create new playlist", "Ok", false, nameInput);
|
val addPlaylistOverlay = SlideUpMenuOverlay(context, findViewById<FrameLayout>(R.id.overlay_create_playlist), context.getString(R.string.create_new_playlist), context.getString(R.string.ok), false, nameInput);
|
||||||
|
|
||||||
_adapterPlaylist.onClick.subscribe { p -> _fragment.navigate<PlaylistFragment>(p); };
|
_adapterPlaylist.onClick.subscribe { p -> _fragment.navigate<PlaylistFragment>(p); };
|
||||||
_adapterPlaylist.onPlay.subscribe { p ->
|
_adapterPlaylist.onPlay.subscribe { p ->
|
||||||
@@ -130,7 +130,7 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
_appBar = findViewById(R.id.app_bar);
|
_appBar = findViewById(R.id.app_bar);
|
||||||
_layoutWatchlist = findViewById(R.id.layout_watchlist);
|
_layoutWatchlist = findViewById(R.id.layout_watchlist);
|
||||||
|
|
||||||
findViewById<TextView>(R.id.text_view_all).setOnClickListener { _fragment.navigate<WatchLaterFragment>("Watch Later"); };
|
findViewById<TextView>(R.id.text_view_all).setOnClickListener { _fragment.navigate<WatchLaterFragment>(context.getString(R.string.watch_later)); };
|
||||||
StatePlaylists.instance.onWatchLaterChanged.subscribe(this) {
|
StatePlaylists.instance.onWatchLaterChanged.subscribe(this) {
|
||||||
updateWatchLater();
|
updateWatchLater();
|
||||||
};
|
};
|
||||||
|
|||||||
+13
-5
@@ -20,6 +20,7 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||||
@@ -154,13 +155,13 @@ class PostDetailFragment : MainFragment {
|
|||||||
{
|
{
|
||||||
val result = StatePlatform.instance.getContentDetails(it).await();
|
val result = StatePlatform.instance.getContentDetails(it).await();
|
||||||
if(result !is IPlatformPostDetails)
|
if(result !is IPlatformPostDetails)
|
||||||
throw IllegalStateException("Expected media content, found ${result.contentType}");
|
throw IllegalStateException(context.getString(R.string.expected_media_content_found) + " ${result.contentType}");
|
||||||
return@TaskHandler result;
|
return@TaskHandler result;
|
||||||
})
|
})
|
||||||
.success { setPostDetails(it) }
|
.success { setPostDetails(it) }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load post.", it);
|
Logger.w(ChannelFragment.TAG, context.getString(R.string.failed_to_load_post), it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, "Failed to load post", it, ::fetchPost);
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost);
|
||||||
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
||||||
|
|
||||||
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
||||||
@@ -210,6 +211,11 @@ class PostDetailFragment : MainFragment {
|
|||||||
|
|
||||||
_repliesOverlay = findViewById(R.id.replies_overlay);
|
_repliesOverlay = findViewById(R.id.replies_overlay);
|
||||||
|
|
||||||
|
_buttonSubscribe.onSubscribed.subscribe {
|
||||||
|
//TODO: add overlay to layout
|
||||||
|
//UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||||
|
};
|
||||||
|
|
||||||
val layoutTop: LinearLayout = findViewById(R.id.layout_top);
|
val layoutTop: LinearLayout = findViewById(R.id.layout_top);
|
||||||
root.removeView(layoutTop);
|
root.removeView(layoutTop);
|
||||||
_commentsList.setPrependedView(layoutTop);
|
_commentsList.setPrependedView(layoutTop);
|
||||||
@@ -222,7 +228,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
val replyCount = c.replyCount ?: 0;
|
val replyCount = c.replyCount ?: 0;
|
||||||
var metadata = "";
|
var metadata = "";
|
||||||
if (replyCount > 0) {
|
if (replyCount > 0) {
|
||||||
metadata += "$replyCount replies";
|
metadata += "$replyCount " + context.getString(R.string.replies);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c is PolycentricPlatformComment) {
|
if (c is PolycentricPlatformComment) {
|
||||||
@@ -357,7 +363,9 @@ class PostDetailFragment : MainFragment {
|
|||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
Logger.i(TAG, "Started backfill");
|
||||||
args.processHandle.fullyBackfillServers();
|
args.processHandle.fullyBackfillServers();
|
||||||
|
Logger.i(TAG, "Finished backfill");
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to backfill servers", e)
|
Logger.e(TAG, "Failed to backfill servers", e)
|
||||||
}
|
}
|
||||||
@@ -601,7 +609,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
val subscribers = value?.author?.subscribers;
|
val subscribers = value?.author?.subscribers;
|
||||||
if(subscribers != null && subscribers > 0) {
|
if(subscribers != null && subscribers > 0) {
|
||||||
_channelMeta.visibility = View.VISIBLE;
|
_channelMeta.visibility = View.VISIBLE;
|
||||||
_channelMeta.text = if((value.author?.subscribers ?: 0) > 0) value.author.subscribers!!.toHumanNumber() + " subscribers" else "";
|
_channelMeta.text = if((value.author.subscribers ?: 0) > 0) value.author.subscribers!!.toHumanNumber() + " " + context.getString(R.string.subscribers) else "";
|
||||||
} else {
|
} else {
|
||||||
_channelMeta.visibility = View.GONE;
|
_channelMeta.visibility = View.GONE;
|
||||||
_channelMeta.text = "";
|
_channelMeta.text = "";
|
||||||
|
|||||||
+57
-27
@@ -63,6 +63,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
|
|
||||||
private val _sourceHeader: SourceHeaderView;
|
private val _sourceHeader: SourceHeaderView;
|
||||||
private val _sourceButtons: LinearLayout;
|
private val _sourceButtons: LinearLayout;
|
||||||
|
private val _sourceAdvancedButtons: LinearLayout;
|
||||||
private val _layoutLoader: FrameLayout;
|
private val _layoutLoader: FrameLayout;
|
||||||
private val _imageSpinner: ImageView;
|
private val _imageSpinner: ImageView;
|
||||||
|
|
||||||
@@ -82,6 +83,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
this.fragment = fragment;
|
this.fragment = fragment;
|
||||||
_sourceHeader = findViewById(R.id.source_header);
|
_sourceHeader = findViewById(R.id.source_header);
|
||||||
_sourceButtons = findViewById(R.id.source_buttons);
|
_sourceButtons = findViewById(R.id.source_buttons);
|
||||||
|
_sourceAdvancedButtons = findViewById(R.id.advanced_source_buttons);
|
||||||
_settingsAppForm = findViewById(R.id.source_app_setings);
|
_settingsAppForm = findViewById(R.id.source_app_setings);
|
||||||
_settingsForm = findViewById(R.id.source_settings);
|
_settingsForm = findViewById(R.id.source_settings);
|
||||||
_layoutLoader = findViewById(R.id.layout_loader);
|
_layoutLoader = findViewById(R.id.layout_loader);
|
||||||
@@ -107,7 +109,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
StatePlugins.instance.setPluginSettings(id, _settings!!);
|
StatePlugins.instance.setPluginSettings(id, _settings!!);
|
||||||
reloadSource(id);
|
reloadSource(id);
|
||||||
|
|
||||||
UIDialogs.toast("Plugin settings saved", false);
|
UIDialogs.toast(context.getString(R.string.plugin_settings_saved), false);
|
||||||
}
|
}
|
||||||
if(_settingsAppChanged) {
|
if(_settingsAppChanged) {
|
||||||
_settingsAppForm.setObjectValues();
|
_settingsAppForm.setObjectValues();
|
||||||
@@ -144,8 +146,8 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
try {
|
try {
|
||||||
_settings = settingValues;
|
_settings = settingValues;
|
||||||
_settingsForm.fromPluginSettings(
|
_settingsForm.fromPluginSettings(
|
||||||
settings, settingValues, "Plugin settings",
|
settings, settingValues, context.getString(R.string.plugin_settings),
|
||||||
"These settings are defined by the plugin"
|
context.getString(R.string.these_settings_are_defined_by_the_plugin)
|
||||||
);
|
);
|
||||||
_settingsForm.onChanged.clear();
|
_settingsForm.onChanged.clear();
|
||||||
_settingsForm.onChanged.subscribe { field, value ->
|
_settingsForm.onChanged.subscribe { field, value ->
|
||||||
@@ -158,7 +160,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
Logger.e(TAG, "Failed to load source", ex);
|
Logger.e(TAG, "Failed to load source", ex);
|
||||||
UIDialogs.toast("Failed to loast source");
|
UIDialogs.toast(context.getString(R.string.failed_to_load_source));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,8 +206,8 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
val isEnabled = StatePlatform.instance.isClientEnabled(source);
|
val isEnabled = StatePlatform.instance.isClientEnabled(source);
|
||||||
|
|
||||||
groups.add(
|
groups.add(
|
||||||
BigButtonGroup(c, "Update",
|
BigButtonGroup(c, context.getString(R.string.update),
|
||||||
BigButton(c, "Check for updates", "Checks for new versions of the source", R.drawable.ic_update) {
|
BigButton(c, context.getString(R.string.check_for_updates), context.getString(R.string.checks_for_new_versions_of_the_source), R.drawable.ic_update) {
|
||||||
checkForUpdatesSource();
|
checkForUpdatesSource();
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -213,9 +215,16 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
|
|
||||||
if (source.isLoggedIn) {
|
if (source.isLoggedIn) {
|
||||||
groups.add(
|
groups.add(
|
||||||
BigButtonGroup(c, "Authentication",
|
BigButtonGroup(c, context.getString(R.string.authentication),
|
||||||
BigButton(c, "Logout", "Sign out of the platform", R.drawable.ic_logout) {
|
BigButton(c, context.getString(R.string.logout), context.getString(R.string.sign_out_of_the_platform), R.drawable.ic_logout) {
|
||||||
logoutSource();
|
logoutSource();
|
||||||
|
},
|
||||||
|
BigButton(c, "Logout without Clear", "Logout but keep the browser cookies.\nThis allows for quick re-logging.", R.drawable.ic_logout) {
|
||||||
|
logoutSource(false);
|
||||||
|
}.apply {
|
||||||
|
this.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -223,7 +232,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
val migrationButtons = mutableListOf<BigButton>();
|
val migrationButtons = mutableListOf<BigButton>();
|
||||||
if (isEnabled && source.capabilities.hasGetUserSubscriptions) {
|
if (isEnabled && source.capabilities.hasGetUserSubscriptions) {
|
||||||
migrationButtons.add(
|
migrationButtons.add(
|
||||||
BigButton(c, "Import Subscriptions", "Import your subscriptions from this source", R.drawable.ic_subscriptions) {
|
BigButton(c, context.getString(R.string.import_subscriptions), context.getString(R.string.import_your_subscriptions_from_this_source), R.drawable.ic_subscriptions) {
|
||||||
Logger.i(TAG, "Import subscriptions clicked.");
|
Logger.i(TAG, "Import subscriptions clicked.");
|
||||||
importSubscriptionsSource();
|
importSubscriptionsSource();
|
||||||
}
|
}
|
||||||
@@ -231,7 +240,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isEnabled && source.capabilities.hasGetUserPlaylists && source.capabilities.hasGetPlaylist) {
|
if (isEnabled && source.capabilities.hasGetUserPlaylists && source.capabilities.hasGetPlaylist) {
|
||||||
val bigButton = BigButton(c, "Import Playlists", "Import your playlists from this source", R.drawable.ic_playlist) {
|
val bigButton = BigButton(c, context.getString(R.string.import_playlists), context.getString(R.string.import_your_playlists_from_this_source), R.drawable.ic_playlist) {
|
||||||
Logger.i(TAG, "Import playlists clicked.");
|
Logger.i(TAG, "Import playlists clicked.");
|
||||||
importPlaylistsSource();
|
importPlaylistsSource();
|
||||||
};
|
};
|
||||||
@@ -244,13 +253,13 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (migrationButtons.size > 0) {
|
if (migrationButtons.size > 0) {
|
||||||
groups.add(BigButtonGroup(c, "Migration", *migrationButtons.toTypedArray()));
|
groups.add(BigButtonGroup(c, context.getString(R.string.migration), *migrationButtons.toTypedArray()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if(config.authentication != null) {
|
if(config.authentication != null) {
|
||||||
groups.add(
|
groups.add(
|
||||||
BigButtonGroup(c, "Authentication",
|
BigButtonGroup(c, context.getString(R.string.authentication),
|
||||||
BigButton(c, "Login", "Sign into the platform of this source", R.drawable.ic_login) {
|
BigButton(c, context.getString(R.string.login), context.getString(R.string.sign_into_the_platform_of_this_source), R.drawable.ic_login) {
|
||||||
loginSource();
|
loginSource();
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -260,8 +269,8 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
|
|
||||||
val clientIfExists = StatePlugins.instance.getPlugin(config.id);
|
val clientIfExists = StatePlugins.instance.getPlugin(config.id);
|
||||||
groups.add(
|
groups.add(
|
||||||
BigButtonGroup(c, "Management",
|
BigButtonGroup(c, context.getString(R.string.management),
|
||||||
BigButton(c, "Uninstall", "Removes the plugin from the app", R.drawable.ic_block) {
|
BigButton(c, context.getString(R.string.uninstall), context.getString(R.string.removes_the_plugin_from_the_app), R.drawable.ic_block) {
|
||||||
uninstallSource();
|
uninstallSource();
|
||||||
}.withBackground(R.drawable.background_big_button_red).apply {
|
}.withBackground(R.drawable.background_big_button_red).apply {
|
||||||
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
@@ -269,8 +278,8 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
if(clientIfExists?.captchaEncrypted != null)
|
if(clientIfExists?.captchaEncrypted != null)
|
||||||
BigButton(c, "Delete Captcha", "Deletes stored captcha answer for this plugin", R.drawable.ic_block) {
|
BigButton(c, context.getString(R.string.delete_captcha), context.getString(R.string.deletes_stored_captcha_answer_for_this_plugin), R.drawable.ic_block) {
|
||||||
clientIfExists?.updateCaptcha(null);
|
clientIfExists.updateCaptcha(null);
|
||||||
}.apply {
|
}.apply {
|
||||||
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
@@ -283,6 +292,27 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
for (group in groups) {
|
for (group in groups) {
|
||||||
_sourceButtons.addView(group);
|
_sourceButtons.addView(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isEmbedded = StatePlugins.instance.getEmbeddedSources(context).any { it.key == config.id };
|
||||||
|
val advancedButtons = BigButtonGroup(c, "Advanced",
|
||||||
|
BigButton(c, "Edit Code", "Modify the source of this plugin", R.drawable.ic_code) {
|
||||||
|
|
||||||
|
}.apply {
|
||||||
|
this.alpha = 0.5f;
|
||||||
|
},
|
||||||
|
if(isEmbedded) BigButton(c, "Reinstall", "Modify the source of this plugin", R.drawable.ic_refresh) {
|
||||||
|
StatePlugins.instance.updateEmbeddedPlugins(context, listOf(config.id), true);
|
||||||
|
reloadSource(config.id);
|
||||||
|
UIDialogs.toast(context, "Embedded plugin reinstalled, may require refresh");
|
||||||
|
}.apply {
|
||||||
|
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
|
};
|
||||||
|
} else null
|
||||||
|
)
|
||||||
|
|
||||||
|
_sourceAdvancedButtons.removeAllViews();
|
||||||
|
_sourceAdvancedButtons.addView(advancedButtons);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -298,7 +328,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
reloadSource(config.id);
|
reloadSource(config.id);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
private fun logoutSource() {
|
private fun logoutSource(clear: Boolean = true) {
|
||||||
val config = _config ?: return;
|
val config = _config ?: return;
|
||||||
|
|
||||||
StatePlugins.instance.setPluginAuth(config.id, null);
|
StatePlugins.instance.setPluginAuth(config.id, null);
|
||||||
@@ -306,7 +336,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
|
|
||||||
|
|
||||||
//TODO: Maybe add a dialog option..
|
//TODO: Maybe add a dialog option..
|
||||||
if(Settings.instance.plugins.clearCookiesOnLogout) {
|
if(Settings.instance.plugins.clearCookiesOnLogout && clear) {
|
||||||
val cookieManager: CookieManager = CookieManager.getInstance();
|
val cookieManager: CookieManager = CookieManager.getInstance();
|
||||||
cookieManager.removeAllCookies(null);
|
cookieManager.removeAllCookies(null);
|
||||||
}
|
}
|
||||||
@@ -329,7 +359,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
context?.let { UIDialogs.showGeneralErrorDialog(it, "Failed to retrieve playlists.", e) }
|
context?.let { UIDialogs.showGeneralErrorDialog(it, it.getString(R.string.failed_to_retrieve_playlists), e) }
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -354,14 +384,14 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val subscriptions = source.getUserSubscriptions().distinct();
|
val subscriptions = source.getUserSubscriptions().distinct();
|
||||||
Logger.i(TAG, "${subscriptions.size} user subscriptions retrieved.");
|
Logger.i(TAG, context.getString(R.string.subscriptioncount_user_subscriptions_retrieved).replace("{subscriptionCount}", subscriptions.size.toString()));
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
fragment.navigate<ImportSubscriptionsFragment>(subscriptions);
|
fragment.navigate<ImportSubscriptionsFragment>(subscriptions);
|
||||||
}
|
}
|
||||||
} catch(e: Throwable) {
|
} catch(e: Throwable) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
context?.let { UIDialogs.showGeneralErrorDialog(it, "Failed to retrieve subscriptions.", e) }
|
context?.let { UIDialogs.showGeneralErrorDialog(it, context.getString(R.string.failed_to_retrieve_subscriptions), e) }
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -375,7 +405,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
val config = _config ?: return;
|
val config = _config ?: return;
|
||||||
val source = StatePlatform.instance.getClient(config.id);
|
val source = StatePlatform.instance.getClient(config.id);
|
||||||
|
|
||||||
UIDialogs.showConfirmationDialog(context, "Are you sure you want to uninstall ${source.name}", {
|
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_uninstall) + " ${source.name}", {
|
||||||
StatePlugins.instance.deletePlugin(source.id);
|
StatePlugins.instance.deletePlugin(source.id);
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
@@ -386,7 +416,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(context, "Uninstalled ${source.name}");
|
UIDialogs.toast(context, context.getString(R.string.uninstalled) + " ${source.name}");
|
||||||
fragment.closeSegment();
|
fragment.closeSegment();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,7 +435,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
|
|
||||||
if (!response.isOk || response.body == null) {
|
if (!response.isOk || response.body == null) {
|
||||||
Logger.w(TAG, "Failed to check for updates (sourceUrl=${sourceUrl}, response.isOk=${response.isOk}, response.body=${response.body}).");
|
Logger.w(TAG, "Failed to check for updates (sourceUrl=${sourceUrl}, response.isOk=${response.isOk}, response.body=${response.body}).");
|
||||||
withContext(Dispatchers.Main) { UIDialogs.toast("Failed to check for updates"); };
|
withContext(Dispatchers.Main) { UIDialogs.toast(context.getString(R.string.failed_to_check_for_updates)); };
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +445,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
val config = SourcePluginConfig.fromJson(configJson);
|
val config = SourcePluginConfig.fromJson(configJson);
|
||||||
if (config.version <= c.version) {
|
if (config.version <= c.version) {
|
||||||
Logger.i(TAG, "Plugin is up to date.");
|
Logger.i(TAG, "Plugin is up to date.");
|
||||||
withContext(Dispatchers.Main) { UIDialogs.toast("Plugin is fully up to date"); };
|
withContext(Dispatchers.Main) { UIDialogs.toast(context.getString(R.string.plugin_is_fully_up_to_date)); };
|
||||||
return@launch;
|
return@launch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,7 +460,7 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
Logger.i(TAG, "Started add source activity.");
|
Logger.i(TAG, "Started add source activity.");
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to check for updates.", e);
|
Logger.e(TAG, "Failed to check for updates.", e);
|
||||||
withContext(Dispatchers.Main) { UIDialogs.toast("Failed to check for updates"); };
|
withContext(Dispatchers.Main) { UIDialogs.toast(context.getString(R.string.failed_to_check_for_updates)); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-25
@@ -81,12 +81,13 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
fun setPreviewsEnabled(previewsEnabled: Boolean) {
|
||||||
_view?.setPreviewsEnabled(previewsEnabled);
|
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.subscriptions.previewFeedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
|
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
|
||||||
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
||||||
|
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
|
||||||
StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.subscribe(this) { progress, total ->
|
StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.subscribe(this) { progress, total ->
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
@@ -107,16 +108,23 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
initializeToolbarContent();
|
initializeToolbarContent();
|
||||||
|
|
||||||
|
setPreviewsEnabled(Settings.instance.subscriptions.previewFeedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onShown() {
|
fun onShown() {
|
||||||
|
Logger.i(TAG, "SubscriptionsFeedFragment onShown()");
|
||||||
val currentProgress = StateSubscriptions.instance.getGlobalSubscriptionProgress();
|
val currentProgress = StateSubscriptions.instance.getGlobalSubscriptionProgress();
|
||||||
setProgress(currentProgress.first, currentProgress.second);
|
setProgress(currentProgress.first, currentProgress.second);
|
||||||
|
|
||||||
if(recyclerData.loadedFeedStyle != feedStyle ||
|
if(recyclerData.loadedFeedStyle != feedStyle ||
|
||||||
recyclerData.lastLoad.getNowDiffSeconds() > 60 ) {
|
recyclerData.lastLoad.getNowDiffSeconds() > 60 ) {
|
||||||
recyclerData.lastLoad = OffsetDateTime.now();
|
recyclerData.lastLoad = OffsetDateTime.now();
|
||||||
loadResults();
|
|
||||||
|
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5 && Settings.instance.subscriptions.fetchOnTabOpen)
|
||||||
|
loadResults(false);
|
||||||
|
else if(recyclerData.results.size == 0)
|
||||||
|
loadCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
val announcementsView = _announcementsView;
|
val announcementsView = _announcementsView;
|
||||||
@@ -170,12 +178,13 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh ->
|
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh ->
|
||||||
if(!_bypassRateLimit) {
|
if(!_bypassRateLimit) {
|
||||||
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
|
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
|
||||||
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.config.subscriptionRateLimit}" }.joinToString("\n");
|
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
|
||||||
val rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true }
|
val rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.getSubscriptionRateLimit()?.let { rateLimit -> clientCount.value > rateLimit } == true }
|
||||||
Logger.w(TAG, "Refreshing subscriptions with requests:\n" + reqCountStr);
|
Logger.w(TAG, "Trying to refreshing subscriptions with requests:\n" + reqCountStr);
|
||||||
if(rateLimitPlugins.any())
|
if(rateLimitPlugins.any())
|
||||||
throw RateLimitException(rateLimitPlugins.map { it.key.id });
|
throw RateLimitException(rateLimitPlugins.map { it.key.id });
|
||||||
}
|
}
|
||||||
|
_bypassRateLimit = false;
|
||||||
val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh);
|
val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh);
|
||||||
|
|
||||||
val currentExs = StateSubscriptions.instance.globalSubscriptionExceptions;
|
val currentExs = StateSubscriptions.instance.globalSubscriptionExceptions;
|
||||||
@@ -184,7 +193,15 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
|
|
||||||
return@TaskHandler resp;
|
return@TaskHandler resp;
|
||||||
})
|
})
|
||||||
.success { loadedResult(it); }
|
.success {
|
||||||
|
if(!Settings.instance.subscriptions.alwaysReloadFromCache)
|
||||||
|
loadedResult(it);
|
||||||
|
else {
|
||||||
|
finishRefreshLayoutLoader();
|
||||||
|
setLoading(false);
|
||||||
|
loadCache();
|
||||||
|
}
|
||||||
|
} //TODO: Remove
|
||||||
.exception<RateLimitException> {
|
.exception<RateLimitException> {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val subs = StateSubscriptions.instance.getSubscriptions();
|
val subs = StateSubscriptions.instance.getSubscriptions();
|
||||||
@@ -195,11 +212,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_security_pred,
|
UIDialogs.showDialog(context, R.drawable.ic_security_pred,
|
||||||
"Rate Limit Warning", "This is a temporary measure to prevent people from hitting rate limit until we have better support for lots of subscriptions." +
|
context.getString(R.string.rate_limit_warning), context.getString(R.string.this_is_a_temporary_measure_to_prevent_people_from_hitting_rate_limit_until_we_have_better_support_for_lots_of_subscriptions) + context.getString(R.string.you_have_too_many_subscriptions_for_the_following_plugins),
|
||||||
"\n\nYou have too many subscriptions for the following plugins:\n",
|
subsByLimited.map { it.first.config.name + ": " + it.second.size + " " + context.getString(R.string.subscriptions) } .joinToString("\n"), 0, UIDialogs.Action("Refresh Anyway", {
|
||||||
subsByLimited.map { "${it.first.config.name}: ${it.second.size} Subscriptions" } .joinToString("\n"), 0, UIDialogs.Action("Refresh Anyway", {
|
|
||||||
_bypassRateLimit = true;
|
_bypassRateLimit = true;
|
||||||
loadResults();
|
loadResults(true);
|
||||||
}, UIDialogs.ActionStyle.DANGEROUS_TEXT),
|
}, UIDialogs.ActionStyle.DANGEROUS_TEXT),
|
||||||
UIDialogs.Action("OK", {
|
UIDialogs.Action("OK", {
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
@@ -211,7 +227,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
||||||
if(it !is CancellationException)
|
if(it !is CancellationException)
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults(true) });
|
||||||
else {
|
else {
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -226,10 +242,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
|
|
||||||
synchronized(_filterLock) {
|
synchronized(_filterLock) {
|
||||||
_subscriptionBar?.setToggles(
|
_subscriptionBar?.setToggles(
|
||||||
SubscriptionBar.Toggle("Videos", _filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), it); },
|
SubscriptionBar.Toggle(context.getString(R.string.videos), _filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), it); },
|
||||||
SubscriptionBar.Toggle("Posts", _filterSettings.allowContentTypes.contains(ContentType.POST)) { toggleFilterContentType(ContentType.POST, it); },
|
SubscriptionBar.Toggle(context.getString(R.string.posts), _filterSettings.allowContentTypes.contains(ContentType.POST)) { toggleFilterContentType(ContentType.POST, it); },
|
||||||
SubscriptionBar.Toggle("Live", _filterSettings.allowLive) { _filterSettings.allowLive = it; _filterSettings.save(); loadResults(false); },
|
SubscriptionBar.Toggle(context.getString(R.string.live), _filterSettings.allowLive) { _filterSettings.allowLive = it; _filterSettings.save(); loadResults(false); },
|
||||||
SubscriptionBar.Toggle("Planned", _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); }
|
SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +264,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
else null;
|
else null;
|
||||||
_filterSettings.save();
|
_filterSettings.save();
|
||||||
};
|
};
|
||||||
loadResults(false)
|
if(Settings.instance.subscriptions.fetchOnTabOpen) //TODO: Do this different, temporary workaround
|
||||||
|
loadResults(false);
|
||||||
|
else
|
||||||
|
loadCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
@@ -276,30 +295,39 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
loadResults(true);
|
loadResults(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun loadCache() {
|
||||||
|
Logger.i(TAG, "Subscriptions load cache");
|
||||||
|
val cachePager = ChannelContentCache.instance.getSubscriptionCachePager();
|
||||||
|
val results = cachePager.getResults();
|
||||||
|
Logger.i(TAG, "Subscriptions show cache (${results.size})");
|
||||||
|
setTextCentered(if (results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null);
|
||||||
|
setPager(cachePager);
|
||||||
|
}
|
||||||
private fun loadResults(withRefetch: Boolean = false) {
|
private fun loadResults(withRefetch: Boolean = false) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
Logger.i(TAG, "Subscriptions load");
|
Logger.i(TAG, "Subscriptions load");
|
||||||
if(recyclerData.results.size == 0) {
|
if(recyclerData.results.size == 0) {
|
||||||
val cachePager = ChannelContentCache.instance.getSubscriptionCachePager();
|
loadCache();
|
||||||
val results = cachePager.getResults();
|
|
||||||
Logger.i(TAG, "Subscription show cache (${results.size})");
|
|
||||||
setTextCentered(if (results.isEmpty()) "No results found\nSwipe down to refresh" else null);
|
|
||||||
setPager(cachePager);
|
|
||||||
} else {
|
} else {
|
||||||
setTextCentered(null);
|
setTextCentered(null);
|
||||||
}
|
}
|
||||||
_taskGetPager.run(withRefetch);
|
_taskGetPager.run(withRefetch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
|
||||||
|
super.onRestoreCachedData(cachedData);
|
||||||
|
setTextCentered(if (cachedData.results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null);
|
||||||
|
}
|
||||||
private fun loadedResult(pager: IPager<IPlatformContent>) {
|
private fun loadedResult(pager: IPager<IPlatformContent>) {
|
||||||
Logger.i(TAG, "Subscriptions new pager loaded");
|
Logger.i(TAG, "Subscriptions new pager loaded (${pager.getResults().size})");
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setPager(pager);
|
setPager(pager);
|
||||||
setTextCentered(if (pager.getResults().isEmpty()) "No results found\nSwipe down to refresh" else null);
|
setTextCentered(if (pager.getResults().isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to finish loading", e)
|
Logger.e(TAG, "Failed to finish loading", e)
|
||||||
}
|
}
|
||||||
@@ -326,7 +354,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
if (toShow is PluginException)
|
if (toShow is PluginException)
|
||||||
UIDialogs.toast(
|
UIDialogs.toast(
|
||||||
it,
|
it,
|
||||||
"Plugin [${toShow.config.name}] (${channel}) failed:\n${toShow.message}"
|
context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", toShow.config.name).replace("{message}", toShow.message ?: "")
|
||||||
);
|
);
|
||||||
else
|
else
|
||||||
UIDialogs.toast(it, ex.message ?: "");
|
UIDialogs.toast(it, ex.message ?: "");
|
||||||
@@ -340,7 +368,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
.map { it!! }
|
.map { it!! }
|
||||||
.toList();
|
.toList();
|
||||||
for(distinctPluginFail in failedPlugins)
|
for(distinctPluginFail in failedPlugins)
|
||||||
UIDialogs.toast(it, "Plugin [${distinctPluginFail.config.name}] failed:\n${distinctPluginFail.message}");
|
UIDialogs.toast(it, context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: ""));
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to handle exceptions", e)
|
Logger.e(TAG, "Failed to handle exceptions", e)
|
||||||
|
|||||||
+4
-1
@@ -117,7 +117,10 @@ class SuggestionsFragment : MainFragment {
|
|||||||
} else if (_searchType == SearchType.PLAYLIST) {
|
} else if (_searchType == SearchType.PLAYLIST) {
|
||||||
navigate<PlaylistSearchResultsFragment>(it);
|
navigate<PlaylistSearchResultsFragment>(it);
|
||||||
} else {
|
} else {
|
||||||
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl));
|
if(it.isHttpUrl())
|
||||||
|
navigate<VideoDetailFragment>(it);
|
||||||
|
else
|
||||||
|
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+211
-77
@@ -12,7 +12,6 @@ import android.graphics.drawable.BitmapDrawable
|
|||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Browser
|
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
@@ -23,7 +22,6 @@ import android.view.MotionEvent
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.view.WindowManager
|
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -38,6 +36,9 @@ import com.futo.platformplayer.api.media.LiveChatManager
|
|||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
|
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
|
||||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.ChapterType
|
||||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||||
@@ -50,6 +51,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
|||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
|
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.casting.CastConnectionState
|
import com.futo.platformplayer.casting.CastConnectionState
|
||||||
@@ -62,13 +64,16 @@ import com.futo.platformplayer.engine.exceptions.ScriptAgeException
|
|||||||
import com.futo.platformplayer.engine.exceptions.ScriptException
|
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||||
|
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||||
import com.futo.platformplayer.states.*
|
import com.futo.platformplayer.states.*
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringArrayStorage
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
|
import com.futo.platformplayer.views.MonetizationView
|
||||||
import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
|
import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
|
||||||
import com.futo.platformplayer.views.casting.CastView
|
import com.futo.platformplayer.views.casting.CastView
|
||||||
import com.futo.platformplayer.views.comments.AddCommentView
|
import com.futo.platformplayer.views.comments.AddCommentView
|
||||||
@@ -78,6 +83,7 @@ import com.futo.platformplayer.views.overlays.DescriptionOverlay
|
|||||||
import com.futo.platformplayer.views.overlays.LiveChatOverlay
|
import com.futo.platformplayer.views.overlays.LiveChatOverlay
|
||||||
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
||||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||||
|
import com.futo.platformplayer.views.overlays.SupportOverlay
|
||||||
import com.futo.platformplayer.views.overlays.slideup.*
|
import com.futo.platformplayer.views.overlays.slideup.*
|
||||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||||
import com.futo.platformplayer.views.pills.RoundButton
|
import com.futo.platformplayer.views.pills.RoundButton
|
||||||
@@ -94,7 +100,6 @@ import com.google.android.exoplayer2.Format
|
|||||||
import com.google.android.exoplayer2.ui.PlayerControlView
|
import com.google.android.exoplayer2.ui.PlayerControlView
|
||||||
import com.google.android.exoplayer2.ui.TimeBar
|
import com.google.android.exoplayer2.ui.TimeBar
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException
|
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException
|
||||||
import com.google.common.base.Stopwatch
|
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
@@ -172,6 +177,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private val _addCommentView: AddCommentView;
|
private val _addCommentView: AddCommentView;
|
||||||
private val _toggleCommentType: Toggle;
|
private val _toggleCommentType: Toggle;
|
||||||
|
|
||||||
|
private val _layoutSkip: LinearLayout;
|
||||||
|
private val _textSkip: TextView;
|
||||||
private val _textResume: TextView;
|
private val _textResume: TextView;
|
||||||
private val _layoutResume: LinearLayout;
|
private val _layoutResume: LinearLayout;
|
||||||
private var _jobHideResume: Job? = null;
|
private var _jobHideResume: Job? = null;
|
||||||
@@ -189,6 +196,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private val _container_content_replies: RepliesOverlay;
|
private val _container_content_replies: RepliesOverlay;
|
||||||
private val _container_content_description: DescriptionOverlay;
|
private val _container_content_description: DescriptionOverlay;
|
||||||
private val _container_content_liveChat: LiveChatOverlay;
|
private val _container_content_liveChat: LiveChatOverlay;
|
||||||
|
private val _container_content_support: SupportOverlay;
|
||||||
|
|
||||||
private var _container_content_current: View;
|
private var _container_content_current: View;
|
||||||
|
|
||||||
@@ -198,9 +206,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private val _imageDislikeIcon: ImageView;
|
private val _imageDislikeIcon: ImageView;
|
||||||
private val _imageLikeIcon: ImageView;
|
private val _imageLikeIcon: ImageView;
|
||||||
|
|
||||||
private val _buttonSupport: LinearLayout;
|
private val _monetization: MonetizationView;
|
||||||
private val _buttonStore: LinearLayout;
|
|
||||||
private val _layoutMonetization: LinearLayout;
|
|
||||||
|
|
||||||
private val _buttonMore: RoundButton;
|
private val _buttonMore: RoundButton;
|
||||||
|
|
||||||
@@ -290,11 +296,14 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_container_content_replies = findViewById(R.id.videodetail_container_replies);
|
_container_content_replies = findViewById(R.id.videodetail_container_replies);
|
||||||
_container_content_description = findViewById(R.id.videodetail_container_description);
|
_container_content_description = findViewById(R.id.videodetail_container_description);
|
||||||
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
||||||
|
_container_content_support = findViewById(R.id.videodetail_container_support)
|
||||||
|
|
||||||
_textComments = findViewById(R.id.text_comments);
|
_textComments = findViewById(R.id.text_comments);
|
||||||
_addCommentView = findViewById(R.id.add_comment_view);
|
_addCommentView = findViewById(R.id.add_comment_view);
|
||||||
_commentsList = findViewById(R.id.comments_list);
|
_commentsList = findViewById(R.id.comments_list);
|
||||||
|
|
||||||
|
_layoutSkip = findViewById(R.id.layout_skip);
|
||||||
|
_textSkip = findViewById(R.id.text_skip);
|
||||||
_layoutResume = findViewById(R.id.layout_resume);
|
_layoutResume = findViewById(R.id.layout_resume);
|
||||||
_textResume = findViewById(R.id.text_resume);
|
_textResume = findViewById(R.id.text_resume);
|
||||||
_layoutPlayerContainer = findViewById(R.id.layout_player_container);
|
_layoutPlayerContainer = findViewById(R.id.layout_player_container);
|
||||||
@@ -306,28 +315,25 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_imageLikeIcon = findViewById(R.id.image_like_icon);
|
_imageLikeIcon = findViewById(R.id.image_like_icon);
|
||||||
_imageDislikeIcon = findViewById(R.id.image_dislike_icon);
|
_imageDislikeIcon = findViewById(R.id.image_dislike_icon);
|
||||||
|
|
||||||
_buttonSupport = findViewById(R.id.button_support);
|
_monetization = findViewById(R.id.monetization);
|
||||||
_buttonStore = findViewById(R.id.button_store);
|
|
||||||
_layoutMonetization = findViewById(R.id.layout_monetization);
|
|
||||||
|
|
||||||
_layoutMonetization.visibility = View.GONE;
|
|
||||||
_player.attachPlayer();
|
_player.attachPlayer();
|
||||||
|
|
||||||
|
|
||||||
|
_buttonSubscribe.onSubscribed.subscribe {
|
||||||
|
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||||
|
};
|
||||||
|
|
||||||
_container_content_liveChat.onRaidNow.subscribe {
|
_container_content_liveChat.onRaidNow.subscribe {
|
||||||
StatePlayer.instance.clearQueue();
|
StatePlayer.instance.clearQueue();
|
||||||
fragment.navigate<VideoDetailFragment>(it.targetUrl);
|
fragment.navigate<VideoDetailFragment>(it.targetUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonSupport.setOnClickListener {
|
_monetization.onSupportTap.subscribe {
|
||||||
val author = video?.author ?: _searchVideo?.author;
|
_container_content_support.setPolycentricProfile(_polycentricProfile?.profile, false);
|
||||||
author?.let { fragment.navigate<ChannelFragment>(it).selectTab(2); };
|
switchContentView(_container_content_support);
|
||||||
fragment.lifecycleScope.launch {
|
|
||||||
delay(100);
|
|
||||||
fragment.minimizeVideoDetail();
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonStore.setOnClickListener {
|
_monetization.onStoreTap.subscribe {
|
||||||
_polycentricProfile?.profile?.systemState?.store?.let {
|
_polycentricProfile?.profile?.systemState?.store?.let {
|
||||||
try {
|
try {
|
||||||
val uri = Uri.parse(it);
|
val uri = Uri.parse(it);
|
||||||
@@ -340,6 +346,13 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_player.attachPlayer();
|
||||||
|
|
||||||
|
_container_content_liveChat.onRaidNow.subscribe {
|
||||||
|
StatePlayer.instance.clearQueue();
|
||||||
|
fragment.navigate<VideoDetailFragment>(it.targetUrl);
|
||||||
|
};
|
||||||
|
|
||||||
StateApp.instance.preventPictureInPicture.subscribe(this) {
|
StateApp.instance.preventPictureInPicture.subscribe(this) {
|
||||||
Logger.i(TAG, "StateApp.instance.preventPictureInPicture.subscribe preventPictureInPicture = true");
|
Logger.i(TAG, "StateApp.instance.preventPictureInPicture.subscribe preventPictureInPicture = true");
|
||||||
preventPictureInPicture = true;
|
preventPictureInPicture = true;
|
||||||
@@ -397,6 +410,21 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_cast.onSettingsClick.subscribe { showVideoSettings() };
|
_cast.onSettingsClick.subscribe { showVideoSettings() };
|
||||||
_player.onVideoSettings.subscribe { showVideoSettings() };
|
_player.onVideoSettings.subscribe { showVideoSettings() };
|
||||||
_player.onToggleFullScreen.subscribe(::handleFullScreen);
|
_player.onToggleFullScreen.subscribe(::handleFullScreen);
|
||||||
|
_player.onChapterChanged.subscribe { chapter, isScrub ->
|
||||||
|
if(_layoutSkip.visibility == VISIBLE && chapter?.type != ChapterType.SKIPPABLE)
|
||||||
|
_layoutSkip.visibility = GONE;
|
||||||
|
|
||||||
|
if(!isScrub) {
|
||||||
|
if(chapter?.type == ChapterType.SKIPPABLE) {
|
||||||
|
_layoutSkip.visibility = VISIBLE;
|
||||||
|
}
|
||||||
|
else if(chapter?.type == ChapterType.SKIP) {
|
||||||
|
_player.seekTo(chapter.timeEnd.toLong() * 1000);
|
||||||
|
UIDialogs.toast(context, "Skipped chapter [${chapter.name}]", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_cast.onMinimizeClick.subscribe {
|
_cast.onMinimizeClick.subscribe {
|
||||||
_player.setFullScreen(false);
|
_player.setFullScreen(false);
|
||||||
onMinimize.emit();
|
onMinimize.emit();
|
||||||
@@ -410,6 +438,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if (!_isCasting && !_didStop) {
|
if (!_isCasting && !_didStop) {
|
||||||
setLastPositionMilliseconds(position, true);
|
setLastPositionMilliseconds(position, true);
|
||||||
}
|
}
|
||||||
|
updatePlaybackTracking(position);
|
||||||
};
|
};
|
||||||
|
|
||||||
_player.onVideoClicked.subscribe {
|
_player.onVideoClicked.subscribe {
|
||||||
@@ -469,8 +498,14 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
updatePillButtonVisibilities();
|
updatePillButtonVisibilities();
|
||||||
|
|
||||||
StateCasting.instance.onActiveDevicePlayChanged.subscribe(this) {
|
StateCasting.instance.onActiveDevicePlayChanged.subscribe(this) {
|
||||||
if (StateCasting.instance.activeDevice != null) {
|
val activeDevice = StateCasting.instance.activeDevice;
|
||||||
|
if (activeDevice != null) {
|
||||||
handlePlayChanged(it);
|
handlePlayChanged(it);
|
||||||
|
|
||||||
|
val v = video;
|
||||||
|
if (!it && v != null && v.duration - activeDevice.time.toLong() < 2L) {
|
||||||
|
nextVideo();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -514,6 +549,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_container_content_liveChat.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_liveChat.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_queue.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_queue.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
|
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
|
|
||||||
_description_viewMore.setOnClickListener {
|
_description_viewMore.setOnClickListener {
|
||||||
switchContentView(_container_content_description);
|
switchContentView(_container_content_description);
|
||||||
@@ -540,7 +576,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
val replyCount = c.replyCount ?: 0;
|
val replyCount = c.replyCount ?: 0;
|
||||||
var metadata = "";
|
var metadata = "";
|
||||||
if (replyCount > 0) {
|
if (replyCount > 0) {
|
||||||
metadata += "$replyCount replies";
|
metadata += "$replyCount " + context.getString(R.string.replies);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c is PolycentricPlatformComment) {
|
if (c is PolycentricPlatformComment) {
|
||||||
@@ -575,16 +611,78 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
_layoutResume.visibility = View.GONE;
|
_layoutResume.visibility = View.GONE;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_layoutSkip.setOnClickListener {
|
||||||
|
val currentChapter = _player.getCurrentChapter(_player.position);
|
||||||
|
if(currentChapter?.type == ChapterType.SKIPPABLE) {
|
||||||
|
_player.seekTo(currentChapter.timeEnd.toLong() * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val _trackingUpdateTimeLock = Object();
|
||||||
|
val _trackingUpdateInterval = 2500;
|
||||||
|
var _trackingLastUpdateTime = System.currentTimeMillis();
|
||||||
|
var _trackingLastPosition: Long = 0;
|
||||||
|
var _trackingLastVideo: IPlatformVideoDetails? = null;
|
||||||
|
var _trackingTotalWatched: Long = 0;
|
||||||
|
var _trackingDidCountView: Boolean = false;
|
||||||
|
var _trackingLastVideoSubscription: Subscription? = null;
|
||||||
|
fun updatePlaybackTracking(position: Long) {
|
||||||
|
if(!Settings.instance.subscriptions.allowPlaytimeTracking)
|
||||||
|
return;
|
||||||
|
val now = System.currentTimeMillis();
|
||||||
|
val shouldUpdate = synchronized(_trackingUpdateTimeLock) {
|
||||||
|
val doUpdate = (now - _trackingLastUpdateTime) > _trackingUpdateInterval;
|
||||||
|
if(doUpdate)
|
||||||
|
_trackingLastUpdateTime = now;
|
||||||
|
return@synchronized doUpdate;
|
||||||
|
}
|
||||||
|
if(shouldUpdate) {
|
||||||
|
val currentVideo = video;
|
||||||
|
val delta = position - _trackingLastPosition;
|
||||||
|
_trackingLastPosition = position;
|
||||||
|
|
||||||
|
if(currentVideo != null && currentVideo == _trackingLastVideo) {
|
||||||
|
if(delta > 500 && delta < _trackingUpdateInterval * 1.5) {
|
||||||
|
_trackingLastVideoSubscription?.let {
|
||||||
|
Logger.i(TAG, "Subscription [${it.channel.name}] watch time delta [${delta}]" +
|
||||||
|
"(${"%.2f".format((_trackingTotalWatched / 1000) / currentVideo.duration.toDouble().coerceAtLeast(1.0))})");
|
||||||
|
it.updatePlayback(currentVideo, (delta / 1000).toInt());
|
||||||
|
_trackingTotalWatched += delta;
|
||||||
|
if(!_trackingDidCountView && currentVideo.duration > 0) {
|
||||||
|
val percentage = (_trackingTotalWatched / 1000) / currentVideo.duration.toDouble();
|
||||||
|
if(percentage > 0.4) {
|
||||||
|
Logger.i(TAG, "Subscription [${it.channel.name}] new view");
|
||||||
|
_trackingDidCountView = true;
|
||||||
|
it.addPlaybackView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.saveAsync();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(_trackingLastVideo == null && currentVideo == null)
|
||||||
|
return;
|
||||||
|
_trackingLastVideo = currentVideo;
|
||||||
|
_trackingTotalWatched = 0;
|
||||||
|
if(currentVideo?.author?.url != null)
|
||||||
|
_trackingLastVideoSubscription = StateSubscriptions.instance.getSubscription(currentVideo.author.url);
|
||||||
|
else
|
||||||
|
_trackingLastVideoSubscription = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateMoreButtons() {
|
fun updateMoreButtons() {
|
||||||
val buttons = listOf(RoundButton(context, R.drawable.ic_add, "Add", TAG_ADD) {
|
val buttons = listOf(RoundButton(context, R.drawable.ic_add, context.getString(R.string.add), TAG_ADD) {
|
||||||
(video ?: _searchVideo)?.let {
|
(video ?: _searchVideo)?.let {
|
||||||
_slideUpOverlay = UISlideOverlays.showAddToOverlay(it, _overlayContainer);
|
_slideUpOverlay = UISlideOverlays.showAddToOverlay(it, _overlayContainer);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
if(video?.isLive ?: false)
|
if(video?.isLive ?: false)
|
||||||
RoundButton(context, R.drawable.ic_chat, "Live Chat", TAG_LIVECHAT) {
|
RoundButton(context, R.drawable.ic_chat, context.getString(R.string.live_chat), TAG_LIVECHAT) {
|
||||||
video?.let {
|
video?.let {
|
||||||
try {
|
try {
|
||||||
loadLiveChat(it);
|
loadLiveChat(it);
|
||||||
@@ -594,7 +692,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
RoundButton(context, R.drawable.ic_screen_share, "Background", TAG_BACKGROUND) {
|
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.background), TAG_BACKGROUND) {
|
||||||
if(!allowBackground) {
|
if(!allowBackground) {
|
||||||
_player.switchToAudioMode();
|
_player.switchToAudioMode();
|
||||||
allowBackground = true;
|
allowBackground = true;
|
||||||
@@ -606,31 +704,31 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
it.text.text = resources.getString(R.string.background);
|
it.text.text = resources.getString(R.string.background);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
RoundButton(context, R.drawable.ic_download, "Download", TAG_DOWNLOAD) {
|
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
||||||
video?.let {
|
video?.let {
|
||||||
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
RoundButton(context, R.drawable.ic_share, "Share", TAG_SHARE) {
|
RoundButton(context, R.drawable.ic_share, context.getString(R.string.share), TAG_SHARE) {
|
||||||
video?.let {
|
video?.let {
|
||||||
Logger.i(TAG, "Share preventPictureInPicture = true");
|
Logger.i(TAG, "Share preventPictureInPicture = true");
|
||||||
preventPictureInPicture = true;
|
preventPictureInPicture = true;
|
||||||
shareVideo();
|
shareVideo();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
RoundButton(context, R.drawable.ic_screen_share, "Overlay", TAG_OVERLAY) {
|
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.overlay), TAG_OVERLAY) {
|
||||||
this.startPictureInPicture();
|
this.startPictureInPicture();
|
||||||
fragment.forcePictureInPicture();
|
fragment.forcePictureInPicture();
|
||||||
//PiPActivity.startPiP(context);
|
//PiPActivity.startPiP(context);
|
||||||
},
|
},
|
||||||
RoundButton(context, R.drawable.ic_export, "Page", TAG_OPEN) {
|
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) {
|
||||||
video?.let {
|
video?.let {
|
||||||
val url = video?.shareUrl ?: _searchVideo?.shareUrl ?: _url;
|
val url = video?.shareUrl ?: _searchVideo?.shareUrl ?: _url;
|
||||||
fragment.navigate<BrowserFragment>(url);
|
fragment.navigate<BrowserFragment>(url);
|
||||||
fragment.minimizeVideoDetail();
|
fragment.minimizeVideoDetail();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
RoundButton(context, R.drawable.ic_refresh, "Reload", "Reload") {
|
RoundButton(context, R.drawable.ic_refresh, context.getString(R.string.reload), "Reload") {
|
||||||
reloadVideo();
|
reloadVideo();
|
||||||
}).filterNotNull();
|
}).filterNotNull();
|
||||||
if(!_buttonPinStore.getAllValues().any())
|
if(!_buttonPinStore.getAllValues().any())
|
||||||
@@ -719,7 +817,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
when (Settings.instance.playback.backgroundPlay) {
|
when (Settings.instance.playback.backgroundPlay) {
|
||||||
0 -> handlePause();
|
0 -> handlePause();
|
||||||
1 -> {
|
1 -> {
|
||||||
if(!(video?.isLive ?: false))
|
if(!(video?.isLive ?: false) && Settings.instance.playback.backgroundSwitchToAudio)
|
||||||
_player.switchToAudioMode();
|
_player.switchToAudioMode();
|
||||||
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
||||||
}
|
}
|
||||||
@@ -754,6 +852,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_container_content_replies.cleanup();
|
_container_content_replies.cleanup();
|
||||||
_container_content_queue.cleanup();
|
_container_content_queue.cleanup();
|
||||||
_container_content_description.cleanup();
|
_container_content_description.cleanup();
|
||||||
|
_container_content_support.cleanup();
|
||||||
StateCasting.instance.onActiveDevicePlayChanged.remove(this);
|
StateCasting.instance.onActiveDevicePlayChanged.remove(this);
|
||||||
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||||
@@ -856,7 +955,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
val subTitleSegments : ArrayList<String> = ArrayList();
|
val subTitleSegments : ArrayList<String> = ArrayList();
|
||||||
if(video.viewCount > 0)
|
if(video.viewCount > 0)
|
||||||
subTitleSegments.add("${video.viewCount.toHumanNumber()} ${if(video.isLive) "watching now" else "views"}");
|
subTitleSegments.add("${video.viewCount.toHumanNumber()} ${if(video.isLive) context.getString(R.string.watching_now) else context.getString(R.string.views)}");
|
||||||
if(video.datetime != null) {
|
if(video.datetime != null) {
|
||||||
val diff = video.datetime?.getNowDiffSeconds() ?: 0;
|
val diff = video.datetime?.getNowDiffSeconds() ?: 0;
|
||||||
val ago = video.datetime?.toHumanNowDiffString(true)
|
val ago = video.datetime?.toHumanNowDiffString(true)
|
||||||
@@ -870,10 +969,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_commentsList.clear();
|
_commentsList.clear();
|
||||||
_platform.setPlatformFromClientID(video.id.pluginId);
|
_platform.setPlatformFromClientID(video.id.pluginId);
|
||||||
_subTitle.text = subTitleSegments.joinToString(" • ");
|
_subTitle.text = subTitleSegments.joinToString(" • ");
|
||||||
_channelName.text = video.author.name;
|
|
||||||
_playWhenReady = true;
|
_playWhenReady = true;
|
||||||
if(video.author.subscribers != null) {
|
if(video.author.subscribers != null) {
|
||||||
_channelMeta.text = if((video.author.subscribers ?: 0) > 0) video.author.subscribers!!.toHumanNumber() + " subscribers" else "";
|
_channelMeta.text = if((video.author.subscribers ?: 0) > 0) video.author.subscribers!!.toHumanNumber() + " " + context.getString(R.string.subscribers)else "";
|
||||||
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0);
|
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0);
|
||||||
} else {
|
} else {
|
||||||
_channelMeta.text = "";
|
_channelMeta.text = "";
|
||||||
@@ -897,6 +995,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
} else {
|
} else {
|
||||||
setPolycentricProfile(null, animate = false);
|
setPolycentricProfile(null, animate = false);
|
||||||
_taskLoadPolycentricProfile.run(video.author.id);
|
_taskLoadPolycentricProfile.run(video.author.id);
|
||||||
|
_channelName.text = video.author.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
_player.clear();
|
_player.clear();
|
||||||
@@ -932,7 +1031,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now())
|
if(videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now())
|
||||||
UIDialogs.toast(context, "Planned in ${videoDetail.datetime?.toHumanNowDiffString(true)}")
|
UIDialogs.toast(context, context.getString(R.string.planned_in) + " ${videoDetail.datetime?.toHumanNowDiffString(true)}")
|
||||||
|
|
||||||
if (!videoDetail.isLive) {
|
if (!videoDetail.isLive) {
|
||||||
_player.setPlaybackRate(Settings.instance.playback.getDefaultPlaybackSpeed());
|
_player.setPlaybackRate(Settings.instance.playback.getDefaultPlaybackSpeed());
|
||||||
@@ -948,6 +1047,19 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if(video is JSVideoDetails) {
|
if(video is JSVideoDetails) {
|
||||||
val me = this;
|
val me = this;
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
//TODO: Implement video.getContentChapters()
|
||||||
|
val chapters = null ?: StatePlatform.instance.getContentChapters(video.url);
|
||||||
|
_player.setChapters(chapters);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to get chapters", ex);
|
||||||
|
_player.setChapters(null);
|
||||||
|
|
||||||
|
/*withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(context, "Failed to get chapters\n" + ex.message);
|
||||||
|
}*/
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
val stopwatch = com.futo.platformplayer.debug.Stopwatch()
|
val stopwatch = com.futo.platformplayer.debug.Stopwatch()
|
||||||
var tracker = video.getPlaybackTracker()
|
var tracker = video.getPlaybackTracker()
|
||||||
@@ -964,7 +1076,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.showGeneralErrorDialog(context, "Failed to get Playback Tracker", ex);
|
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_get_playback_tracker), ex);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -982,13 +1094,18 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_title.text = video.name;
|
_title.text = video.name;
|
||||||
_channelName.text = video.author.name;
|
_channelName.text = video.author.name;
|
||||||
if(video.author.subscribers != null) {
|
if(video.author.subscribers != null) {
|
||||||
_channelMeta.text = if((video.author.subscribers ?: 0) > 0) video.author.subscribers!!.toHumanNumber() + " subscribers" else "";
|
_channelMeta.text = if((video.author.subscribers ?: 0) > 0) video.author.subscribers!!.toHumanNumber() + " " + context.getString(R.string.subscribers) else "";
|
||||||
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0);
|
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0);
|
||||||
} else {
|
} else {
|
||||||
_channelMeta.text = "";
|
_channelMeta.text = "";
|
||||||
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_2).toInt(), 0, 0);
|
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_2).toInt(), 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
video.author.let {
|
||||||
|
if(it is PlatformAuthorMembershipLink && !it.membershipUrl.isNullOrEmpty())
|
||||||
|
_monetization.setPlatformMembership(video.id.pluginId, it.membershipUrl);
|
||||||
|
}
|
||||||
|
|
||||||
_minimize_title.text = video.name;
|
_minimize_title.text = video.name;
|
||||||
_minimize_meta.text = video.author.name;
|
_minimize_meta.text = video.author.name;
|
||||||
|
|
||||||
@@ -1007,7 +1124,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_platform.setPlatformFromClientID(video.id.pluginId);
|
_platform.setPlatformFromClientID(video.id.pluginId);
|
||||||
val subTitleSegments : ArrayList<String> = ArrayList();
|
val subTitleSegments : ArrayList<String> = ArrayList();
|
||||||
if(video.viewCount > 0)
|
if(video.viewCount > 0)
|
||||||
subTitleSegments.add("${video.viewCount.toHumanNumber()} ${if(video.isLive) "watching now" else "views"}");
|
subTitleSegments.add("${video.viewCount.toHumanNumber()} ${if(video.isLive) context.getString(R.string.watching_now) else context.getString(R.string.views)}");
|
||||||
if(video.datetime != null) {
|
if(video.datetime != null) {
|
||||||
val diff = video.datetime?.getNowDiffSeconds() ?: 0;
|
val diff = video.datetime?.getNowDiffSeconds() ?: 0;
|
||||||
val ago = video.datetime?.toHumanNowDiffString(true)
|
val ago = video.datetime?.toHumanNowDiffString(true)
|
||||||
@@ -1053,7 +1170,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
Logger.i(TAG, "Started backfill");
|
||||||
args.processHandle.fullyBackfillServers();
|
args.processHandle.fullyBackfillServers();
|
||||||
|
Logger.i(TAG, "Finished backfill");
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to backfill servers", e)
|
Logger.e(TAG, "Failed to backfill servers", e)
|
||||||
}
|
}
|
||||||
@@ -1166,7 +1285,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
livePager = StatePlatform.instance.getLiveEvents(video.url);
|
livePager = StatePlatform.instance.getLiveEvents(video.url);
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
livePager = null;
|
livePager = null;
|
||||||
UIDialogs.toast("Exception retrieving live events:\n" + ex.message);
|
UIDialogs.toast(context.getString(R.string.exception_retrieving_live_events) + "\n" + ex.message);
|
||||||
Logger.e(TAG, "Failed to retrieve live chat events", ex);
|
Logger.e(TAG, "Failed to retrieve live chat events", ex);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -1177,7 +1296,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
liveChatWindow = null;
|
liveChatWindow = null;
|
||||||
UIDialogs.toast("Exception retrieving live chat window:\n" + ex.message);
|
UIDialogs.toast(context.getString(R.string.exception_retrieving_live_chat_window) + "\n" + ex.message);
|
||||||
Logger.e(TAG, "Failed to retrieve live chat window", ex);
|
Logger.e(TAG, "Failed to retrieve live chat window", ex);
|
||||||
}
|
}
|
||||||
val liveChat = livePager?.let {
|
val liveChat = livePager?.let {
|
||||||
@@ -1199,7 +1318,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
Logger.e(TAG, "Failed to load live chat", ex);
|
Logger.e(TAG, "Failed to load live chat", ex);
|
||||||
|
|
||||||
UIDialogs.toast("Live chat failed to load\n" + ex.message);
|
UIDialogs.toast(context.getString(R.string.live_chat_failed_to_load) + "\n" + ex.message);
|
||||||
//_liveChat?.handleEvents(listOf(LiveEventComment("SYSTEM", null, "Failed to load live chat:\n" + ex.message, "#FF0000")))
|
//_liveChat?.handleEvents(listOf(LiveEventComment("SYSTEM", null, "Failed to load live chat:\n" + ex.message, "#FF0000")))
|
||||||
/*
|
/*
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
@@ -1254,9 +1373,13 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_lastVideoSource = videoSource;
|
_lastVideoSource = videoSource;
|
||||||
_lastAudioSource = audioSource;
|
_lastAudioSource = audioSource;
|
||||||
}
|
}
|
||||||
|
catch(ex: UnsupportedCastException) {
|
||||||
|
Logger.e(TAG, "Failed to load cast media", ex);
|
||||||
|
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.unsupported_cast_format), ex);
|
||||||
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
Logger.e(TAG, "Failed to load media", ex);
|
Logger.e(TAG, "Failed to load media", ex);
|
||||||
UIDialogs.showGeneralErrorDialog(context, "Failed to load media", ex);
|
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_load_media), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long) {
|
private fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long) {
|
||||||
@@ -1274,7 +1397,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
Logger.i(TAG, "onSourceChanged(videoSource=$videoSource, audioSource=$audioSource, resume=$resume)")
|
Logger.i(TAG, "onSourceChanged(videoSource=$videoSource, audioSource=$audioSource, resume=$resume)")
|
||||||
|
|
||||||
if((videoSource == null || videoSource is LocalVideoSource) && (audioSource == null || audioSource is LocalAudioSource))
|
if((videoSource == null || videoSource is LocalVideoSource) && (audioSource == null || audioSource is LocalAudioSource))
|
||||||
UIDialogs.toast(context, "Offline Playback", false);
|
UIDialogs.toast(context, context.getString(R.string.offline_playback), false);
|
||||||
//If LiveStream, set to end
|
//If LiveStream, set to end
|
||||||
if(videoSource is IDashManifestSource || videoSource is IHLSManifestSource) {
|
if(videoSource is IDashManifestSource || videoSource is IHLSManifestSource) {
|
||||||
if (video?.isLive == true) {
|
if (video?.isLive == true) {
|
||||||
@@ -1315,12 +1438,12 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_didTriggerDatasourceError = true;
|
_didTriggerDatasourceError = true;
|
||||||
|
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_error_pred,
|
UIDialogs.showDialog(context, R.drawable.ic_error_pred,
|
||||||
"Media Error",
|
context.getString(R.string.media_error),
|
||||||
"The media source encountered an unauthorized error.\nThis might be solved by a plugin reload.\nWould you like to reload?\n(Experimental)",
|
context.getString(R.string.the_media_source_encountered_an_unauthorized_error_this_might_be_solved_by_a_plugin_reload_would_you_like_to_reload_experimental),
|
||||||
null,
|
null,
|
||||||
0,
|
0,
|
||||||
UIDialogs.Action("No", { _didTriggerDatasourceError = false }),
|
UIDialogs.Action(context.getString(R.string.no), { _didTriggerDatasourceError = false }),
|
||||||
UIDialogs.Action("Yes", {
|
UIDialogs.Action(context.getString(R.string.yes), {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
StatePlatform.instance.reloadClient(context, config.id);
|
StatePlatform.instance.reloadClient(context, config.id);
|
||||||
@@ -1418,8 +1541,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
?.filter { it.container == bestAudioContainer }
|
?.filter { it.container == bestAudioContainer }
|
||||||
?.toList() ?: listOf();
|
?.toList() ?: listOf();
|
||||||
|
|
||||||
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, "Quality", null, true,
|
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
|
||||||
if (!_isCasting) SlideUpMenuTitle(this.context).apply { setTitle("Playback Rate") } else null,
|
R.string.quality), null, true,
|
||||||
|
if (!_isCasting) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate)) } else null,
|
||||||
if (!_isCasting) SlideUpMenuButtonList(this.context).apply {
|
if (!_isCasting) SlideUpMenuButtonList(this.context).apply {
|
||||||
setButtons(listOf("0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "2.25"), _player.getPlaybackRate().toString());
|
setButtons(listOf("0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "2.25"), _player.getPlaybackRate().toString());
|
||||||
onClick.subscribe { v ->
|
onClick.subscribe { v ->
|
||||||
@@ -1433,7 +1557,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
} else null,
|
} else null,
|
||||||
|
|
||||||
if(localVideoSources?.isNotEmpty() == true)
|
if(localVideoSources?.isNotEmpty() == true)
|
||||||
SlideUpMenuGroup(this.context, "Offline Video", "video",
|
SlideUpMenuGroup(this.context, context.getString(R.string.offline_video), "video",
|
||||||
*localVideoSources.stream()
|
*localVideoSources.stream()
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(this.context, R.drawable.ic_movie, it!!.name, "${it.width}x${it.height}", it,
|
SlideUpMenuItem(this.context, R.drawable.ic_movie, it!!.name, "${it.width}x${it.height}", it,
|
||||||
@@ -1441,7 +1565,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}.toList().toTypedArray())
|
}.toList().toTypedArray())
|
||||||
else null,
|
else null,
|
||||||
if(localAudioSource?.isNotEmpty() == true)
|
if(localAudioSource?.isNotEmpty() == true)
|
||||||
SlideUpMenuGroup(this.context, "Offline Audio", "audio",
|
SlideUpMenuGroup(this.context, context.getString(R.string.offline_audio), "audio",
|
||||||
*localAudioSource.stream()
|
*localAudioSource.stream()
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), it,
|
SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), it,
|
||||||
@@ -1449,7 +1573,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}.toList().toTypedArray())
|
}.toList().toTypedArray())
|
||||||
else null,
|
else null,
|
||||||
if(localSubtitleSources?.isNotEmpty() == true)
|
if(localSubtitleSources?.isNotEmpty() == true)
|
||||||
SlideUpMenuGroup(this.context, "Offline Subtitles", "subtitles",
|
SlideUpMenuGroup(this.context, context.getString(R.string.offline_subtitles), "subtitles",
|
||||||
*localSubtitleSources
|
*localSubtitleSources
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", it,
|
SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", it,
|
||||||
@@ -1457,7 +1581,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}.toList().toTypedArray())
|
}.toList().toTypedArray())
|
||||||
else null,
|
else null,
|
||||||
if(liveStreamVideoFormats?.isEmpty() == false)
|
if(liveStreamVideoFormats?.isEmpty() == false)
|
||||||
SlideUpMenuGroup(this.context, "Stream Video", "video",
|
SlideUpMenuGroup(this.context, context.getString(R.string.stream_video), "video",
|
||||||
*liveStreamVideoFormats.stream()
|
*liveStreamVideoFormats.stream()
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(this.context, R.drawable.ic_movie, it?.label ?: it.containerMimeType ?: it.bitrate.toString(), "${it.width}x${it.height}", it,
|
SlideUpMenuItem(this.context, R.drawable.ic_movie, it?.label ?: it.containerMimeType ?: it.bitrate.toString(), "${it.width}x${it.height}", it,
|
||||||
@@ -1465,7 +1589,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}.toList().toTypedArray())
|
}.toList().toTypedArray())
|
||||||
else null,
|
else null,
|
||||||
if(liveStreamAudioFormats?.isEmpty() == false)
|
if(liveStreamAudioFormats?.isEmpty() == false)
|
||||||
SlideUpMenuGroup(this.context, "Stream Audio", "audio",
|
SlideUpMenuGroup(this.context, context.getString(R.string.stream_audio), "audio",
|
||||||
*liveStreamAudioFormats.stream()
|
*liveStreamAudioFormats.stream()
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(this.context, R.drawable.ic_music, "${it?.label ?: it.containerMimeType} ${it.bitrate}", "", it,
|
SlideUpMenuItem(this.context, R.drawable.ic_music, "${it?.label ?: it.containerMimeType} ${it.bitrate}", "", it,
|
||||||
@@ -1474,7 +1598,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
else null,
|
else null,
|
||||||
|
|
||||||
if(bestVideoSources.isNotEmpty())
|
if(bestVideoSources.isNotEmpty())
|
||||||
SlideUpMenuGroup(this.context, "Video", "video",
|
SlideUpMenuGroup(this.context, context.getString(R.string.video), "video",
|
||||||
*bestVideoSources.stream()
|
*bestVideoSources.stream()
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(this.context, R.drawable.ic_movie, it!!.name, if (it.width > 0 && it.height > 0) "${it.width}x${it.height}" else "", it,
|
SlideUpMenuItem(this.context, R.drawable.ic_movie, it!!.name, if (it.width > 0 && it.height > 0) "${it.width}x${it.height}" else "", it,
|
||||||
@@ -1482,7 +1606,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}.toList().toTypedArray())
|
}.toList().toTypedArray())
|
||||||
else null,
|
else null,
|
||||||
if(bestAudioSources.isNotEmpty())
|
if(bestAudioSources.isNotEmpty())
|
||||||
SlideUpMenuGroup(this.context, "Audio", "audio",
|
SlideUpMenuGroup(this.context, context.getString(R.string.audio), "audio",
|
||||||
*bestAudioSources.stream()
|
*bestAudioSources.stream()
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), it,
|
SlideUpMenuItem(this.context, R.drawable.ic_music, it.name, it.bitrate.toHumanBitrate(), it,
|
||||||
@@ -1490,7 +1614,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}.toList().toTypedArray())
|
}.toList().toTypedArray())
|
||||||
else null,
|
else null,
|
||||||
if(video?.subtitles?.isNotEmpty() ?: false && video != null)
|
if(video?.subtitles?.isNotEmpty() ?: false && video != null)
|
||||||
SlideUpMenuGroup(this.context, "Subtitles", "subtitles",
|
SlideUpMenuGroup(this.context, context.getString(R.string.subtitles), "subtitles",
|
||||||
*video.subtitles
|
*video.subtitles
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", it,
|
SlideUpMenuItem(this.context, R.drawable.ic_edit, it.name, "", it,
|
||||||
@@ -1613,12 +1737,13 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private fun handleUnavailableVideo(msg: String? = null) {
|
private fun handleUnavailableVideo(msg: String? = null) {
|
||||||
if (!nextVideo()) {
|
if (!nextVideo()) {
|
||||||
if(video?.datetime == null || video?.datetime!! < OffsetDateTime.now().minusHours(1))
|
if(video?.datetime == null || video?.datetime!! < OffsetDateTime.now().minusHours(1))
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_lock, "Unavailable video", msg ?: "This video is unavailable.", null, 0,
|
UIDialogs.showDialog(context, R.drawable.ic_lock, context.getString(R.string.unavailable_video), msg ?: context.getString(R.string.this_video_is_unavailable), null, 0,
|
||||||
UIDialogs.Action("Back", {
|
UIDialogs.Action(context.getString(R.string.back), {
|
||||||
this@VideoDetailView.onClose.emit();
|
this@VideoDetailView.onClose.emit();
|
||||||
}, UIDialogs.ActionStyle.PRIMARY));
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_UNAVAILABLE", "Unavailable video", "There was an unavailable video in your queue [${video?.name}] by [${video?.author?.name}].", AnnouncementType.SESSION)
|
StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_UNAVAILABLE", context.getString(R.string.unavailable_video), context.getString(R.string.there_was_an_unavailable_video_in_your_queue_videoname_by_authorname).replace("{videoName}", video?.name ?: "").replace("{authorName}", video?.author?.name ?: ""), AnnouncementType.SESSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
video?.let { StatePlatform.instance.clearContentDetailCache(it.url) };
|
video?.let { StatePlatform.instance.clearContentDetailCache(it.url) };
|
||||||
@@ -1696,6 +1821,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_isCasting = isCasting;
|
_isCasting = isCasting;
|
||||||
|
|
||||||
if(isCasting) {
|
if(isCasting) {
|
||||||
|
setFullscreen(false);
|
||||||
_player.stop();
|
_player.stop();
|
||||||
_player.hideControls(false);
|
_player.hideControls(false);
|
||||||
_cast.visibility = View.VISIBLE;
|
_cast.visibility = View.VISIBLE;
|
||||||
@@ -1870,9 +1996,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_player.getGlobalVisibleRect(r);
|
_player.getGlobalVisibleRect(r);
|
||||||
r.right = r.right - _player.paddingEnd;
|
r.right = r.right - _player.paddingEnd;
|
||||||
val playpauseAction = if(_player.playing)
|
val playpauseAction = if(_player.playing)
|
||||||
RemoteAction(Icon.createWithResource(context, R.drawable.ic_pause_notif), "Pause", "Pauses the video", MediaControlReceiver.getPauseIntent(context, 5));
|
RemoteAction(Icon.createWithResource(context, R.drawable.ic_pause_notif), context.getString(R.string.pause), context.getString(R.string.pauses_the_video), MediaControlReceiver.getPauseIntent(context, 5));
|
||||||
else
|
else
|
||||||
RemoteAction(Icon.createWithResource(context, R.drawable.ic_play_notif), "Play", "Resumes the video", MediaControlReceiver.getPlayIntent(context, 6));
|
RemoteAction(Icon.createWithResource(context, R.drawable.ic_play_notif), context.getString(R.string.play), context.getString(R.string.resumes_the_video), MediaControlReceiver.getPlayIntent(context, 6));
|
||||||
|
|
||||||
return PictureInPictureParams.Builder()
|
return PictureInPictureParams.Builder()
|
||||||
.setAspectRatio(Rational(videoSourceWidth, videoSourceHeight))
|
.setAspectRatio(Rational(videoSourceWidth, videoSourceHeight))
|
||||||
@@ -1971,14 +2097,19 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||||
_polycentricProfile = cachedPolycentricProfile;
|
_polycentricProfile = cachedPolycentricProfile;
|
||||||
|
|
||||||
if (cachedPolycentricProfile?.profile == null) {
|
val dp_35 = 35.dp(context.resources)
|
||||||
_layoutMonetization.visibility = View.GONE;
|
val profile = cachedPolycentricProfile?.profile;
|
||||||
_creatorThumbnail.setHarborAvailable(false, animate);
|
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
|
||||||
return;
|
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||||
|
|
||||||
|
if (avatar != null) {
|
||||||
|
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||||
|
} else {
|
||||||
|
_creatorThumbnail.setThumbnail(video?.author?.thumbnail, animate);
|
||||||
|
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
_layoutMonetization.visibility = View.VISIBLE;
|
_monetization.setPolycentricProfile(cachedPolycentricProfile, animate);
|
||||||
_creatorThumbnail.setHarborAvailable(true, animate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
|
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
|
||||||
@@ -2051,7 +2182,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_NOSOURCES", "Video without source", "There was a in your queue [${video?.name}] by [${video?.author?.name}] without the required source being enabled, playback was skipped.", AnnouncementType.SESSION)
|
StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_NOSOURCES", context.getString(R.string.video_without_source), context.getString(R.string.there_was_a_in_your_queue_videoname_by_authorname_without_the_required_source_being_enabled_playback_was_skipped).replace("{videoName}", video?.name ?: "").replace("{authorName}", video?.author?.name ?: ""), AnnouncementType.SESSION)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.exception<ContentNotAvailableYetException> {
|
.exception<ContentNotAvailableYetException> {
|
||||||
@@ -2070,9 +2201,10 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
Logger.w(TAG, "exception<ScriptImplementationException>", it)
|
Logger.w(TAG, "exception<ScriptImplementationException>", it)
|
||||||
|
|
||||||
if (!nextVideo()) {
|
if (!nextVideo()) {
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, "Failed to load video (ScriptImplementationException)", it, ::fetchVideo);
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, ::fetchVideo);
|
||||||
} else {
|
} else {
|
||||||
StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_INVALIDVIDEO", "Invalid video", "There was an invalid video in your queue [${video?.name}] by [${video?.author?.name}], playback was skipped.", AnnouncementType.SESSION)
|
StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_INVALIDVIDEO", context.getString(R.string.invalid_video), context.getString(
|
||||||
|
R.string.there_was_an_invalid_video_in_your_queue_videoname_by_authorname_playback_was_skipped).replace("{videoName}", video?.name ?: "").replace("{authorName}", video?.author?.name ?: ""), AnnouncementType.SESSION)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.exception<ScriptAgeException> {
|
.exception<ScriptAgeException> {
|
||||||
@@ -2087,7 +2219,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
this@VideoDetailView.onClose.emit();
|
this@VideoDetailView.onClose.emit();
|
||||||
}, UIDialogs.ActionStyle.PRIMARY));
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
} else {
|
} else {
|
||||||
StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_AGERESTRICT", "Age restricted video", "There was an age restricted video in your queue [${video?.name}] by [${video?.author?.name}], this video was not accessible and playback was skipped.", AnnouncementType.SESSION)
|
StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_AGERESTRICT", context.getString(R.string.age_restricted_video),
|
||||||
|
context.getString(R.string.there_was_an_age_restricted_video_in_your_queue_videoname_by_authorname_this_video_was_not_accessible_and_playback_was_skipped).replace("{videoName}", video?.name ?: "").replace("{authorName}", video?.author?.name ?: ""),
|
||||||
|
AnnouncementType.SESSION)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.exception<ScriptUnavailableException> {
|
.exception<ScriptUnavailableException> {
|
||||||
@@ -2103,7 +2237,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_retryJob = null;
|
_retryJob = null;
|
||||||
_liveTryJob?.cancel();
|
_liveTryJob?.cancel();
|
||||||
_liveTryJob = null;
|
_liveTryJob = null;
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, "Failed to load video (ScriptException)", it, ::fetchVideo);
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, ::fetchVideo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
@@ -2115,7 +2249,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_retryJob = null;
|
_retryJob = null;
|
||||||
_liveTryJob?.cancel();
|
_liveTryJob?.cancel();
|
||||||
_liveTryJob = null;
|
_liveTryJob = null;
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, "Failed to load video", it, ::fetchVideo);
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo);
|
||||||
}
|
}
|
||||||
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
|
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
|
||||||
|
|
||||||
@@ -2156,7 +2290,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
val toWait = _liveStreamCheckInterval.toList().sortedBy { abs(diffSeconds - it.first) }.firstOrNull()?.second?.toLong() ?: return;
|
val toWait = _liveStreamCheckInterval.toList().sortedBy { abs(diffSeconds - it.first) }.firstOrNull()?.second?.toLong() ?: return;
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main){
|
fragment.lifecycleScope.launch(Dispatchers.Main){
|
||||||
UIDialogs.toast(context, "Not yet available, retrying in ${toWait}s");
|
UIDialogs.toast(context, context.getString(R.string.not_yet_available_retrying_in_time_s).replace("{time}", toWait.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
_liveTryJob?.cancel();
|
_liveTryJob?.cancel();
|
||||||
@@ -2170,7 +2304,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if(videoDetail.datetime != null && videoDetail.live == null && !videoDetail.video.videoSources.any()) {
|
if(videoDetail.datetime != null && videoDetail.live == null && !videoDetail.video.videoSources.any()) {
|
||||||
if(videoDetail.datetime!! > OffsetDateTime.now())
|
if(videoDetail.datetime!! > OffsetDateTime.now())
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(context, "Planned in ${videoDetail.datetime?.toHumanNowDiffString(true)}");
|
UIDialogs.toast(context, context.getString(R.string.planned_in) + " ${videoDetail.datetime?.toHumanNowDiffString(true)}");
|
||||||
}
|
}
|
||||||
startLiveTry(liveTryVideo);
|
startLiveTry(liveTryVideo);
|
||||||
}
|
}
|
||||||
@@ -2183,7 +2317,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
Logger.e(TAG, "Failed to live try fetch video.", ex);
|
Logger.e(TAG, "Failed to live try fetch video.", ex);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(context, "Failed to retry for live stream");
|
UIDialogs.toast(context, context.getString(R.string.failed_to_retry_for_live_stream));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-6
@@ -57,10 +57,8 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
buttonPlayAll.setOnClickListener { onPlayAllClick(); };
|
buttonPlayAll.setOnClickListener { onPlayAllClick(); };
|
||||||
buttonShuffle.setOnClickListener { onShuffleClick(); };
|
buttonShuffle.setOnClickListener { onShuffleClick(); };
|
||||||
|
|
||||||
if (canEdit())
|
_buttonEdit.setOnClickListener { onEditClick(); };
|
||||||
_buttonEdit.setOnClickListener { onEditClick(); };
|
setButtonDownloadVisible(canEdit());
|
||||||
else
|
|
||||||
_buttonEdit.visibility = View.GONE;
|
|
||||||
|
|
||||||
videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged);
|
videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged);
|
||||||
videoListEditorView.onVideoRemoved.subscribe(::onVideoRemoved);
|
videoListEditorView.onVideoRemoved.subscribe(::onVideoRemoved);
|
||||||
@@ -93,7 +91,7 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected fun setVideoCount(videoCount: Int = -1) {
|
protected fun setVideoCount(videoCount: Int = -1) {
|
||||||
_textMetadata.text = if (videoCount == -1) "" else "${videoCount} videos";
|
_textMetadata.text = if (videoCount == -1) "" else "${videoCount} " + context.getString(R.string.videos);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun setVideos(videos: List<IPlatformVideo>?, canEdit: Boolean) {
|
protected fun setVideos(videos: List<IPlatformVideo>?, canEdit: Boolean) {
|
||||||
@@ -107,7 +105,7 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
.into(it);
|
.into(it);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
_textMetadata.text = "0 videos";
|
_textMetadata.text = "0 " + context.getString(R.string.videos);
|
||||||
if(_imagePlaylistThumbnail != null) {
|
if(_imagePlaylistThumbnail != null) {
|
||||||
Glide.with(_imagePlaylistThumbnail)
|
Glide.with(_imagePlaylistThumbnail)
|
||||||
.load(R.drawable.placeholder_video_thumbnail)
|
.load(R.drawable.placeholder_video_thumbnail)
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ class GeneralTopBarFragment : TopFragment() {
|
|||||||
val view = inflater.inflate(R.layout.fragment_overview_top_bar, container, false);
|
val view = inflater.inflate(R.layout.fragment_overview_top_bar, container, false);
|
||||||
|
|
||||||
view.findViewById<ImageView>(R.id.app_icon).setOnClickListener {
|
view.findViewById<ImageView>(R.id.app_icon).setOnClickListener {
|
||||||
UIDialogs.toast("This app is in development. Please submit bug reports and understand that many features are incomplete.", true);
|
UIDialogs.toast(getString(R.string.this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete), true);
|
||||||
};
|
};
|
||||||
|
|
||||||
val buttonSearch: ImageButton = view.findViewById(R.id.button_search);
|
val buttonSearch: ImageButton = view.findViewById(R.id.button_search);
|
||||||
|
|||||||
+1
-1
@@ -194,7 +194,7 @@ class SearchTopBarFragment : TopFragment() {
|
|||||||
if (editSearch != null) {
|
if (editSearch != null) {
|
||||||
val text = editSearch.text.toString();
|
val text = editSearch.text.toString();
|
||||||
if (text.length < 3) {
|
if (text.length < 3) {
|
||||||
UIDialogs.toast("Please use at least 3 characters");
|
UIDialogs.toast(getString(R.string.please_use_at_least_3_characters));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.futo.platformplayer.images;
|
package com.futo.platformplayer.images;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.bumptech.glide.Priority;
|
import com.bumptech.glide.Priority;
|
||||||
|
|||||||
@@ -64,12 +64,11 @@ class Logging {
|
|||||||
|
|
||||||
val client = OkHttpClient()
|
val client = OkHttpClient()
|
||||||
val response: Response = client.newCall(request).execute()
|
val response: Response = client.newCall(request).execute()
|
||||||
if (response.isSuccessful) {
|
return if (response.isSuccessful) {
|
||||||
val body = response.body?.string();
|
response.body?.string();
|
||||||
return if (body != null) Json.decodeFromString<String>(body) else null;
|
|
||||||
} else {
|
} else {
|
||||||
Logger.e("Failed to submit log.") { "Failed to submit logs (${response.code}): ${response.body?.string()}" };
|
Logger.e("Failed to submit log.") { "Failed to submit logs (${response.code}): ${response.body?.string()}" };
|
||||||
return null;
|
null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
package com.futo.platformplayer.models
|
package com.futo.platformplayer.models
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
import com.futo.platformplayer.getNowDiffDays
|
import com.futo.platformplayer.getNowDiffDays
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class Subscription {
|
class Subscription {
|
||||||
var channel: SerializedChannel;
|
var channel: SerializedChannel;
|
||||||
|
|
||||||
|
//Settings
|
||||||
|
var doNotifications: Boolean = false;
|
||||||
|
var doFetchLive: Boolean = false;
|
||||||
|
var doFetchStreams: Boolean = true;
|
||||||
|
var doFetchVideos: Boolean = true;
|
||||||
|
var doFetchPosts: Boolean = false;
|
||||||
|
|
||||||
//Last found content
|
//Last found content
|
||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
var lastVideo : OffsetDateTime = OffsetDateTime.MAX;
|
var lastVideo : OffsetDateTime = OffsetDateTime.MAX;
|
||||||
@@ -32,24 +43,104 @@ class Subscription {
|
|||||||
|
|
||||||
//Last video interval
|
//Last video interval
|
||||||
var uploadInterval : Int = 0;
|
var uploadInterval : Int = 0;
|
||||||
|
var uploadStreamInterval : Int = 0;
|
||||||
var uploadPostInterval : Int = 0;
|
var uploadPostInterval : Int = 0;
|
||||||
|
|
||||||
|
var playbackSeconds: Int = 0;
|
||||||
|
var playbackViews: Int = 0;
|
||||||
|
|
||||||
|
|
||||||
constructor(channel : SerializedChannel) {
|
constructor(channel : SerializedChannel) {
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shouldFetchStreams() = lastLiveStream.getNowDiffDays() < 7;
|
fun shouldFetchVideos() = doFetchVideos &&
|
||||||
fun shouldFetchLiveStreams() = lastLiveStream.getNowDiffDays() < 14;
|
(lastVideo.getNowDiffDays() < 30 || lastVideoUpdate.getNowDiffDays() >= 1) &&
|
||||||
fun shouldFetchPosts() = lastPost.getNowDiffDays() < 2;
|
(lastVideo.getNowDiffDays() < 180 || lastVideoUpdate.getNowDiffDays() >= 3);
|
||||||
|
fun shouldFetchStreams() = doFetchStreams && (lastLiveStream.getNowDiffDays() < 7);
|
||||||
|
fun shouldFetchLiveStreams() = doFetchLive && (lastLiveStream.getNowDiffDays() < 14);
|
||||||
|
fun shouldFetchPosts() = doFetchPosts && (lastPost.getNowDiffDays() < 5);
|
||||||
|
|
||||||
fun getClient() = StatePlatform.instance.getChannelClientOrNull(channel.url);
|
fun getClient() = StatePlatform.instance.getChannelClientOrNull(channel.url);
|
||||||
|
|
||||||
|
fun save() {
|
||||||
|
StateSubscriptions.instance.saveSubscription(this);
|
||||||
|
}
|
||||||
|
fun saveAsync() {
|
||||||
|
StateSubscriptions.instance.saveSubscription(this);
|
||||||
|
}
|
||||||
|
|
||||||
fun updateChannel(channel: IPlatformChannel) {
|
fun updateChannel(channel: IPlatformChannel) {
|
||||||
this.channel = SerializedChannel.fromChannel(channel);
|
this.channel = SerializedChannel.fromChannel(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateVideoStatus(allVideos: List<IPlatformContent>? = null, liveStreams: List<IPlatformContent>? = null) {
|
fun updatePlayback(content: IPlatformContentDetails, seconds: Int) {
|
||||||
|
playbackSeconds += seconds;
|
||||||
|
}
|
||||||
|
fun addPlaybackView() {
|
||||||
|
playbackViews += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSubscriptionState(type: String, initialPage: List<IPlatformContent>) {
|
||||||
|
val interval: Int;
|
||||||
|
val mostRecent: OffsetDateTime?;
|
||||||
|
if(!initialPage.isEmpty()) {
|
||||||
|
val newestVideoDays = initialPage[0].datetime?.getNowDiffDays()?.toInt() ?: 0;
|
||||||
|
val diffs = mutableListOf<Int>()
|
||||||
|
for(i in (initialPage.size - 1) downTo 1) {
|
||||||
|
val currentVideoDays = initialPage[i].datetime?.getNowDiffDays();
|
||||||
|
val nextVideoDays = initialPage[i - 1].datetime?.getNowDiffDays();
|
||||||
|
|
||||||
|
if(currentVideoDays != null && nextVideoDays != null) {
|
||||||
|
val diff = nextVideoDays - currentVideoDays;
|
||||||
|
diffs.add(diff.toInt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val averageDiff = if(diffs.size > 0)
|
||||||
|
newestVideoDays.coerceAtLeast(diffs.average().toInt())
|
||||||
|
else
|
||||||
|
newestVideoDays;
|
||||||
|
interval = averageDiff.coerceAtLeast(1);
|
||||||
|
mostRecent = initialPage[0].datetime;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
interval = 5;
|
||||||
|
mostRecent = null;
|
||||||
|
Logger.i("Subscription", "Subscription [${channel.name}]:${type} no results found");
|
||||||
|
}
|
||||||
|
when(type) {
|
||||||
|
ResultCapabilities.TYPE_VIDEOS -> {
|
||||||
|
uploadInterval = interval;
|
||||||
|
if(mostRecent != null)
|
||||||
|
lastVideo = mostRecent;
|
||||||
|
else if(lastVideo.year > 3000)
|
||||||
|
lastVideo = OffsetDateTime.MIN;
|
||||||
|
lastVideoUpdate = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
ResultCapabilities.TYPE_MIXED -> {
|
||||||
|
uploadInterval = interval;
|
||||||
|
if(mostRecent != null)
|
||||||
|
lastVideo = mostRecent;
|
||||||
|
else if(lastVideo.year > 3000)
|
||||||
|
lastVideo = OffsetDateTime.MIN;
|
||||||
|
lastVideoUpdate = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
ResultCapabilities.TYPE_STREAMS -> {
|
||||||
|
uploadStreamInterval = interval;
|
||||||
|
if(mostRecent != null)
|
||||||
|
lastLiveStream = mostRecent;
|
||||||
|
else if(lastLiveStream.year > 3000)
|
||||||
|
lastLiveStream = OffsetDateTime.MIN;
|
||||||
|
lastStreamUpdate = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
ResultCapabilities.TYPE_POSTS -> {
|
||||||
|
uploadPostInterval = interval;
|
||||||
|
if(mostRecent != null)
|
||||||
|
lastPost = mostRecent;
|
||||||
|
else if(lastPost.year > 3000)
|
||||||
|
lastPost = OffsetDateTime.MIN;
|
||||||
|
lastPostUpdate = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,5 +11,6 @@ data class Telemetry(
|
|||||||
val isUnstableBuild: Boolean,
|
val isUnstableBuild: Boolean,
|
||||||
val brand: String,
|
val brand: String,
|
||||||
val manufacturer: String,
|
val manufacturer: String,
|
||||||
val model: String
|
val model: String,
|
||||||
|
val sdkVersion: Int
|
||||||
) { }
|
) { }
|
||||||
@@ -8,6 +8,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|||||||
import com.futo.platformplayer.getNowDiffSeconds
|
import com.futo.platformplayer.getNowDiffSeconds
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
|
import com.futo.platformplayer.resolveChannelUrls
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
import com.futo.platformplayer.stores.CachedPolycentricProfileStorage
|
import com.futo.platformplayer.stores.CachedPolycentricProfileStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
@@ -37,7 +38,13 @@ class PolycentricCache {
|
|||||||
ContentType.AVATAR.value,
|
ContentType.AVATAR.value,
|
||||||
ContentType.USERNAME.value,
|
ContentType.USERNAME.value,
|
||||||
ContentType.DESCRIPTION.value,
|
ContentType.DESCRIPTION.value,
|
||||||
ContentType.STORE.value
|
ContentType.STORE.value,
|
||||||
|
ContentType.SERVER.value,
|
||||||
|
ContentType.STORE_DATA.value,
|
||||||
|
ContentType.PROMOTION_BANNER.value,
|
||||||
|
ContentType.PROMOTION.value,
|
||||||
|
ContentType.MEMBERSHIP_URLS.value,
|
||||||
|
ContentType.DONATION_DESTINATIONS.value
|
||||||
)
|
)
|
||||||
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
||||||
|
|
||||||
@@ -88,8 +95,9 @@ class PolycentricCache {
|
|||||||
|
|
||||||
if (result.profile != null) {
|
if (result.profile != null) {
|
||||||
for (claim in result.profile.ownedClaims) {
|
for (claim in result.profile.ownedClaims) {
|
||||||
val url = claim.claim.resolveChannelUrl() ?: continue;
|
val urls = claim.claim.resolveChannelUrls();
|
||||||
_profileUrlCache.map[url] = result;
|
for (url in urls)
|
||||||
|
_profileUrlCache.map[url] = result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.media.AudioFocusRequest
|
|||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.AudioManager.OnAudioFocusChangeListener
|
import android.media.AudioManager.OnAudioFocusChangeListener
|
||||||
import android.media.MediaMetadata
|
import android.media.MediaMetadata
|
||||||
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
@@ -278,7 +279,13 @@ class MediaPlaybackService : Service() {
|
|||||||
|
|
||||||
Logger.i(TAG, "Updating notification bitmap=${if (bitmap != null) "yes" else "no."} channelId=${channel.id} icon=${icon} video=${video?.name ?: ""} playWhenReady=${playWhenReady} session.sessionToken=${session.sessionToken}");
|
Logger.i(TAG, "Updating notification bitmap=${if (bitmap != null) "yes" else "no."} channelId=${channel.id} icon=${icon} video=${video?.name ?: ""} playWhenReady=${playWhenReady} session.sessionToken=${session.sessionToken}");
|
||||||
|
|
||||||
startForeground(MEDIA_NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
// For API 29 and above
|
||||||
|
startForeground(MEDIA_NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
||||||
|
} else {
|
||||||
|
// For API 28 and below
|
||||||
|
startForeground(MEDIA_NOTIF_ID, notif);
|
||||||
|
}
|
||||||
|
|
||||||
_notif_last_bitmap = bitmap;
|
_notif_last_bitmap = bitmap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
import com.futo.platformplayer.background.BackgroundWorker
|
import com.futo.platformplayer.background.BackgroundWorker
|
||||||
|
import com.futo.platformplayer.cache.ChannelContentCache
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
@@ -54,6 +55,8 @@ import java.io.File
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
import kotlin.time.measureTime
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* This class contains global context for unconventional cases where obtaining context is hard.
|
* This class contains global context for unconventional cases where obtaining context is hard.
|
||||||
@@ -236,6 +239,25 @@ class StateApp {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun requestFileReadAccess(activity: IWithResultLauncher, path: Uri?, handle: (DocumentFile?)->Unit) {
|
||||||
|
if(activity is Context) {
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||||
|
if(path != null)
|
||||||
|
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path);
|
||||||
|
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
.or(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
|
||||||
|
activity.launchForResult(intent, 98) {
|
||||||
|
if(it.resultCode == Activity.RESULT_OK) {
|
||||||
|
val uri = it.data?.data;
|
||||||
|
if(uri != null)
|
||||||
|
handle(DocumentFile.fromSingleUri(activity, uri));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
UIDialogs.showDialogOk(context, R.drawable.ic_security_pred, "No access granted");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
fun requestDirectoryAccess(activity: IWithResultLauncher, name: String, purpose: String? = null, path: Uri?, handle: (Uri?)->Unit)
|
fun requestDirectoryAccess(activity: IWithResultLauncher, name: String, purpose: String? = null, path: Uri?, handle: (Uri?)->Unit)
|
||||||
{
|
{
|
||||||
if(activity is Context)
|
if(activity is Context)
|
||||||
@@ -332,6 +354,7 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun mainAppStarting(context: Context) {
|
fun mainAppStarting(context: Context) {
|
||||||
|
Logger.i(TAG, "MainApp Starting");
|
||||||
initializeFiles(true);
|
initializeFiles(true);
|
||||||
|
|
||||||
val logFile = File(context.filesDir, "log.txt");
|
val logFile = File(context.filesDir, "log.txt");
|
||||||
@@ -350,14 +373,18 @@ class StateApp {
|
|||||||
|
|
||||||
Logger.setLogConsumers(listOf(AndroidLogConsumer()));
|
Logger.setLogConsumers(listOf(AndroidLogConsumer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePayment.instance.initialize();
|
StatePayment.instance.initialize();
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Starting: Initializing [Polycentric]");
|
||||||
StatePolycentric.instance.load(context);
|
StatePolycentric.instance.load(context);
|
||||||
|
Logger.i(TAG, "MainApp Starting: Initializing [Saved]");
|
||||||
StateSaved.instance.load();
|
StateSaved.instance.load();
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Starting: Initializing [Connectivity]");
|
||||||
displayMetrics = context.resources.displayMetrics;
|
displayMetrics = context.resources.displayMetrics;
|
||||||
ensureConnectivityManager(context);
|
ensureConnectivityManager(context);
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Starting: Initializing [Telemetry]");
|
||||||
if (!BuildConfig.DEBUG) {
|
if (!BuildConfig.DEBUG) {
|
||||||
StateTelemetry.instance.initialize();
|
StateTelemetry.instance.initialize();
|
||||||
StateTelemetry.instance.upload();
|
StateTelemetry.instance.upload();
|
||||||
@@ -378,17 +405,32 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun mainAppStarted(context: Context) {
|
fun mainAppStarted(context: Context) {
|
||||||
Logger.i(TAG, "App started");
|
Logger.i(TAG, "MainApp Started");
|
||||||
|
|
||||||
|
//Start loading cache
|
||||||
|
instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
Logger.i(TAG, "MainApp Started: Initializing [ChannelContentCache]");
|
||||||
|
val time = measureTimeMillis {
|
||||||
|
ChannelContentCache.instance;
|
||||||
|
}
|
||||||
|
Logger.i(TAG, "ChannelContentCache initialized in ${time}ms");
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to load announcements.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
StateAnnouncement.instance.registerAnnouncement("fa4647d3-36fa-4c8c-832d-85b00fc72dca", "Disclaimer", "This is an early alpha build of the application, expect bugs and unfinished features.", AnnouncementType.DELETABLE, OffsetDateTime.now())
|
StateAnnouncement.instance.registerAnnouncement("fa4647d3-36fa-4c8c-832d-85b00fc72dca", "Disclaimer", "This is an early alpha build of the application, expect bugs and unfinished features.", AnnouncementType.DELETABLE, OffsetDateTime.now())
|
||||||
|
|
||||||
if(SettingsDev.instance.developerMode && SettingsDev.instance.devServerSettings.devServerOnBoot)
|
if(SettingsDev.instance.developerMode && SettingsDev.instance.devServerSettings.devServerOnBoot)
|
||||||
StateDeveloper.instance.runServer();
|
StateDeveloper.instance.runServer();
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Started: Check [Migration (Subscriptions)]");
|
||||||
if(StateSubscriptions.instance.shouldMigrate())
|
if(StateSubscriptions.instance.shouldMigrate())
|
||||||
StateSubscriptions.instance.tryMigrateIfNecessary();
|
StateSubscriptions.instance.tryMigrateIfNecessary();
|
||||||
|
|
||||||
if(Settings.instance.downloads.shouldDownload()) {
|
if(Settings.instance.downloads.shouldDownload()) {
|
||||||
|
Logger.i(TAG, "MainApp Started: Check [Downloads]");
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
|
|
||||||
StateDownloads.instance.getDownloadPlaylists();
|
StateDownloads.instance.getDownloadPlaylists();
|
||||||
@@ -396,8 +438,10 @@ class StateApp {
|
|||||||
DownloadService.getOrCreateService(context);
|
DownloadService.getOrCreateService(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Started: Check [Exports]");
|
||||||
StateDownloads.instance.checkForExportTodos();
|
StateDownloads.instance.checkForExportTodos();
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate]");
|
||||||
val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled();
|
val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled();
|
||||||
val shouldDownload = Settings.instance.autoUpdate.shouldDownload();
|
val shouldDownload = Settings.instance.autoUpdate.shouldDownload();
|
||||||
val backgroundDownload = Settings.instance.autoUpdate.backgroundDownload == 1;
|
val backgroundDownload = Settings.instance.autoUpdate.backgroundDownload == 1;
|
||||||
@@ -421,6 +465,7 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Started: Initialize [Noisy]");
|
||||||
_receiverBecomingNoisy?.let {
|
_receiverBecomingNoisy?.let {
|
||||||
_receiverBecomingNoisy = null;
|
_receiverBecomingNoisy = null;
|
||||||
context.unregisterReceiver(it);
|
context.unregisterReceiver(it);
|
||||||
@@ -429,6 +474,7 @@ class StateApp {
|
|||||||
context.registerReceiver(_receiverBecomingNoisy, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
context.registerReceiver(_receiverBecomingNoisy, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
||||||
|
|
||||||
//Migration
|
//Migration
|
||||||
|
Logger.i(TAG, "MainApp Started: Check [Migrations]");
|
||||||
migrateStores(context, listOf(
|
migrateStores(context, listOf(
|
||||||
StateSubscriptions.instance.toMigrateCheck(),
|
StateSubscriptions.instance.toMigrateCheck(),
|
||||||
StatePlaylists.instance.toMigrateCheck()
|
StatePlaylists.instance.toMigrateCheck()
|
||||||
@@ -436,22 +482,26 @@ class StateApp {
|
|||||||
|
|
||||||
if(Settings.instance.subscriptions.fetchOnAppBoot) {
|
if(Settings.instance.subscriptions.fetchOnAppBoot) {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
|
Logger.i(TAG, "MainApp Started: Fetch [Subscriptions]");
|
||||||
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
|
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
|
||||||
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.config.subscriptionRateLimit}" }.joinToString("\n");
|
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
|
||||||
val isRateLimitReached = !subRequestCounts.any { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true };
|
val isRateLimitReached = !subRequestCounts.any { clientCount -> clientCount.key.getSubscriptionRateLimit()?.let { rateLimit -> clientCount.value > rateLimit } == true };
|
||||||
if (isRateLimitReached) {
|
if (isRateLimitReached) {
|
||||||
Logger.w(TAG, "Subscriptions request on boot, request counts:\n${reqCountStr}");
|
Logger.w(TAG, "Subscriptions request on boot, request counts:\n${reqCountStr}");
|
||||||
delay(5000);
|
delay(5000);
|
||||||
StateSubscriptions.instance.updateSubscriptionFeed(scope, false);
|
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5)
|
||||||
|
StateSubscriptions.instance.updateSubscriptionFeed(scope, false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
Logger.w(TAG, "Too many subscription requests required:\n${reqCountStr}");
|
Logger.w(TAG, "Too many subscription requests required:\n${reqCountStr}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Started: Initialize [BackgroundWork]");
|
||||||
val interval = Settings.instance.subscriptions.getSubscriptionsBackgroundIntervalMinutes();
|
val interval = Settings.instance.subscriptions.getSubscriptionsBackgroundIntervalMinutes();
|
||||||
scheduleBackgroundWork(context, interval != 0, interval);
|
scheduleBackgroundWork(context, interval != 0, interval);
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Started: Initialize [AutoBackup]");
|
||||||
if(!Settings.instance.backup.didAskAutoBackup && !Settings.instance.backup.shouldAutomaticBackup()) {
|
if(!Settings.instance.backup.didAskAutoBackup && !Settings.instance.backup.shouldAutomaticBackup()) {
|
||||||
StateAnnouncement.instance.registerAnnouncement("backup", "Set Automatic Backup", "Configure daily backups of your data to restore in case of catastrophic failure.", AnnouncementType.SESSION, null, null, "Configure", {
|
StateAnnouncement.instance.registerAnnouncement("backup", "Set Automatic Backup", "Configure daily backups of your data to restore in case of catastrophic failure.", AnnouncementType.SESSION, null, null, "Configure", {
|
||||||
if(context is IWithResultLauncher && !Settings.instance.storage.isStorageMainValid(context)) {
|
if(context is IWithResultLauncher && !Settings.instance.storage.isStorageMainValid(context)) {
|
||||||
@@ -479,6 +529,7 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "MainApp Started: Initialize [Announcements]");
|
||||||
instance.scopeOrNull?.launch(Dispatchers.IO) {
|
instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
StateAnnouncement.instance.loadAnnouncements();
|
StateAnnouncement.instance.loadAnnouncements();
|
||||||
@@ -497,7 +548,7 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StateAnnouncement.instance.registerDidYouKnow();
|
StateAnnouncement.instance.registerDidYouKnow();
|
||||||
|
Logger.i(TAG, "MainApp Started: Finished");
|
||||||
}
|
}
|
||||||
fun mainAppStartedWithExternalFiles(context: Context) {
|
fun mainAppStartedWithExternalFiles(context: Context) {
|
||||||
if(!Settings.instance.didFirstStart) {
|
if(!Settings.instance.didFirstStart) {
|
||||||
@@ -696,6 +747,34 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getLocaleContext(baseContext: Context?): Context? {
|
||||||
|
val locale = getLocaleSetting(baseContext);
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (baseContext != null && locale != null) {
|
||||||
|
val config = baseContext.resources.configuration;
|
||||||
|
config.setLocale(locale);
|
||||||
|
return baseContext.createConfigurationContext(config);
|
||||||
|
}
|
||||||
|
return baseContext;
|
||||||
|
}
|
||||||
|
catch (ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to load locale", ex);
|
||||||
|
return baseContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun getLocaleSetting(context: Context?): Locale? {
|
||||||
|
return context?.getSharedPreferences("language", Context.MODE_PRIVATE)
|
||||||
|
?.getString("language", null)
|
||||||
|
?.let { Locale(it) };
|
||||||
|
}
|
||||||
|
fun setLocaleSetting(context: Context?, locale: String?) {
|
||||||
|
context?.getSharedPreferences("language", Context.MODE_PRIVATE)
|
||||||
|
?.edit()
|
||||||
|
?.putString("language", locale)
|
||||||
|
?.apply();
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "StateApp";
|
private val TAG = "StateApp";
|
||||||
@SuppressLint("StaticFieldLeak") //This is only alive while MainActivity is alive
|
@SuppressLint("StaticFieldLeak") //This is only alive while MainActivity is alive
|
||||||
|
|||||||
@@ -1,28 +1,19 @@
|
|||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.DocumentsContract.EXTRA_INITIAL_URI
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.core.app.ShareCompat
|
import androidx.core.app.ShareCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.copyTo
|
import com.futo.platformplayer.copyTo
|
||||||
import com.futo.platformplayer.copyToOutputStream
|
import com.futo.platformplayer.encryption.GPasswordEncryptionProvider
|
||||||
import com.futo.platformplayer.encryption.EncryptionProvider
|
import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0
|
||||||
import com.futo.platformplayer.getInputStream
|
|
||||||
import com.futo.platformplayer.getNowDiffHours
|
import com.futo.platformplayer.getNowDiffHours
|
||||||
import com.futo.platformplayer.getOutputStream
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.readBytes
|
import com.futo.platformplayer.readBytes
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
@@ -38,9 +29,8 @@ import kotlinx.serialization.json.Json
|
|||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.InputStream
|
import java.lang.Exception
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
@@ -82,7 +72,7 @@ class StateBackup {
|
|||||||
val pbytes = password.toByteArray();
|
val pbytes = password.toByteArray();
|
||||||
if(pbytes.size < 4 || pbytes.size > 32)
|
if(pbytes.size < 4 || pbytes.size > 32)
|
||||||
throw IllegalStateException("Automatic backup passwords should atleast be 4 character and smaller than 32");
|
throw IllegalStateException("Automatic backup passwords should atleast be 4 character and smaller than 32");
|
||||||
return password.padStart(32, '9');
|
return password;
|
||||||
}
|
}
|
||||||
fun hasAutomaticBackup(): Boolean {
|
fun hasAutomaticBackup(): Boolean {
|
||||||
val context = StateApp.instance.contextOrNull ?: return false;
|
val context = StateApp.instance.contextOrNull ?: return false;
|
||||||
@@ -106,8 +96,8 @@ class StateBackup {
|
|||||||
val data = export();
|
val data = export();
|
||||||
val zip = data.asZip();
|
val zip = data.asZip();
|
||||||
|
|
||||||
val encryptedZip = EncryptionProvider.instance.encrypt(zip, getAutomaticBackupPassword());
|
//Prepend some magic bytes to identify everything version 1 and up
|
||||||
|
val encryptedZip = byteArrayOf(0x11, 0x22, 0x33, 0x44, GPasswordEncryptionProvider.version.toByte()) + GPasswordEncryptionProvider.instance.encrypt(zip, getAutomaticBackupPassword());
|
||||||
if(!Settings.instance.storage.isStorageMainValid(context)) {
|
if(!Settings.instance.storage.isStorageMainValid(context)) {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
UIDialogs.toast("Missing permissions for auto-backup, please set the external general directory in settings");
|
UIDialogs.toast("Missing permissions for auto-backup, please set the external general directory in settings");
|
||||||
@@ -151,8 +141,7 @@ class StateBackup {
|
|||||||
throw IllegalStateException("Backup file does not exist");
|
throw IllegalStateException("Backup file does not exist");
|
||||||
|
|
||||||
val backupBytesEncrypted = backupFiles.first!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.first?.uri}]");
|
val backupBytesEncrypted = backupFiles.first!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.first?.uri}]");
|
||||||
val backupBytes = EncryptionProvider.instance.decrypt(backupBytesEncrypted, getAutomaticBackupPassword(password));
|
importEncryptedZipBytes(context, scope, backupBytesEncrypted, password);
|
||||||
importZipBytes(context, scope, backupBytes);
|
|
||||||
Logger.i(TAG, "Finished AutoBackup restore");
|
Logger.i(TAG, "Finished AutoBackup restore");
|
||||||
}
|
}
|
||||||
catch (exSec: FileNotFoundException) {
|
catch (exSec: FileNotFoundException) {
|
||||||
@@ -179,13 +168,30 @@ class StateBackup {
|
|||||||
throw ex;
|
throw ex;
|
||||||
|
|
||||||
val backupBytesEncrypted = backupFiles.second!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.second?.uri}]");
|
val backupBytesEncrypted = backupFiles.second!!.readBytes(context) ?: throw IllegalStateException("Could not read stream of [${backupFiles.second?.uri}]");
|
||||||
val backupBytes = EncryptionProvider.instance.decrypt(backupBytesEncrypted, getAutomaticBackupPassword(password));
|
importEncryptedZipBytes(context, scope, backupBytesEncrypted, password);
|
||||||
importZipBytes(context, scope, backupBytes);
|
|
||||||
Logger.i(TAG, "Finished AutoBackup restore");
|
Logger.i(TAG, "Finished AutoBackup restore");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun importEncryptedZipBytes(context: Context, scope: CoroutineScope, backupBytesEncrypted: ByteArray, password: String) {
|
||||||
|
val backupBytes: ByteArray;
|
||||||
|
//Check magic bytes indicating version 1 and up
|
||||||
|
if (backupBytesEncrypted[0] == 0x11.toByte() && backupBytesEncrypted[1] == 0x22.toByte() && backupBytesEncrypted[2] == 0x33.toByte() && backupBytesEncrypted[3] == 0x44.toByte()) {
|
||||||
|
val version = backupBytesEncrypted[4].toInt();
|
||||||
|
if (version != GPasswordEncryptionProvider.version) {
|
||||||
|
throw Exception("Invalid encryption version");
|
||||||
|
}
|
||||||
|
|
||||||
|
backupBytes = GPasswordEncryptionProvider.instance.decrypt(backupBytesEncrypted.sliceArray(IntRange(5, backupBytesEncrypted.size - 1)), getAutomaticBackupPassword(password))
|
||||||
|
} else {
|
||||||
|
//Else its a version 0
|
||||||
|
backupBytes = GPasswordEncryptionProviderV0(getAutomaticBackupPassword(password).padStart(32, '9')).decrypt(backupBytesEncrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
importZipBytes(context, scope, backupBytes);
|
||||||
|
}
|
||||||
|
|
||||||
fun startExternalBackup() {
|
fun startExternalBackup() {
|
||||||
val data = export();
|
val data = export();
|
||||||
val now = OffsetDateTime.now();
|
val now = OffsetDateTime.now();
|
||||||
|
|||||||
@@ -5,15 +5,39 @@ import com.futo.platformplayer.stores.StringHashSetStorage
|
|||||||
|
|
||||||
class StateMeta {
|
class StateMeta {
|
||||||
val hiddenVideos = FragmentedStorage.get<StringHashSetStorage>("hiddenVideos");
|
val hiddenVideos = FragmentedStorage.get<StringHashSetStorage>("hiddenVideos");
|
||||||
|
val hiddenCreators = FragmentedStorage.get<StringHashSetStorage>("hiddenCreators");
|
||||||
|
|
||||||
fun isVideoHidden(videoUrl: String) : Boolean {
|
fun isVideoHidden(videoUrl: String) : Boolean {
|
||||||
return hiddenVideos.contains(videoUrl);
|
return hiddenVideos.contains(videoUrl);
|
||||||
}
|
}
|
||||||
fun addHiddenVideo(videoUrl: String) {
|
fun addHiddenVideo(videoUrl: String) {
|
||||||
hiddenVideos.addDistinct(videoUrl);
|
hiddenVideos.addDistinct(videoUrl);
|
||||||
|
hiddenVideos.save();
|
||||||
}
|
}
|
||||||
fun removeHiddenVideo(videoUrl: String) {
|
fun removeHiddenVideo(videoUrl: String) {
|
||||||
hiddenVideos.remove(videoUrl);
|
hiddenVideos.remove(videoUrl);
|
||||||
|
hiddenVideos.save();
|
||||||
|
}
|
||||||
|
fun removeAllHiddenVideos() {
|
||||||
|
hiddenVideos.removeAll();
|
||||||
|
hiddenVideos.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun isCreatorHidden(creatorUrl: String): Boolean {
|
||||||
|
return hiddenCreators.contains(creatorUrl);
|
||||||
|
}
|
||||||
|
fun addHiddenCreator(creatorUrl: String) {
|
||||||
|
hiddenCreators.addDistinct(creatorUrl);
|
||||||
|
hiddenCreators.save();
|
||||||
|
}
|
||||||
|
fun removeHiddenCreator(creatorUrl: String) {
|
||||||
|
hiddenCreators.remove(creatorUrl);
|
||||||
|
hiddenCreators.save();
|
||||||
|
}
|
||||||
|
fun removeAllHiddenCreators() {
|
||||||
|
hiddenCreators.removeAll();
|
||||||
|
hiddenCreators.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.futo.platformplayer.api.media.models.FilterGroup
|
|||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
@@ -406,8 +407,9 @@ class StatePlatform {
|
|||||||
return@async searchResult;
|
return@async searchResult;
|
||||||
} catch(ex: Throwable) {
|
} catch(ex: Throwable) {
|
||||||
Logger.e(TAG, "getHomeRefresh", ex);
|
Logger.e(TAG, "getHomeRefresh", ex);
|
||||||
throw ex;
|
//throw ex;
|
||||||
//return@async null;
|
//return@async null;
|
||||||
|
return@async PlaceholderPager(10, { PlatformContentPlaceholder(it.id, ex) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}.toList();
|
}.toList();
|
||||||
@@ -615,6 +617,14 @@ class StatePlatform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getContentChapters(url: String): List<IChapter>? {
|
||||||
|
val baseClient = getContentClientOrNull(url) ?: return null;
|
||||||
|
if (baseClient !is JSClient) {
|
||||||
|
return baseClient.getContentChapters(url);
|
||||||
|
}
|
||||||
|
val client = _trackerClientPool.getClientPooled(baseClient, 1);
|
||||||
|
return client.getContentChapters(url);
|
||||||
|
}
|
||||||
fun getPlaybackTracker(url: String): IPlaybackTracker? {
|
fun getPlaybackTracker(url: String): IPlaybackTracker? {
|
||||||
val baseClient = getContentClientOrNull(url) ?: return null;
|
val baseClient = getContentClientOrNull(url) ?: return null;
|
||||||
if (baseClient !is JSClient) {
|
if (baseClient !is JSClient) {
|
||||||
@@ -642,11 +652,8 @@ class StatePlatform {
|
|||||||
return _scope.async { getChannelLive(url, updateSubscriptions) };
|
return _scope.async { getChannelLive(url, updateSubscriptions) };
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getChannelContent(channelUrl: String, isSubscriptionOptimized: Boolean = false, usePooledClients: Int = 0, ignorePlugins: List<String>? = null): IPager<IPlatformContent> {
|
fun getChannelContent(baseClient: IPlatformClient, channelUrl: String, isSubscriptionOptimized: Boolean = false, usePooledClients: Int = 0, ignorePlugins: List<String>? = null): IPager<IPlatformContent> {
|
||||||
Logger.i(TAG, "Platform - getChannelVideos");
|
|
||||||
val baseClient = getChannelClient(channelUrl, ignorePlugins);
|
|
||||||
val clientCapabilities = baseClient.getChannelCapabilities();
|
val clientCapabilities = baseClient.getChannelCapabilities();
|
||||||
|
|
||||||
val client = if(usePooledClients > 1)
|
val client = if(usePooledClients > 1)
|
||||||
_channelClientPool.getClientPooled(baseClient, usePooledClients);
|
_channelClientPool.getClientPooled(baseClient, usePooledClients);
|
||||||
else baseClient;
|
else baseClient;
|
||||||
@@ -756,11 +763,25 @@ class StatePlatform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(hasChanges)
|
if(hasChanges)
|
||||||
StateSubscriptions.instance.saveSubscription(sub);
|
sub.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
return pagerResult;
|
return pagerResult;
|
||||||
}
|
}
|
||||||
|
fun getChannelContent(channelUrl: String, isSubscriptionOptimized: Boolean = false, usePooledClients: Int = 0, ignorePlugins: List<String>? = null): IPager<IPlatformContent> {
|
||||||
|
Logger.i(TAG, "Platform - getChannelVideos");
|
||||||
|
val baseClient = getChannelClient(channelUrl, ignorePlugins);
|
||||||
|
return getChannelContent(baseClient, channelUrl, isSubscriptionOptimized, usePooledClients, ignorePlugins);
|
||||||
|
}
|
||||||
|
fun getChannelContent(channelUrl: String, type: String?, ordering: String = ResultCapabilities.ORDER_CHONOLOGICAL): IPager<IPlatformContent> {
|
||||||
|
val client = getChannelClient(channelUrl);
|
||||||
|
return getChannelContent(client, channelUrl, type, ordering);
|
||||||
|
}
|
||||||
|
fun getChannelContent(baseClient: IPlatformClient, channelUrl: String, type: String?, ordering: String = ResultCapabilities.ORDER_CHONOLOGICAL): IPager<IPlatformContent> {
|
||||||
|
val client = _channelClientPool.getClientPooled(baseClient, Settings.instance.subscriptions.getSubscriptionsConcurrency());
|
||||||
|
return client.getChannelContents(channelUrl, type, ordering) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getChannelLive(url: String, updateSubscriptions: Boolean = true): IPlatformChannel {
|
fun getChannelLive(url: String, updateSubscriptions: Boolean = true): IPlatformChannel {
|
||||||
val channel = getChannelClient(url).getChannel(url);
|
val channel = getChannelClient(url).getChannel(url);
|
||||||
@@ -784,6 +805,15 @@ class StatePlatform {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resolveChannelUrlsByClaimTemplates(claimType: Int, claimValues: Map<Int, String>): List<String> {
|
||||||
|
val urls = arrayListOf<String>();
|
||||||
|
for(client in getClientsByClaimType(claimType).filter { it is JSClient }) {
|
||||||
|
val res = (client as JSClient).resolveChannelUrlsByClaimTemplates(claimType, claimValues);
|
||||||
|
urls.addAll(res);
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
fun getPlaylistClientOrNull(url: String): IPlatformClient? = getEnabledClients().find { it.isPlaylistUrl(url) }
|
fun getPlaylistClientOrNull(url: String): IPlatformClient? = getEnabledClients().find { it.isPlaylistUrl(url) }
|
||||||
fun getPlaylistClient(url: String): IPlatformClient = getEnabledClients().find { it.isPlaylistUrl(url) }
|
fun getPlaylistClient(url: String): IPlatformClient = getEnabledClients().find { it.isPlaylistUrl(url) }
|
||||||
?: throw NoPlatformClientException("No client enabled that supports this playlist url (${url})");
|
?: throw NoPlatformClientException("No client enabled that supports this playlist url (${url})");
|
||||||
|
|||||||
@@ -108,16 +108,15 @@ class StatePlugins {
|
|||||||
instance.deletePlugin(embedded.key);
|
instance.deletePlugin(embedded.key);
|
||||||
StatePlatform.instance.updateAvailableClients(context);
|
StatePlatform.instance.updateAvailableClients(context);
|
||||||
}
|
}
|
||||||
fun updateEmbeddedPlugins(context: Context) {
|
fun updateEmbeddedPlugins(context: Context, subset: List<String>? = null, force: Boolean = false) {
|
||||||
for(embedded in getEmbeddedSources(context)) {
|
for(embedded in getEmbeddedSources(context).filter { subset == null || subset.contains(it.key) }) {
|
||||||
val embeddedConfig = getEmbeddedPluginConfig(context, embedded.value);
|
val embeddedConfig = getEmbeddedPluginConfig(context, embedded.value);
|
||||||
if(FORCE_REINSTALL_EMBEDDED)
|
if(embeddedConfig != null) {
|
||||||
deletePlugin(embedded.key);
|
|
||||||
else if(embeddedConfig != null) {
|
|
||||||
val existing = getPlugin(embedded.key);
|
val existing = getPlugin(embedded.key);
|
||||||
if(existing != null && existing.config.version < embeddedConfig.version ) {
|
if(existing != null && (existing.config.version < embeddedConfig.version || (force || FORCE_REINSTALL_EMBEDDED))) {
|
||||||
Logger.i(TAG, "Found outdated embedded plugin [${existing.config.id}] ${existing.config.name}, deleting and reinstalling");
|
Logger.i(TAG, "Outdated Embedded plugin [${existing.config.id}] ${existing.config.name} (${existing.config.version} < ${embeddedConfig?.version}), reinstalling");
|
||||||
deletePlugin(embedded.key);
|
//deletePlugin(embedded.key);
|
||||||
|
installEmbeddedPlugin(context, embedded.value)
|
||||||
}
|
}
|
||||||
else if(existing != null && _isFirstEmbedUpdate)
|
else if(existing != null && _isFirstEmbedUpdate)
|
||||||
Logger.i(TAG, "Embedded plugin [${existing.config.id}] ${existing.config.name}, up to date (${existing.config.version} >= ${embeddedConfig?.version})");
|
Logger.i(TAG, "Embedded plugin [${existing.config.id}] ${existing.config.name}, up to date (${existing.config.version} >= ${embeddedConfig?.version})");
|
||||||
@@ -360,6 +359,8 @@ class StatePlugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val existing = getPlugin(config.id)
|
val existing = getPlugin(config.id)
|
||||||
|
val existingAuth = existing?.getAuth();
|
||||||
|
val existingCaptcha = existing?.getCaptchaData();
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
if(!reinstall)
|
if(!reinstall)
|
||||||
throw IllegalStateException("Plugin with id ${config.id} already exists");
|
throw IllegalStateException("Plugin with id ${config.id} already exists");
|
||||||
@@ -373,7 +374,10 @@ class StatePlugins {
|
|||||||
if(icon != null)
|
if(icon != null)
|
||||||
iconsDir.saveIconBinary(config.id, icon);
|
iconsDir.saveIconBinary(config.id, icon);
|
||||||
|
|
||||||
_plugins.save(SourcePluginDescriptor(config, null, null, flags));
|
val descriptor = SourcePluginDescriptor(config, existingAuth?.toEncrypted(), existingCaptcha?.toEncrypted(), flags);
|
||||||
|
descriptor.settings = existing?.settings ?: descriptor.settings;
|
||||||
|
descriptor.appSettings = existing?.appSettings ?: descriptor.appSettings;
|
||||||
|
_plugins.save(descriptor);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import com.futo.platformplayer.api.media.structures.IAsyncPager
|
|||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||||
import com.futo.platformplayer.api.media.structures.PlaceholderPager
|
import com.futo.platformplayer.api.media.structures.PlaceholderPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.RefreshChronoContentPager
|
||||||
import com.futo.platformplayer.api.media.structures.RefreshDedupContentPager
|
import com.futo.platformplayer.api.media.structures.RefreshDedupContentPager
|
||||||
import com.futo.platformplayer.api.media.structures.RefreshDistributionContentPager
|
import com.futo.platformplayer.api.media.structures.RefreshDistributionContentPager
|
||||||
import com.futo.platformplayer.awaitFirstDeferred
|
import com.futo.platformplayer.awaitFirstDeferred
|
||||||
@@ -130,10 +131,7 @@ class StatePolycentric {
|
|||||||
//TODO: Currently abusing subscription concurrency for parallelism
|
//TODO: Currently abusing subscription concurrency for parallelism
|
||||||
val concurrency = if (channelConcurrency == -1) Settings.instance.subscriptions.getSubscriptionsConcurrency() else channelConcurrency;
|
val concurrency = if (channelConcurrency == -1) Settings.instance.subscriptions.getSubscriptionsConcurrency() else channelConcurrency;
|
||||||
val pagers = profile.ownedClaims.groupBy { it.claim.claimType }.mapNotNull {
|
val pagers = profile.ownedClaims.groupBy { it.claim.claimType }.mapNotNull {
|
||||||
//TODO: Deduplicate once multiple urls in single claim is supported
|
val url = it.value.firstOrNull()?.claim?.resolveChannelUrl() ?: return@mapNotNull null;
|
||||||
return@mapNotNull it.value.firstOrNull();
|
|
||||||
}.mapNotNull {
|
|
||||||
val url = it.claim.resolveChannelUrl() ?: return@mapNotNull null;
|
|
||||||
if (!StatePlatform.instance.hasEnabledChannelClient(url)) {
|
if (!StatePlatform.instance.hasEnabledChannelClient(url)) {
|
||||||
return@mapNotNull null;
|
return@mapNotNull null;
|
||||||
}
|
}
|
||||||
@@ -146,15 +144,40 @@ class StatePolycentric {
|
|||||||
return DedupContentPager(pager, StatePlatform.instance.getEnabledClients().map { it.id });
|
return DedupContentPager(pager, StatePlatform.instance.getEnabledClients().map { it.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getChannelUrls(url: String, channelId: PlatformID? = null, cacheOnly: Boolean = false): List<String> {
|
||||||
|
|
||||||
|
var polycentricProfile: PolycentricProfile? = null;
|
||||||
|
try {
|
||||||
|
polycentricProfile = PolycentricCache.instance.getCachedProfile(url)?.profile;
|
||||||
|
if (polycentricProfile == null && channelId != null) {
|
||||||
|
Logger.i("StateSubscriptions", "Get polycentric profile not cached");
|
||||||
|
if(!cacheOnly)
|
||||||
|
polycentricProfile = runBlocking { PolycentricCache.instance.getProfileAsync(channelId) }?.profile;
|
||||||
|
} else {
|
||||||
|
Logger.i("StateSubscriptions", "Get polycentric profile cached");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.w(StateSubscriptions.TAG, "Polycentric getCachedProfile failed for subscriptions", ex);
|
||||||
|
//TODO: Some way to communicate polycentric failing without blocking here
|
||||||
|
}
|
||||||
|
if(polycentricProfile != null) {
|
||||||
|
val urls = polycentricProfile.ownedClaims.groupBy { it.claim.claimType }
|
||||||
|
.mapNotNull { it.value.firstOrNull()?.claim?.resolveChannelUrl() }.toMutableList();
|
||||||
|
if(urls.any { it.equals(url, true) })
|
||||||
|
return urls;
|
||||||
|
else
|
||||||
|
return listOf(url) + urls;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return listOf(url);
|
||||||
|
}
|
||||||
fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager<IPlatformContent>? {
|
fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager<IPlatformContent>? {
|
||||||
//TODO: Currently abusing subscription concurrency for parallelism
|
//TODO: Currently abusing subscription concurrency for parallelism
|
||||||
val concurrency = if (channelConcurrency == -1) Settings.instance.subscriptions.getSubscriptionsConcurrency() else channelConcurrency;
|
val concurrency = if (channelConcurrency == -1) Settings.instance.subscriptions.getSubscriptionsConcurrency() else channelConcurrency;
|
||||||
val deferred = profile.ownedClaims.groupBy { it.claim.claimType }
|
val deferred = profile.ownedClaims.groupBy { it.claim.claimType }
|
||||||
.mapNotNull {
|
.mapNotNull {
|
||||||
//TODO: Deduplicate once multiple urls in single claim is supported
|
val url = it.value.firstOrNull()?.claim?.resolveChannelUrl() ?: return@mapNotNull null;
|
||||||
return@mapNotNull it.value.firstOrNull();
|
|
||||||
}.mapNotNull {
|
|
||||||
val url = it.claim.resolveChannelUrl() ?: return@mapNotNull null;
|
|
||||||
val client = StatePlatform.instance.getChannelClientOrNull(url) ?: return@mapNotNull null;
|
val client = StatePlatform.instance.getChannelClientOrNull(url) ?: return@mapNotNull null;
|
||||||
|
|
||||||
return@mapNotNull Pair(client, scope.async(Dispatchers.IO) {
|
return@mapNotNull Pair(client, scope.async(Dispatchers.IO) {
|
||||||
@@ -173,12 +196,21 @@ class StatePolycentric {
|
|||||||
}) ?: return null;
|
}) ?: return null;
|
||||||
|
|
||||||
val toAwait = deferred.filter { it.second != finishedPager.first };
|
val toAwait = deferred.filter { it.second != finishedPager.first };
|
||||||
|
|
||||||
|
//TODO: Get a Parallel pager to work here.
|
||||||
|
val innerPager = MultiChronoContentPager(listOf(finishedPager.second!!) + toAwait.mapNotNull { runBlocking { it.second.await(); } });
|
||||||
|
innerPager.initialize();
|
||||||
|
//return RefreshChronoContentPager(listOf(finishedPager.second!!), toAwait.map { it.second }, listOf());
|
||||||
|
//return RefreshDedupContentPager(RefreshChronoContentPager(listOf(finishedPager.second!!), toAwait.map { it.second }, listOf()), StatePlatform.instance.getEnabledClients().map { it.id });
|
||||||
|
return DedupContentPager(innerPager, StatePlatform.instance.getEnabledClients().map { it.id });
|
||||||
|
|
||||||
|
/* //Gives out-of-order results
|
||||||
return RefreshDedupContentPager(RefreshDistributionContentPager(
|
return RefreshDedupContentPager(RefreshDistributionContentPager(
|
||||||
listOf(finishedPager.second!!),
|
listOf(finishedPager.second!!),
|
||||||
toAwait.map { it.second },
|
toAwait.map { it.second },
|
||||||
toAwait.map { PlaceholderPager(5) { PlatformContentPlaceholder(it.first.id) } }),
|
toAwait.map { PlaceholderPager(5) { PlatformContentPlaceholder(it.first.id) } }),
|
||||||
StatePlatform.instance.getEnabledClients().map { it.id }
|
StatePlatform.instance.getEnabledClients().map { it.id }
|
||||||
);
|
);*/
|
||||||
}
|
}
|
||||||
suspend fun getChannelContent(profile: PolycentricProfile): IPager<IPlatformContent> {
|
suspend fun getChannelContent(profile: PolycentricProfile): IPager<IPlatformContent> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
|
|||||||
@@ -29,13 +29,17 @@ import com.futo.platformplayer.stores.FragmentedStorage
|
|||||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||||
import com.futo.platformplayer.stores.v2.ReconstructStore
|
import com.futo.platformplayer.stores.v2.ReconstructStore
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
|
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
|
||||||
|
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
import java.util.concurrent.ForkJoinPool
|
import java.util.concurrent.ForkJoinPool
|
||||||
import java.util.concurrent.ForkJoinTask
|
import java.util.concurrent.ForkJoinTask
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
import kotlin.streams.toList
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
/***
|
/***
|
||||||
@@ -53,7 +57,6 @@ class StateSubscriptions {
|
|||||||
private val _subscriptionsPool = ForkJoinPool(Settings.instance.subscriptions.getSubscriptionsConcurrency());
|
private val _subscriptionsPool = ForkJoinPool(Settings.instance.subscriptions.getSubscriptionsConcurrency());
|
||||||
private val _legacySubscriptions = FragmentedStorage.get<SubscriptionStorage>();
|
private val _legacySubscriptions = FragmentedStorage.get<SubscriptionStorage>();
|
||||||
|
|
||||||
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
|
||||||
|
|
||||||
private var _globalSubscriptionsLock = Object();
|
private var _globalSubscriptionsLock = Object();
|
||||||
private var _globalSubscriptionFeed: ReusablePager<IPlatformContent>? = null;
|
private var _globalSubscriptionFeed: ReusablePager<IPlatformContent>? = null;
|
||||||
@@ -62,6 +65,8 @@ class StateSubscriptions {
|
|||||||
var globalSubscriptionExceptions: List<Throwable> = listOf()
|
var globalSubscriptionExceptions: List<Throwable> = listOf()
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
|
private val _algorithmSubscriptions = SubscriptionFetchAlgorithms.SMART;
|
||||||
|
|
||||||
private var _lastGlobalSubscriptionProgress: Int = 0;
|
private var _lastGlobalSubscriptionProgress: Int = 0;
|
||||||
private var _lastGlobalSubscriptionTotal: Int = 0;
|
private var _lastGlobalSubscriptionTotal: Int = 0;
|
||||||
val onGlobalSubscriptionsUpdateProgress = Event2<Int, Int>();
|
val onGlobalSubscriptionsUpdateProgress = Event2<Int, Int>();
|
||||||
@@ -69,6 +74,15 @@ class StateSubscriptions {
|
|||||||
val onGlobalSubscriptionsUpdatedOnce = Event1<Throwable?>();
|
val onGlobalSubscriptionsUpdatedOnce = Event1<Throwable?>();
|
||||||
val onGlobalSubscriptionsException = Event1<List<Throwable>>();
|
val onGlobalSubscriptionsException = Event1<List<Throwable>>();
|
||||||
|
|
||||||
|
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
||||||
|
|
||||||
|
fun getOldestUpdateTime(): OffsetDateTime {
|
||||||
|
val subs = getSubscriptions();
|
||||||
|
if(subs.size == 0)
|
||||||
|
return OffsetDateTime.now();
|
||||||
|
else
|
||||||
|
return subs.minOf { it.lastVideoUpdate };
|
||||||
|
}
|
||||||
fun getGlobalSubscriptionProgress(): Pair<Int, Int> {
|
fun getGlobalSubscriptionProgress(): Pair<Int, Int> {
|
||||||
return Pair(_lastGlobalSubscriptionProgress, _lastGlobalSubscriptionTotal);
|
return Pair(_lastGlobalSubscriptionProgress, _lastGlobalSubscriptionTotal);
|
||||||
}
|
}
|
||||||
@@ -165,6 +179,9 @@ class StateSubscriptions {
|
|||||||
fun saveSubscription(sub: Subscription) {
|
fun saveSubscription(sub: Subscription) {
|
||||||
_subscriptions.save(sub, false, true);
|
_subscriptions.save(sub, false, true);
|
||||||
}
|
}
|
||||||
|
fun saveSubscriptionAsync(sub: Subscription) {
|
||||||
|
_subscriptions.saveAsync(sub, false, true);
|
||||||
|
}
|
||||||
fun getSubscriptionCount(): Int {
|
fun getSubscriptionCount(): Int {
|
||||||
synchronized(_subscriptions) {
|
synchronized(_subscriptions) {
|
||||||
return _subscriptions.getItems().size;
|
return _subscriptions.getItems().size;
|
||||||
@@ -223,167 +240,30 @@ class StateSubscriptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getSubscriptionRequestCount(): Map<JSClient, Int> {
|
fun getSubscriptionRequestCount(): Map<JSClient, Int> {
|
||||||
val subs = getSubscriptions();
|
return SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, StateApp.instance.scope)
|
||||||
val pluginReqCounts = mutableMapOf<JSClient, Int>();
|
.countRequests(getSubscriptions().associateWith { StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id, true) });
|
||||||
|
}
|
||||||
|
|
||||||
for(sub in subs) {
|
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
||||||
val client = StatePlatform.instance.getChannelClientOrNull(sub.channel.url);
|
val algo = SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, cacheScope, allowFailure, withCacheFallback, _subscriptionsPool);
|
||||||
if(client !is JSClient)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
val channelCaps = client.getChannelCapabilities();
|
algo.onProgress.subscribe { progress, total ->
|
||||||
if(!pluginReqCounts.containsKey(client))
|
onProgress?.invoke(progress, total);
|
||||||
pluginReqCounts[client] = 1;
|
}
|
||||||
|
algo.onNewCacheHit.subscribe { sub, content ->
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
val usePolycentric = true;
|
||||||
|
val subUrls = getSubscriptions().parallelStream().map {
|
||||||
|
if(usePolycentric)
|
||||||
|
Pair(it, StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id));
|
||||||
else
|
else
|
||||||
pluginReqCounts[client] = pluginReqCounts[client]!! + 1;
|
Pair(it, listOf(it.channel.url));
|
||||||
|
}.toList().associate { it };
|
||||||
|
|
||||||
if(channelCaps.hasType(ResultCapabilities.TYPE_STREAMS) && sub.shouldFetchStreams())
|
val result = algo.getSubscriptions(subUrls);
|
||||||
pluginReqCounts[client] = pluginReqCounts[client]!! + 1;
|
return Pair(result.pager, result.exceptions);
|
||||||
if(channelCaps.hasType(ResultCapabilities.TYPE_LIVE) && sub.shouldFetchLiveStreams())
|
|
||||||
pluginReqCounts[client] = pluginReqCounts[client]!! + 1;
|
|
||||||
if(channelCaps.hasType(ResultCapabilities.TYPE_POSTS) && sub.shouldFetchPosts())
|
|
||||||
pluginReqCounts[client] = pluginReqCounts[client]!! + 1;
|
|
||||||
}
|
|
||||||
return pluginReqCounts;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSubscriptionsFeed(allowFailure: Boolean = false): IPager<IPlatformContent> {
|
|
||||||
val result = getSubscriptionsFeedWithExceptions(allowFailure, true);
|
|
||||||
if(result.second.any())
|
|
||||||
throw result.second.first();
|
|
||||||
return result.first;
|
|
||||||
}
|
|
||||||
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope? = null, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
|
||||||
val subsPager: Array<IPager<IPlatformContent>>;
|
|
||||||
val exs: ArrayList<Throwable> = arrayListOf();
|
|
||||||
|
|
||||||
val tasks = mutableListOf<ForkJoinTask<Pair<Subscription, IPager<IPlatformContent>?>>>();
|
|
||||||
var finished = 0;
|
|
||||||
val exceptionMap: HashMap<Subscription, Throwable> = hashMapOf();
|
|
||||||
val concurrency = Settings.instance.subscriptions.getSubscriptionsConcurrency();
|
|
||||||
val failedPlugins = arrayListOf<String>();
|
|
||||||
for (sub in getSubscriptions().filter { StatePlatform.instance.hasEnabledChannelClient(it.channel.url) }) {
|
|
||||||
tasks.add(_subscriptionsPool.submit<Pair<Subscription, IPager<IPlatformContent>?>> {
|
|
||||||
val toIgnore = synchronized(failedPlugins){ failedPlugins.toList() };
|
|
||||||
|
|
||||||
var polycentricProfile : PolycentricCache.CachedPolycentricProfile? = null;
|
|
||||||
val getProfileTime = measureTimeMillis {
|
|
||||||
try {
|
|
||||||
polycentricProfile = PolycentricCache.instance.getCachedProfile(sub.channel.url);
|
|
||||||
if (polycentricProfile == null) {
|
|
||||||
Logger.i("StateSubscriptions", "Get polycentric profile not cached");
|
|
||||||
polycentricProfile = runBlocking { PolycentricCache.instance.getProfileAsync(sub.channel.id) };
|
|
||||||
} else {
|
|
||||||
Logger.i("StateSubscriptions", "Get polycentric profile cached");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.w(TAG, "Polycentric getCachedProfile failed for subscriptions", ex);
|
|
||||||
//TODO: Some way to communicate polycentric failing without blocking here
|
|
||||||
//UIDialogs.toast("Polycentric failed\n" + ex.message, false);
|
|
||||||
//UIDialogs.showGeneralErrorDialog(it, "Polycentric getCachedProfile failed for subscriptions", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i("StateSubscriptions", "Get polycentric profile time ${getProfileTime}ms");
|
|
||||||
|
|
||||||
var pager: IPager<IPlatformContent>;
|
|
||||||
try {
|
|
||||||
val time = measureTimeMillis {
|
|
||||||
val profile = polycentricProfile?.profile
|
|
||||||
pager = if (profile != null)
|
|
||||||
StatePolycentric.instance.getChannelContent(profile, true, concurrency, toIgnore)
|
|
||||||
else
|
|
||||||
StatePlatform.instance.getChannelContent(sub.channel.url, true, concurrency, toIgnore);
|
|
||||||
|
|
||||||
if (cacheScope != null)
|
|
||||||
pager = ChannelContentCache.cachePagerResults(cacheScope, pager) {
|
|
||||||
onNewCacheHit?.invoke(sub, it);
|
|
||||||
};
|
|
||||||
|
|
||||||
finished++;
|
|
||||||
onProgress?.invoke(finished, tasks.size);
|
|
||||||
}
|
|
||||||
Logger.i(
|
|
||||||
"StateSubscriptions",
|
|
||||||
"Subscription [${sub.channel.name}] results in ${time}ms"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.e(TAG, "Subscription [${sub.channel.name}] failed", ex);
|
|
||||||
finished++;
|
|
||||||
onProgress?.invoke(finished, tasks.size);
|
|
||||||
val channelEx = ChannelException(sub.channel, ex);
|
|
||||||
synchronized(exceptionMap) {
|
|
||||||
exceptionMap.put(sub, channelEx);
|
|
||||||
}
|
|
||||||
if(ex is ScriptCaptchaRequiredException) {
|
|
||||||
synchronized(failedPlugins) {
|
|
||||||
//Fail all subscription calls to plugin if it has a captcha issue
|
|
||||||
if(ex.config is SourcePluginConfig && !failedPlugins.contains(ex.config.id)) {
|
|
||||||
Logger.w(TAG, "Subscriptionsgnoring plugin [${ex.config.name}] due to Captcha");
|
|
||||||
failedPlugins.add(ex.config.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(ex is ScriptCriticalException) {
|
|
||||||
synchronized(failedPlugins) {
|
|
||||||
//Fail all subscription calls to plugin if it has a critical issue
|
|
||||||
if(ex.config is SourcePluginConfig && !failedPlugins.contains(ex.config.id)) {
|
|
||||||
Logger.w(TAG, "Subscriptions ignoring plugin [${ex.config.name}] due to critical exception:\n" + ex.message);
|
|
||||||
failedPlugins.add(ex.config.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(!withCacheFallback)
|
|
||||||
throw channelEx;
|
|
||||||
else {
|
|
||||||
Logger.i(TAG, "Channel ${sub.channel.name} failed, substituting with cache");
|
|
||||||
pager = ChannelContentCache.instance.getChannelCachePager(sub.channel.url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return@submit Pair(sub, pager);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
val timeTotal = measureTimeMillis {
|
|
||||||
val taskResults = arrayListOf<IPager<IPlatformContent>>();
|
|
||||||
for(task in tasks) {
|
|
||||||
try {
|
|
||||||
val result = task.get();
|
|
||||||
if(result != null) {
|
|
||||||
if(result.second != null)
|
|
||||||
taskResults.add(result.second!!);
|
|
||||||
if(exceptionMap.containsKey(result.first)) {
|
|
||||||
val ex = exceptionMap[result.first];
|
|
||||||
if(ex != null) {
|
|
||||||
val nonRuntimeEx = findNonRuntimeException(ex);
|
|
||||||
if (nonRuntimeEx != null && (nonRuntimeEx is PluginException || nonRuntimeEx is ChannelException))
|
|
||||||
exs.add(nonRuntimeEx);
|
|
||||||
else
|
|
||||||
throw ex.cause ?: ex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ex: ExecutionException) {
|
|
||||||
val nonRuntimeEx = findNonRuntimeException(ex.cause);
|
|
||||||
if(nonRuntimeEx != null && (nonRuntimeEx is PluginException || nonRuntimeEx is ChannelException))
|
|
||||||
exs.add(nonRuntimeEx);
|
|
||||||
else
|
|
||||||
throw ex.cause ?: ex;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
subsPager = taskResults.toTypedArray();
|
|
||||||
}
|
|
||||||
Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms")
|
|
||||||
|
|
||||||
if(subsPager.size <= 0 && exs.any())
|
|
||||||
throw exs.first();
|
|
||||||
|
|
||||||
Logger.i(TAG, "Subscription pager with ${subsPager.size} channels");
|
|
||||||
val pager = MultiChronoContentPager(subsPager, allowFailure, 15);
|
|
||||||
pager.initialize();
|
|
||||||
//return Pair(pager, exs);
|
|
||||||
return Pair(DedupContentPager(pager), exs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//New Migration
|
//New Migration
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ class StateTelemetry {
|
|||||||
BuildConfig.IS_UNSTABLE_BUILD,
|
BuildConfig.IS_UNSTABLE_BUILD,
|
||||||
Build.BRAND,
|
Build.BRAND,
|
||||||
Build.MANUFACTURER,
|
Build.MANUFACTURER,
|
||||||
Build.MODEL
|
Build.MODEL,
|
||||||
|
Build.VERSION.SDK_INT
|
||||||
);
|
);
|
||||||
|
|
||||||
val headers = hashMapOf(
|
val headers = hashMapOf(
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ class StringHashSetStorage : FragmentedStorageFileJson() {
|
|||||||
values.remove(obj);
|
values.remove(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun removeAll() {
|
||||||
|
synchronized(values) {
|
||||||
|
values.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
fun set(vararg objs: String) {
|
fun set(vararg objs: String) {
|
||||||
synchronized(values) {
|
synchronized(values) {
|
||||||
values.clear();
|
values.clear();
|
||||||
|
|||||||
@@ -181,6 +181,12 @@ class ManagedStore<T>{
|
|||||||
return ReconstructionResult(successes, exs, builder.messages);
|
return ReconstructionResult(successes, exs, builder.messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun count(): Int {
|
||||||
|
synchronized(_files) {
|
||||||
|
return _files.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
fun getItems(): List<T> {
|
fun getItems(): List<T> {
|
||||||
synchronized(_files) {
|
synchronized(_files) {
|
||||||
return _files.map { it.key };
|
return _files.map { it.key };
|
||||||
@@ -221,18 +227,18 @@ class ManagedStore<T>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun saveAsync(obj: T, withReconstruction: Boolean = false) {
|
fun saveAsync(obj: T, withReconstruction: Boolean = false, onlyExisting: Boolean = false) {
|
||||||
val scope = StateApp.instance.scopeOrNull;
|
val scope = StateApp.instance.scopeOrNull;
|
||||||
if(scope != null)
|
if(scope != null)
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
save(obj, withReconstruction);
|
save(obj, withReconstruction, onlyExisting);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to save.", e);
|
Logger.e(TAG, "Failed to save.", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
else
|
else
|
||||||
save(obj, withReconstruction);
|
save(obj, withReconstruction, onlyExisting);
|
||||||
}
|
}
|
||||||
fun saveAllAsync(objs: List<T>, withReconstruction: Boolean = false) {
|
fun saveAllAsync(objs: List<T>, withReconstruction: Boolean = false) {
|
||||||
val scope = StateApp.instance.scopeOrNull;
|
val scope = StateApp.instance.scopeOrNull;
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.futo.platformplayer.subscription
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.structures.DedupContentPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.PlatformContentPager
|
||||||
|
import com.futo.platformplayer.cache.ChannelContentCache
|
||||||
|
import com.futo.platformplayer.models.Subscription
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
|
import com.futo.platformplayer.toSafeFileName
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import java.util.concurrent.ForkJoinPool
|
||||||
|
|
||||||
|
class CachedSubscriptionAlgorithm(pageSize: Int = 150, scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = true, threadPool: ForkJoinPool? = null)
|
||||||
|
: SubscriptionFetchAlgorithm(scope, allowFailure, withCacheFallback, threadPool) {
|
||||||
|
|
||||||
|
private val _pageSize: Int = pageSize;
|
||||||
|
|
||||||
|
override fun countRequests(subs: Map<Subscription, List<String>>): Map<JSClient, Int> {
|
||||||
|
return mapOf<JSClient, Int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
|
||||||
|
val validSubIds = subs.flatMap { it.value } .map { it.toSafeFileName() }.toHashSet();
|
||||||
|
|
||||||
|
val validStores = ChannelContentCache.instance._channelContents
|
||||||
|
.filter { validSubIds.contains(it.key) }
|
||||||
|
.map { it.value };
|
||||||
|
|
||||||
|
val items = validStores.flatMap { it.getItems() }
|
||||||
|
.sortedByDescending { it.datetime };
|
||||||
|
|
||||||
|
return Result(DedupContentPager(PlatformContentPager(items, Math.min(_pageSize, items.size)), StatePlatform.instance.getEnabledClients().map { it.id }), listOf());
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user