mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-26 17:55:20 +02:00
Compare commits
160 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 | |||
| c2dce52a5b | |||
| a2c63c59c5 | |||
| 7e54a2ce3d | |||
| 5b7fb2c818 | |||
| da0ac281e2 | |||
| 576b37f64c | |||
| 26c2db5023 | |||
| f344dbf35c | |||
| a04acbd4a5 | |||
| bd48aba8d3 | |||
| 12b73bb248 | |||
| c3ff897ef4 | |||
| 242728fbe7 | |||
| 14df7c8d43 | |||
| 229377bd6e | |||
| d4317ff06f | |||
| c70dbb56c8 | |||
| f9b772b729 | |||
| bbcc424393 | |||
| f433cb1280 | |||
| 9cf81ad20a | |||
| f65e293e45 | |||
| 9a08762e9e | |||
| 66dbd20a90 | |||
| 8254bcc647 | |||
| 51d0f18168 | |||
| 5dcb535c0f | |||
| b7cbeb3837 | |||
| 2067561c09 | |||
| 1ac70dba3f | |||
| f4370c1bfd | |||
| 73321ee362 | |||
| 182c88fc9e | |||
| 9d39d74be5 | |||
| d8d8d6f666 | |||
| df0504cead | |||
| 851b547d64 | |||
| f49ecf1159 | |||
| 081ae1dd88 | |||
| 374d9950be | |||
| 9ffdf39f13 | |||
| 8bb1ff87c0 | |||
| 67e29999ef | |||
| f3f13a71dc | |||
| 5155423a1e | |||
| a7d558e48d | |||
| 7afd75c712 | |||
| 10a661ad4c | |||
| 201fe6f0df | |||
| f76a5b5f01 | |||
| 3a7e477e9b | |||
| b1aae244de | |||
| 7ebd8f13c2 | |||
| 1768d73c01 | |||
| ebcb894011 | |||
| 25cbdcb504 | |||
| 14ed45e833 | |||
| e365e0219e | |||
| 1531a558a5 | |||
| f19b7fa584 | |||
| c8ab7f7d42 |
+3
-2
@@ -4,6 +4,7 @@ variables:
|
|||||||
stages:
|
stages:
|
||||||
- buildAndDeployApkUnstable
|
- buildAndDeployApkUnstable
|
||||||
- buildAndDeployApkStable
|
- buildAndDeployApkStable
|
||||||
|
- buildAndDeployPlaystore
|
||||||
|
|
||||||
buildAndDeployApkUnstable:
|
buildAndDeployApkUnstable:
|
||||||
stage: buildAndDeployApkUnstable
|
stage: buildAndDeployApkUnstable
|
||||||
@@ -25,8 +26,8 @@ buildAndDeployApkStable:
|
|||||||
- branches
|
- branches
|
||||||
when: manual
|
when: manual
|
||||||
|
|
||||||
buildAndDeployApkStable:
|
buildAndDeployPlaystore:
|
||||||
stage: buildAndDeployApkStable
|
stage: buildAndDeployPlaystore
|
||||||
script:
|
script:
|
||||||
- sh deploy-playstore.sh
|
- sh deploy-playstore.sh
|
||||||
only:
|
only:
|
||||||
|
|||||||
+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
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -95,7 +95,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk 29
|
minSdk 28
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode gitVersionCode
|
versionCode gitVersionCode
|
||||||
versionName gitVersionName
|
versionName gitVersionName
|
||||||
|
|||||||
+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">
|
||||||
@@ -127,6 +148,10 @@
|
|||||||
android:name=".activities.ExceptionActivity"
|
android:name=".activities.ExceptionActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.CaptchaActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.LoginActivity"
|
android:name=".activities.LoginActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
@@ -178,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>
|
||||||
|
|||||||
@@ -217,6 +217,9 @@ function pluginUpdateTestPlugin(config) {
|
|||||||
}
|
}
|
||||||
function pluginLoginTestPlugin() {
|
function pluginLoginTestPlugin() {
|
||||||
return syncGET("/plugin/loginTestPlugin", {});
|
return syncGET("/plugin/loginTestPlugin", {});
|
||||||
|
}//captchaLoginTestPlugin
|
||||||
|
function pluginCaptchaTestPlugin(url, html) {
|
||||||
|
return syncPOST("/plugin/captchaTestPlugin?url=" + url, {}, html);
|
||||||
}
|
}
|
||||||
function pluginLogoutTestPlugin() {
|
function pluginLogoutTestPlugin() {
|
||||||
return syncGET("/plugin/logoutTestPlugin", {});
|
return syncGET("/plugin/logoutTestPlugin", {});
|
||||||
|
|||||||
@@ -681,6 +681,9 @@
|
|||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
|
captchaTestPlugin() {
|
||||||
|
captchaLoginTestPlugin();
|
||||||
|
},
|
||||||
logoutTestPlugin() {
|
logoutTestPlugin() {
|
||||||
pluginLogoutTestPlugin();
|
pluginLogoutTestPlugin();
|
||||||
},
|
},
|
||||||
@@ -838,6 +841,12 @@
|
|||||||
this.Testing.lastResultError = "";
|
this.Testing.lastResultError = "";
|
||||||
}
|
}
|
||||||
catch(ex) {
|
catch(ex) {
|
||||||
|
if(ex.plugin_type == "CaptchaRequiredException") {
|
||||||
|
let shouldCaptcha = confirm("Do you want to request captcha?");
|
||||||
|
if(shouldCaptcha) {
|
||||||
|
pluginCaptchaTestPlugin(ex.url, ex.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
console.error("Failed to run test for " + req.title, ex);
|
console.error("Failed to run test for " + req.title, ex);
|
||||||
this.Testing.lastResult = ""
|
this.Testing.lastResult = ""
|
||||||
if(ex.message)
|
if(ex.message)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,6 +70,19 @@ class ScriptException extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class CaptchaRequiredException extends Error {
|
||||||
|
constructor(url, body) {
|
||||||
|
super(JSON.stringify({ 'plugin_type': 'CaptchaRequiredException', url, body }));
|
||||||
|
this.plugin_type = "CaptchaRequiredException";
|
||||||
|
this.url = url;
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class CriticalException extends ScriptException {
|
||||||
|
constructor(msg) {
|
||||||
|
super("CriticalException", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
class UnavailableException extends ScriptException {
|
class UnavailableException extends ScriptException {
|
||||||
constructor(msg) {
|
constructor(msg) {
|
||||||
super("UnavailableException", msg);
|
super("UnavailableException", msg);
|
||||||
@@ -140,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 {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
|
||||||
|
fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||||
|
fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||||
|
fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
|
||||||
|
|
||||||
|
|
||||||
|
fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any());
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,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
|
||||||
@@ -27,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);
|
||||||
@@ -42,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", -2
|
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) {
|
||||||
@@ -56,14 +59,44 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
"Submit feedback", FieldForm.BUTTON,
|
R.string.show_faq, FieldForm.BUTTON,
|
||||||
"Give feedback on the application", -1
|
R.string.get_answers_to_common_questions, -4
|
||||||
)
|
)
|
||||||
|
@FormFieldButton(R.drawable.ic_quiz)
|
||||||
|
fun openFAQ() {
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
//Ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
@FormField(
|
||||||
|
R.string.submit_feedback, FieldForm.BUTTON,
|
||||||
|
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);
|
||||||
val subject = "Feedback Grayjay";
|
val subject = "Feedback Grayjay";
|
||||||
val body = "Hey,\n\nI have some feedback on the Grayjay app.\nVersion information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE}})\n\n";
|
val body = "Hey,\n\nI have some feedback on the Grayjay app.\nVersion 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";
|
||||||
val data = Uri.parse("mailto:grayjay@futo.org?subject=" + Uri.encode(subject) + "&body=" + Uri.encode(body));
|
val data = Uri.parse("mailto:grayjay@futo.org?subject=" + Uri.encode(subject) + "&body=" + Uri.encode(body));
|
||||||
i.data = data;
|
i.data = data;
|
||||||
|
|
||||||
@@ -71,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 {
|
||||||
@@ -87,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;
|
||||||
|
|
||||||
@@ -101,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)
|
||||||
@@ -125,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;
|
||||||
|
|
||||||
@@ -140,7 +220,17 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
return FeedStyle.THUMBNAIL;
|
return FeedStyle.THUMBNAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField("Background Update", FieldForm.DROPDOWN, "Experimental background update for subscriptions cache (requires restart)", 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)
|
||||||
|
var fetchOnAppBoot: Boolean = true;
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
@@ -156,26 +246,36 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@FormField("Subscription Concurrency", FieldForm.DROPDOWN, "Specify how many threads are used to fetch channels (requires restart)", 7)
|
@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) {
|
||||||
@@ -191,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, "Auto-rotate deadzone in 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;
|
||||||
|
|
||||||
@@ -221,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;
|
||||||
@@ -243,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;
|
||||||
|
|
||||||
@@ -263,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 {
|
||||||
@@ -285,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;
|
||||||
|
|
||||||
@@ -320,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.scopeGetter().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) {
|
||||||
@@ -349,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) {
|
||||||
@@ -391,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) {
|
||||||
@@ -406,19 +510,46 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@FormField("Auto Update", "group", "Configure the auto updater", 12)
|
@FormField(R.string.external_storage, FieldForm.GROUP, -1, 12)
|
||||||
|
var storage = Storage();
|
||||||
|
@Serializable
|
||||||
|
class Storage {
|
||||||
|
var storage_general: String? = null;
|
||||||
|
var storage_download: String? = null;
|
||||||
|
|
||||||
|
fun getStorageGeneralUri(): Uri? = storage_general?.let { Uri.parse(it) };
|
||||||
|
fun getStorageDownloadUri(): Uri? = storage_download?.let { Uri.parse(it) };
|
||||||
|
fun isStorageMainValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageGeneralUri());
|
||||||
|
fun isStorageDownloadValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageDownloadUri());
|
||||||
|
|
||||||
|
@FormField(R.string.change_external_general_directory, FieldForm.BUTTON, R.string.change_the_external_directory_for_general_files, 3)
|
||||||
|
fun changeStorageGeneral() {
|
||||||
|
SettingsActivity.getActivity()?.let {
|
||||||
|
StateApp.instance.changeExternalGeneralDirectory(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@FormField(R.string.change_external_downloads_directory, FieldForm.BUTTON, R.string.change_the_external_storage_for_download_files, 4)
|
||||||
|
fun changeStorageDownload() {
|
||||||
|
SettingsActivity.getActivity()?.let {
|
||||||
|
StateApp.instance.changeExternalDownloadDirectory(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
@@ -436,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) {
|
||||||
@@ -449,20 +580,21 @@ 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 {
|
||||||
StateApp.instance.scopeGetter().launch(Dispatchers.IO) {
|
UIDialogs.toast(it.getString(R.string.retrieving_changelog));
|
||||||
|
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val version = StateUpdate.instance.downloadVersionCode(ManagedHttpClient()) ?: return@launch;
|
val version = StateUpdate.instance.downloadVersionCode(ManagedHttpClient()) ?: return@launch;
|
||||||
Logger.i(TAG, "Version retrieved $version");
|
Logger.i(TAG, "Version retrieved $version");
|
||||||
@@ -478,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 {
|
||||||
@@ -496,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 {
|
||||||
@@ -506,55 +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()!!);
|
UIDialogs.showAutomaticBackupDialog(SettingsActivity.getActivity()!!, autoBackupPassword != null) {
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,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;
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import com.futo.platformplayer.casting.StateCasting
|
|||||||
import com.futo.platformplayer.dialogs.*
|
import com.futo.platformplayer.dialogs.*
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateBackup
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -90,11 +92,25 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showAutomaticBackupDialog(context: Context) {
|
fun showAutomaticBackupDialog(context: Context, skipRestoreCheck: Boolean = false, onClosed: (()->Unit)? = null) {
|
||||||
val dialog = AutomaticBackupDialog(context);
|
val dialogAction: ()->Unit = {
|
||||||
registerDialogOpened(dialog);
|
val dialog = AutomaticBackupDialog(context);
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
registerDialogOpened(dialog);
|
||||||
dialog.show();
|
dialog.setOnDismissListener { registerDialogClosed(dialog); onClosed?.invoke() };
|
||||||
|
dialog.show();
|
||||||
|
};
|
||||||
|
if(StateBackup.hasAutomaticBackup() && !skipRestoreCheck)
|
||||||
|
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(context.getString(R.string.cancel), {}), //To nothing
|
||||||
|
UIDialogs.Action(context.getString(R.string.override), {
|
||||||
|
dialogAction();
|
||||||
|
}, UIDialogs.ActionStyle.DANGEROUS),
|
||||||
|
UIDialogs.Action(context.getString(R.string.restore), {
|
||||||
|
UIDialogs.showAutomaticRestoreDialog(context, StateApp.instance.scope);
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
else {
|
||||||
|
dialogAction();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fun showAutomaticRestoreDialog(context: Context, scope: CoroutineScope) {
|
fun showAutomaticRestoreDialog(context: Context, scope: CoroutineScope) {
|
||||||
val dialog = AutomaticRestoreDialog(context, scope);
|
val dialog = AutomaticRestoreDialog(context, scope);
|
||||||
@@ -134,10 +150,10 @@ class UIDialogs {
|
|||||||
val buttonView = TextView(context);
|
val buttonView = TextView(context);
|
||||||
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt();
|
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt();
|
||||||
val dp28 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28f, resources.displayMetrics).toInt();
|
val dp28 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28f, resources.displayMetrics).toInt();
|
||||||
val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics);
|
val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics).toInt();
|
||||||
buttonView.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
buttonView.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||||
if(actions.size > 1)
|
if(actions.size > 1)
|
||||||
this.marginEnd = dp28;
|
this.marginEnd = if(actions.size > 2) dp14 else dp28;
|
||||||
};
|
};
|
||||||
buttonView.setTextColor(Color.WHITE);
|
buttonView.setTextColor(Color.WHITE);
|
||||||
buttonView.textSize = 14f;
|
buttonView.textSize = 14f;
|
||||||
@@ -151,8 +167,9 @@ class UIDialogs {
|
|||||||
ActionStyle.DANGEROUS_TEXT -> buttonView.setTextColor(ContextCompat.getColor(context, R.color.pastel_red))
|
ActionStyle.DANGEROUS_TEXT -> buttonView.setTextColor(ContextCompat.getColor(context, R.color.pastel_red))
|
||||||
else -> buttonView.setTextColor(ContextCompat.getColor(context, R.color.colorPrimary))
|
else -> buttonView.setTextColor(ContextCompat.getColor(context, R.color.colorPrimary))
|
||||||
}
|
}
|
||||||
|
val paddingSpecialButtons = if(actions.size > 2) dp14 else dp28;
|
||||||
if(act.style != ActionStyle.NONE && act.style != ActionStyle.DANGEROUS_TEXT)
|
if(act.style != ActionStyle.NONE && act.style != ActionStyle.DANGEROUS_TEXT)
|
||||||
buttonView.setPadding(dp28, dp10, dp28, dp10);
|
buttonView.setPadding(paddingSpecialButtons, dp10, paddingSpecialButtons, dp10);
|
||||||
else
|
else
|
||||||
buttonView.setPadding(dp10, dp10, dp10, dp10);
|
buttonView.setPadding(dp10, dp10, dp10, dp10);
|
||||||
|
|
||||||
@@ -194,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)
|
||||||
);
|
);
|
||||||
@@ -209,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,10 +1,13 @@
|
|||||||
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.http.server.handlers.HttpConstantHandler
|
|
||||||
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
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
@@ -17,7 +20,9 @@ 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.overlays.slideup.SlideUpMenuGroup
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
@@ -29,7 +34,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.lang.IllegalStateException
|
||||||
|
|
||||||
class UISlideOverlays {
|
class UISlideOverlays {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -45,7 +50,67 @@ class UISlideOverlays {
|
|||||||
menu.show();
|
menu.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showDownloadVideoOverlay(contentResolver: ContentResolver, video: IPlatformVideoDetails, container: ViewGroup): SlideUpMenuOverlay? {
|
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? {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
var menu: SlideUpMenuOverlay? = null;
|
var menu: SlideUpMenuOverlay? = null;
|
||||||
|
|
||||||
@@ -64,43 +129,49 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, "Video", videoSources,
|
if(!VideoHelper.isDownloadable(video)) {
|
||||||
listOf(listOf(SlideUpMenuItem(container.context, R.drawable.ic_movie, "None", "Audio Only", "none", {
|
Logger.i(TAG, "Attempted to open downloads without valid sources for [${video.name}]: ${video.url}");
|
||||||
|
UIDialogs.toast( container.context.getString(R.string.no_downloadable_sources_yet));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
||||||
|
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 is IVideoUrlSource }
|
.filter { it.isDownloadable() }
|
||||||
.map {
|
.map {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
||||||
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()
|
||||||
));
|
));
|
||||||
|
|
||||||
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.size > 0)
|
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.size > 0)
|
||||||
selectedVideo = VideoHelper.selectBestVideoSource(videoSources.filter { it is IVideoUrlSource }.asIterable(),
|
selectedVideo = VideoHelper.selectBestVideoSource(videoSources.filter { it.isDownloadable() }.asIterable(),
|
||||||
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
||||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) as IVideoUrlSource;
|
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) as IVideoUrlSource;
|
||||||
|
|
||||||
|
|
||||||
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 { it is IAudioUrlSource }
|
.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;
|
||||||
@@ -111,26 +182,29 @@ class UISlideOverlays {
|
|||||||
menu?.selectOption(asources, preferredAudioSource);
|
menu?.selectOption(asources, preferredAudioSource);
|
||||||
|
|
||||||
|
|
||||||
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource }.asIterable(),
|
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it.isDownloadable() }.asIterable(),
|
||||||
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
||||||
Settings.instance.playback.getPrimaryLanguage(container.context),
|
Settings.instance.playback.getPrimaryLanguage(container.context),
|
||||||
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
|
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
|
||||||
}
|
}
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, "Subtitles", subtitleSources, subtitleSources
|
//ContentResolver is required for subtitles..
|
||||||
.map {
|
if(contentResolver != null) {
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources
|
||||||
if (selectedSubtitle == it) {
|
.map {
|
||||||
selectedSubtitle = null;
|
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
|
||||||
menu?.selectOption(subtitleSources, null);
|
if (selectedSubtitle == it) {
|
||||||
} else {
|
selectedSubtitle = null;
|
||||||
selectedSubtitle = it;
|
menu?.selectOption(subtitleSources, null);
|
||||||
menu?.selectOption(subtitleSources, it);
|
} else {
|
||||||
}
|
selectedSubtitle = it;
|
||||||
}, false);
|
menu?.selectOption(subtitleSources, it);
|
||||||
}));
|
}
|
||||||
|
}, false);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -139,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 {
|
||||||
@@ -153,29 +227,12 @@ class UISlideOverlays {
|
|||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val subtitleUri = subtitleToDownload.getSubtitlesURI();
|
val subtitleUri = subtitleToDownload.getSubtitlesURI();
|
||||||
if (subtitleUri != null) {
|
//TODO: Remove uri dependency, should be able to work with raw aswell?
|
||||||
var subtitles: String? = null;
|
if (subtitleUri != null && contentResolver != null) {
|
||||||
if ("file" == subtitleUri.scheme) {
|
val subtitlesRaw = StateDownloads.instance.downloadSubtitles(subtitleToDownload, contentResolver);
|
||||||
val inputStream = contentResolver.openInputStream(subtitleUri);
|
|
||||||
inputStream?.use { stream ->
|
|
||||||
val reader = stream.bufferedReader();
|
|
||||||
subtitles = reader.use { it.readText() };
|
|
||||||
}
|
|
||||||
} else if ("http" == subtitleUri.scheme || "https" == subtitleUri.scheme) {
|
|
||||||
val client = ManagedHttpClient();
|
|
||||||
val subtitleResponse = client.get(subtitleUri.toString());
|
|
||||||
if (!subtitleResponse.isOk) {
|
|
||||||
throw Exception("Cannot fetch subtitles from source '${subtitleUri}': ${subtitleResponse.code}");
|
|
||||||
}
|
|
||||||
|
|
||||||
subtitles = subtitleResponse.body?.toString()
|
|
||||||
?: throw Exception("Subtitles are invalid '${subtitleUri}': ${subtitleResponse.code}");
|
|
||||||
} else {
|
|
||||||
throw Exception("Unsuported scheme");
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
StateDownloads.instance.download(video, selectedVideo, selectedAudio, if (subtitles != null) SubtitleRawSource(subtitleToDownload.name, subtitleToDownload.format, subtitles!!) else null);
|
StateDownloads.instance.download(video, selectedVideo, selectedAudio, subtitlesRaw);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@@ -191,13 +248,44 @@ class UISlideOverlays {
|
|||||||
};
|
};
|
||||||
return menu.apply { show() };
|
return menu.apply { show() };
|
||||||
}
|
}
|
||||||
fun showDownloadVideoOverlay(video: IPlatformVideo, container: ViewGroup) {
|
fun showDownloadVideoOverlay(video: IPlatformVideo, container: ViewGroup, useDetails: Boolean = false) {
|
||||||
showUnknownVideoDownload("Video", container) { px, bitrate ->
|
val handleUnknownDownload: ()->Unit = {
|
||||||
StateDownloads.instance.download(video, px, bitrate)
|
showUnknownVideoDownload(container.context.getString(R.string.video), container) { px, bitrate ->
|
||||||
|
StateDownloads.instance.download(video, px, bitrate)
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
if(!useDetails)
|
||||||
|
handleUnknownDownload();
|
||||||
|
else {
|
||||||
|
val scope = StateApp.instance.scopeOrNull;
|
||||||
|
|
||||||
|
if(scope != null) {
|
||||||
|
val loader = showLoaderOverlay(container.context.getString(R.string.fetching_video_details), container);
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val videoDetails = StatePlatform.instance.getContentDetails(video.url, false).await();
|
||||||
|
if(videoDetails !is IPlatformVideoDetails)
|
||||||
|
throw IllegalStateException("Not a video details");
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if(showDownloadVideoOverlay(videoDetails, container, StateApp.instance.contextOrNull?.contentResolver) == null)
|
||||||
|
loader.hide(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.toast(container.context.getString(R.string.failed_to_fetch_details_for_download));
|
||||||
|
handleUnknownDownload();
|
||||||
|
loader.hide(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else handleUnknownDownload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -209,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),
|
||||||
@@ -217,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)
|
||||||
)));
|
)));
|
||||||
|
|
||||||
@@ -254,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 {
|
||||||
@@ -269,14 +357,26 @@ class UISlideOverlays {
|
|||||||
menu.show();
|
menu.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, onVideoHidden: (()->Unit)? = null): SlideUpMenuOverlay {
|
fun showLoaderOverlay(text: String, container: ViewGroup): SlideUpMenuOverlay {
|
||||||
|
val dp70 = 70.dp(container.context.resources);
|
||||||
|
val dp15 = 15.dp(container.context.resources);
|
||||||
|
val overlay = SlideUpMenuOverlay(container.context, container, text, null, true, listOf(
|
||||||
|
Loader(container.context, true, dp70).apply {
|
||||||
|
this.setPadding(0, dp15, 0, dp15);
|
||||||
|
}
|
||||||
|
), true);
|
||||||
|
overlay.show();
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
@@ -287,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); }, 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();
|
||||||
@@ -311,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() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -325,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();
|
||||||
@@ -338,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); }, 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();
|
||||||
@@ -357,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 {
|
||||||
@@ -377,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 }
|
||||||
@@ -390,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) {
|
||||||
@@ -398,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))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.content.Intent
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.icu.util.Output
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.OperationCanceledException
|
import android.os.OperationCanceledException
|
||||||
@@ -15,8 +16,12 @@ import android.view.WindowInsetsController
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.PlatformMultiClientPool
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.PlatformVideoWithTime
|
import com.futo.platformplayer.models.PlatformVideoWithTime
|
||||||
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
||||||
@@ -51,6 +56,11 @@ fun findNonRuntimeException(ex: Throwable?): Throwable? {
|
|||||||
return ex;
|
return ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun warnIfMainThread(context: String) {
|
||||||
|
if(BuildConfig.DEBUG && Looper.myLooper() == Looper.getMainLooper())
|
||||||
|
Logger.w(V8Plugin.TAG, "JAVASCRIPT ON MAIN THREAD\nAt: ${context}\n" + Thread.currentThread().stackTrace.joinToString { it.toString() });
|
||||||
|
}
|
||||||
|
|
||||||
fun ensureNotMainThread() {
|
fun ensureNotMainThread() {
|
||||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||||
Logger.e("Utility", "Throwing exception because a function that should not be called on main thread, is called on main thread")
|
Logger.e("Utility", "Throwing exception because a function that should not be called on main thread, is called on main thread")
|
||||||
@@ -58,13 +68,31 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun IPlatformClient.fromPool(pool: PlatformMultiClientPool) = pool.getClientPooled(this);
|
||||||
|
|
||||||
fun IPlatformVideo.withTimestamp(sec: Long) = PlatformVideoWithTime(this, sec);
|
fun IPlatformVideo.withTimestamp(sec: Long) = PlatformVideoWithTime(this, sec);
|
||||||
|
|
||||||
|
fun DocumentFile.getInputStream(context: Context) = context.contentResolver.openInputStream(this.uri);
|
||||||
|
fun DocumentFile.getOutputStream(context: Context) = context.contentResolver.openOutputStream(this.uri);
|
||||||
|
fun DocumentFile.copyTo(context: Context, file: DocumentFile) = this.getInputStream(context).use { input ->
|
||||||
|
file.getOutputStream(context)?.use { output -> input?.copyTo(output) }
|
||||||
|
};
|
||||||
|
fun DocumentFile.readBytes(context: Context) = this.getInputStream(context).use { input -> input?.readBytes() };
|
||||||
|
fun DocumentFile.writeBytes(context: Context, byteArray: ByteArray) = context.contentResolver.openOutputStream(this.uri)?.use {
|
||||||
|
it.write(byteArray);
|
||||||
|
it.flush();
|
||||||
|
};
|
||||||
|
|
||||||
fun loadBitmap(url: String): Bitmap {
|
fun loadBitmap(url: String): Bitmap {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private val _client = ManagedHttpClient();
|
private val _client = ManagedHttpClient();
|
||||||
|
|
||||||
private var _config : SourcePluginConfig? = null;
|
private var _config: SourcePluginConfig? = null;
|
||||||
|
private var _script: String? = null;
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -74,14 +75,14 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
_buttonInstall = findViewById(R.id.button_install);
|
_buttonInstall = findViewById(R.id.button_install);
|
||||||
|
|
||||||
_buttonBack.setOnClickListener {
|
_buttonBack.setOnClickListener {
|
||||||
onBackPressed();
|
finish();
|
||||||
};
|
};
|
||||||
_buttonCancel.setOnClickListener {
|
_buttonCancel.setOnClickListener {
|
||||||
onBackPressed();
|
finish();
|
||||||
}
|
}
|
||||||
_buttonInstall.setOnClickListener {
|
_buttonInstall.setOnClickListener {
|
||||||
_config?.let {
|
_config?.let {
|
||||||
install(_config!!);
|
install(_config!!, _script!!);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,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);
|
||||||
@@ -114,6 +115,7 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val config: SourcePluginConfig;
|
||||||
try {
|
try {
|
||||||
val configResp = _client.get(url);
|
val configResp = _client.get(url);
|
||||||
if(!configResp.isOk)
|
if(!configResp.isOk)
|
||||||
@@ -121,33 +123,51 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
val configJson = configResp.body?.string();
|
val configJson = configResp.body?.string();
|
||||||
if(configJson.isNullOrEmpty())
|
if(configJson.isNullOrEmpty())
|
||||||
throw IllegalStateException("No response");
|
throw IllegalStateException("No response");
|
||||||
val config = SourcePluginConfig.fromJson(configJson, url);
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
config = SourcePluginConfig.fromJson(configJson, url);
|
||||||
loadConfig(config);
|
} catch(ex: SerializationException) {
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(ex: SerializationException) {
|
|
||||||
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;
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
val script: String?
|
||||||
|
try {
|
||||||
|
val scriptResp = _client.get(config.absoluteScriptUrl);
|
||||||
|
if (!scriptResp.isOk)
|
||||||
|
throw IllegalStateException("script not available [${scriptResp.code}]");
|
||||||
|
script = scriptResp.body?.string();
|
||||||
|
if (script.isNullOrEmpty())
|
||||||
|
throw IllegalStateException("script empty");
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Logger.e(TAG, "Failed fetch script", ex);
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.showGeneralErrorDialog(this@AddSourceActivity, getString(R.string.failed_to_fetch_script), ex);
|
||||||
|
};
|
||||||
|
return@launch;
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
loadConfig(config, script);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadConfig(config: SourcePluginConfig) {
|
private fun loadConfig(config: SourcePluginConfig, script: String) {
|
||||||
_config = config;
|
_config = config;
|
||||||
|
_script = script;
|
||||||
|
|
||||||
_sourceHeader.loadConfig(config);
|
_sourceHeader.loadConfig(config, script);
|
||||||
_sourcePermissions.removeAllViews();
|
_sourcePermissions.removeAllViews();
|
||||||
_sourceWarnings.removeAllViews();
|
_sourceWarnings.removeAllViews();
|
||||||
|
|
||||||
@@ -155,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)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -164,14 +184,14 @@ 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)
|
||||||
)
|
)
|
||||||
|
|
||||||
val pastelRed = resources.getColor(R.color.pastel_red);
|
val pastelRed = resources.getColor(R.color.pastel_red);
|
||||||
|
|
||||||
for(warning in config.getWarnings())
|
for(warning in config.getWarnings(script))
|
||||||
_sourceWarnings.addView(
|
_sourceWarnings.addView(
|
||||||
SourceInfoView(this,
|
SourceInfoView(this,
|
||||||
R.drawable.ic_security_pred,
|
R.drawable.ic_security_pred,
|
||||||
@@ -182,8 +202,8 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun install(config: SourcePluginConfig) {
|
fun install(config: SourcePluginConfig, script: String) {
|
||||||
StatePlugins.instance.installPlugin(this, lifecycleScope, config) {
|
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
||||||
if(it)
|
if(it)
|
||||||
backToSources();
|
backToSources();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
@@ -13,6 +16,32 @@ 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 ->
|
||||||
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
|
scanResult?.let {
|
||||||
|
val content = it.contents
|
||||||
|
if (content == null) {
|
||||||
|
UIDialogs.toast(this, getString(R.string.failed_to_scan_qr_code))
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = if (content.startsWith("https://")) {
|
||||||
|
content
|
||||||
|
} else if (content.startsWith("grayjay://plugin/")) {
|
||||||
|
content.substring("grayjay://plugin/".length)
|
||||||
|
} else {
|
||||||
|
UIDialogs.toast(this, getString(R.string.not_a_plugin_url))
|
||||||
|
return@let;
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
||||||
|
data = Uri.parse(url);
|
||||||
|
};
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -23,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();
|
||||||
@@ -31,21 +61,17 @@ 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)
|
||||||
integrator.setBarcodeImageEnabled(true)
|
integrator.setBarcodeImageEnabled(true)
|
||||||
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
||||||
integrator.initiateScan()
|
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonURL.onClick.subscribe {
|
_buttonURL.onClick.subscribe {
|
||||||
UIDialogs.toast(this, "Not implemented yet..");
|
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class QRCaptureActivity: CaptureActivity() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.widget.Button
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginCaptchaConfig
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.others.CaptchaWebViewClient
|
||||||
|
import com.futo.platformplayer.others.LoginWebViewClient
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.io.File
|
||||||
|
import java.lang.Exception
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class CaptchaActivity : AppCompatActivity() {
|
||||||
|
private lateinit var _webView: WebView;
|
||||||
|
private lateinit var _buttonClose: Button;
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_captcha);
|
||||||
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
|
_buttonClose = findViewById(R.id.button_close);
|
||||||
|
_buttonClose.setOnClickListener { finish(); };
|
||||||
|
|
||||||
|
_webView = findViewById(R.id.web_view);
|
||||||
|
_webView.settings.javaScriptEnabled = true;
|
||||||
|
CookieManager.getInstance().setAcceptCookie(true);
|
||||||
|
|
||||||
|
|
||||||
|
val config = if(intent.hasExtra("plugin"))
|
||||||
|
Json.decodeFromString<SourcePluginConfig>(intent.getStringExtra("plugin")!!);
|
||||||
|
else null;
|
||||||
|
|
||||||
|
val captchaConfig = if(config != null)
|
||||||
|
config.captcha ?: throw IllegalStateException("Plugin has no captcha support");
|
||||||
|
else if(intent.hasExtra("captcha"))
|
||||||
|
Json.decodeFromString<SourcePluginCaptchaConfig>(intent.getStringExtra("captcha")!!);
|
||||||
|
else throw IllegalStateException("No valid configuration?");
|
||||||
|
//TODO: Backwards compat removal?
|
||||||
|
|
||||||
|
val extraUrl = if (intent.hasExtra("url"))
|
||||||
|
intent.getStringExtra("url");
|
||||||
|
else null;
|
||||||
|
|
||||||
|
val extraBody = if (intent.hasExtra("body"))
|
||||||
|
intent.getStringExtra("body");
|
||||||
|
else null;
|
||||||
|
|
||||||
|
_webView.settings.userAgentString = captchaConfig.userAgent ?: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36";
|
||||||
|
_webView.settings.useWideViewPort = true;
|
||||||
|
_webView.settings.loadWithOverviewMode = true;
|
||||||
|
|
||||||
|
val webViewClient = if(config != null) CaptchaWebViewClient(config) else CaptchaWebViewClient(captchaConfig);
|
||||||
|
webViewClient.onCaptchaFinished.subscribe { captcha ->
|
||||||
|
_callback?.let {
|
||||||
|
_callback = null;
|
||||||
|
it.invoke(captcha);
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
};
|
||||||
|
_webView.settings.domStorageEnabled = true;
|
||||||
|
_webView.webViewClient = webViewClient;
|
||||||
|
|
||||||
|
if(captchaConfig.captchaUrl != null)
|
||||||
|
_webView.loadUrl(captchaConfig.captchaUrl);
|
||||||
|
else if(extraUrl != null && extraBody != null)
|
||||||
|
_webView.loadDataWithBaseURL(extraUrl, extraBody, "text/html", "utf-8", null);
|
||||||
|
else if(extraUrl != null)
|
||||||
|
_webView.loadUrl(extraUrl);
|
||||||
|
else throw IllegalStateException("No valid captcha info provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finish() {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
_webView.loadUrl("about:blank");
|
||||||
|
}
|
||||||
|
_callback?.let {
|
||||||
|
_callback = null;
|
||||||
|
it.invoke(null);
|
||||||
|
}
|
||||||
|
super.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "CaptchaActivity";
|
||||||
|
private var _callback: ((SourceCaptchaData?) -> Unit)? = null;
|
||||||
|
|
||||||
|
private fun getCaptchaIntent(context: Context, config: SourcePluginConfig, url: String? = null, body: String? = null): Intent {
|
||||||
|
val intent = Intent(context, CaptchaActivity::class.java);
|
||||||
|
if(url != null)
|
||||||
|
intent.putExtra("url", url);
|
||||||
|
if(body != null)
|
||||||
|
intent.putExtra("body", body);
|
||||||
|
intent.putExtra("plugin", Json.encodeToString(config));
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showCaptcha(context: Context, config: SourcePluginConfig, url: String? = null, body: String? = null, callback: ((SourceCaptchaData?) -> Unit)? = null) {
|
||||||
|
_callback = callback;
|
||||||
|
context.startActivity(getCaptchaIntent(context, config, url, body));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
@@ -37,10 +38,11 @@ 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\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" +
|
||||||
Logging.buildLogString(LogLevel.ERROR, TAG, "Uncaught exception (\"$context\"): $stack");
|
Logging.buildLogString(LogLevel.ERROR, TAG, "Uncaught exception (\"$context\"): $stack");
|
||||||
try {
|
try {
|
||||||
val file = File(filesDir, "log.txt");
|
val file = File(filesDir, "log.txt");
|
||||||
@@ -77,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,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
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
|
||||||
|
interface IWithResultLauncher {
|
||||||
|
fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit);
|
||||||
|
}
|
||||||
@@ -3,8 +3,12 @@ package com.futo.platformplayer.activities
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.webkit.ConsoleMessage
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
|
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.*
|
||||||
@@ -21,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?) {
|
||||||
@@ -28,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);
|
||||||
@@ -58,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;
|
||||||
@@ -68,9 +83,15 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
|
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//TODO: Required for some...TBD what to do with it. Clear on finish?
|
|
||||||
_webView.settings.domStorageEnabled = true;
|
_webView.settings.domStorageEnabled = true;
|
||||||
|
|
||||||
|
/*
|
||||||
|
_webView.webChromeClient = object: WebChromeClient() {
|
||||||
|
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||||
|
Logger.w(TAG, "Login Console: " + consoleMessage?.message());
|
||||||
|
return super.onConsoleMessage(consoleMessage);
|
||||||
|
}
|
||||||
|
}*/
|
||||||
_webView.webViewClient = webViewClient;
|
_webView.webViewClient = webViewClient;
|
||||||
_webView.loadUrl(authConfig.loginUrl);
|
_webView.loadUrl(authConfig.loginUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,13 @@ 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
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
@@ -24,6 +28,7 @@ import com.futo.platformplayer.api.media.PlatformID
|
|||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.constructs.Event3
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.*
|
import com.futo.platformplayer.fragment.mainactivity.main.*
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
||||||
@@ -48,7 +53,7 @@ import java.io.StringWriter
|
|||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity {
|
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
|
|
||||||
//TODO: Move to dimensions
|
//TODO: Move to dimensions
|
||||||
private val HEIGHT_MENU_DP = 48f;
|
private val HEIGHT_MENU_DP = 48f;
|
||||||
@@ -150,6 +155,11 @@ class MainActivity : AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -364,6 +374,7 @@ class MainActivity : AppCompatActivity {
|
|||||||
//startActivity(Intent(this, TestActivity::class.java));
|
//startActivity(Intent(this, TestActivity::class.java));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
@@ -387,7 +398,7 @@ class MainActivity : AppCompatActivity {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
Logger.i(TAG, "onResume")
|
Logger.v(TAG, "onResume")
|
||||||
|
|
||||||
val curOrientation = _orientationManager.orientation;
|
val curOrientation = _orientationManager.orientation;
|
||||||
|
|
||||||
@@ -403,13 +414,10 @@ class MainActivity : AppCompatActivity {
|
|||||||
val videoToOpen = StateSaved.instance.videoToOpen;
|
val videoToOpen = StateSaved.instance.videoToOpen;
|
||||||
|
|
||||||
if (_wasStopped) {
|
if (_wasStopped) {
|
||||||
Logger.i(TAG, "_wasStopped is true");
|
|
||||||
Logger.i(TAG, "set _wasStopped = false");
|
|
||||||
_wasStopped = false;
|
_wasStopped = false;
|
||||||
|
|
||||||
Logger.i(TAG, "onResume videoToOpen=$videoToOpen");
|
|
||||||
|
|
||||||
if (videoToOpen != null && _fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
|
if (videoToOpen != null && _fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
|
||||||
|
Logger.i(TAG, "onResume videoToOpen=$videoToOpen");
|
||||||
if (StatePlatform.instance.hasEnabledVideoClient(videoToOpen.url)) {
|
if (StatePlatform.instance.hasEnabledVideoClient(videoToOpen.url)) {
|
||||||
navigate(_fragVideoDetail, UrlVideoWithTime(videoToOpen.url, videoToOpen.timeSeconds, false));
|
navigate(_fragVideoDetail, UrlVideoWithTime(videoToOpen.url, videoToOpen.timeSeconds, false));
|
||||||
_fragVideoDetail.maximizeVideoDetail(true);
|
_fragVideoDetail.maximizeVideoDetail(true);
|
||||||
@@ -422,13 +430,13 @@ class MainActivity : AppCompatActivity {
|
|||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
Logger.i(TAG, "onPause")
|
Logger.v(TAG, "onPause")
|
||||||
_isVisible = false;
|
_isVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
Logger.i(TAG, "_wasStopped = true");
|
Logger.v(TAG, "_wasStopped = true");
|
||||||
_wasStopped = true;
|
_wasStopped = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,6 +465,10 @@ class MainActivity : AppCompatActivity {
|
|||||||
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" -> {
|
||||||
@@ -476,13 +488,13 @@ class MainActivity : AppCompatActivity {
|
|||||||
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/")) {
|
||||||
@@ -491,13 +503,21 @@ class MainActivity : AppCompatActivity {
|
|||||||
};
|
};
|
||||||
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",
|
||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
@@ -507,7 +527,7 @@ class MainActivity : AppCompatActivity {
|
|||||||
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",
|
||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
@@ -517,7 +537,7 @@ class MainActivity : AppCompatActivity {
|
|||||||
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",
|
||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
@@ -527,7 +547,7 @@ class MainActivity : AppCompatActivity {
|
|||||||
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",
|
||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
@@ -536,7 +556,7 @@ class MainActivity : AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,6 +597,9 @@ class MainActivity : AppCompatActivity {
|
|||||||
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 {
|
||||||
@@ -594,6 +617,9 @@ class MainActivity : AppCompatActivity {
|
|||||||
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) {
|
||||||
@@ -601,10 +627,11 @@ class MainActivity : AppCompatActivity {
|
|||||||
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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
val name = when(type) {
|
val name = when(type) {
|
||||||
"Playlist" -> recon.split("\n").filter { !it.startsWith(ManagedStore.RECONSTRUCTION_HEADER_OPERATOR) }.firstOrNull() ?: type;
|
"Playlist" -> recon.split("\n").filter { !it.startsWith(ManagedStore.RECONSTRUCTION_HEADER_OPERATOR) }.firstOrNull() ?: type;
|
||||||
else -> type
|
else -> type
|
||||||
@@ -618,6 +645,20 @@ class MainActivity : AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -643,7 +684,7 @@ class MainActivity : AppCompatActivity {
|
|||||||
}
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -717,22 +758,20 @@ class MainActivity : AppCompatActivity {
|
|||||||
}
|
}
|
||||||
_fragVideoDetail.onOrientationChanged(OrientationManager.Orientation.PORTRAIT);
|
_fragVideoDetail.onOrientationChanged(OrientationManager.Orientation.PORTRAIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "onRestart5");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
||||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
|
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
|
||||||
|
|
||||||
val isStop: Boolean = lifecycle.currentState == Lifecycle.State.CREATED;
|
val isStop: Boolean = lifecycle.currentState == Lifecycle.State.CREATED;
|
||||||
Logger.i(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop")
|
Logger.v(TAG, "onPictureInPictureModeChanged isInPictureInPictureMode=$isInPictureInPictureMode isStop=$isStop")
|
||||||
_fragVideoDetail?.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig);
|
_fragVideoDetail?.onPictureInPictureModeChanged(isInPictureInPictureMode, isStop, newConfig);
|
||||||
Logger.i(TAG, "onPictureInPictureModeChanged Ready");
|
Logger.v(TAG, "onPictureInPictureModeChanged Ready");
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
Logger.i(TAG, "onDestroy")
|
Logger.v(TAG, "onDestroy")
|
||||||
|
|
||||||
_orientationManager.disable();
|
_orientationManager.disable();
|
||||||
|
|
||||||
@@ -740,6 +779,9 @@ class MainActivity : AppCompatActivity {
|
|||||||
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
|
||||||
@@ -892,6 +934,28 @@ class MainActivity : AppCompatActivity {
|
|||||||
_fragContainerMain.setPadding(0,0,0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom, resources.displayMetrics).toInt());
|
_fragContainerMain.setPadding(0,0,0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom, resources.displayMetrics).toInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers.
|
||||||
|
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
|
||||||
|
private var requestCode: Int? = -1;
|
||||||
|
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
result: ActivityResult ->
|
||||||
|
val handler = synchronized(resultLauncherMap) {
|
||||||
|
resultLauncherMap.remove(requestCode);
|
||||||
|
}
|
||||||
|
if(handler != null)
|
||||||
|
handler(result);
|
||||||
|
};
|
||||||
|
override fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit) {
|
||||||
|
synchronized(resultLauncherMap) {
|
||||||
|
resultLauncherMap[code] = handler;
|
||||||
|
}
|
||||||
|
requestCode = code;
|
||||||
|
resultLauncher.launch(intent);
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "MainActivity"
|
private val TAG = "MainActivity"
|
||||||
|
|
||||||
@@ -902,5 +966,12 @@ class MainActivity : AppCompatActivity {
|
|||||||
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));
|
||||||
|
|||||||
+25
-20
@@ -5,6 +5,7 @@ import android.os.Bundle
|
|||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
@@ -14,6 +15,7 @@ import com.futo.platformplayer.setNavigationBarColorAndIcons
|
|||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.polycentric.core.*
|
import com.futo.polycentric.core.*
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
|
import com.journeyapps.barcodescanner.CaptureActivity
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -27,6 +29,16 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonImportProfile: LinearLayout;
|
private lateinit var _buttonImportProfile: LinearLayout;
|
||||||
private lateinit var _editProfile: EditText;
|
private lateinit var _editProfile: EditText;
|
||||||
|
|
||||||
|
private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||||
|
scanResult?.let {
|
||||||
|
if (it.contents != null) {
|
||||||
|
val scannedUrl = it.contents
|
||||||
|
import(scannedUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_polycentric_import_profile);
|
setContentView(R.layout.activity_polycentric_import_profile);
|
||||||
@@ -45,15 +57,20 @@ 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.initiateScan();
|
integrator.setOrientationLocked(true);
|
||||||
|
integrator.setCameraId(0)
|
||||||
|
integrator.setBeepEnabled(false)
|
||||||
|
integrator.setBarcodeImageEnabled(true)
|
||||||
|
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
||||||
|
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||||
};
|
};
|
||||||
|
|
||||||
_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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,21 +83,9 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
|
|
||||||
if (result != null) {
|
|
||||||
if (result.contents != null) {
|
|
||||||
val scannedUrl = result.contents;
|
|
||||||
import(scannedUrl);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +124,7 @@ 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}'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+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,26 +1,39 @@
|
|||||||
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
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
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
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
||||||
private lateinit var _form: FieldForm;
|
private lateinit var _form: FieldForm;
|
||||||
private lateinit var _buttonBack: ImageButton;
|
private lateinit var _buttonBack: ImageButton;
|
||||||
|
private lateinit var _loader: Loader;
|
||||||
|
|
||||||
private lateinit var _devSets: LinearLayout;
|
private lateinit var _devSets: LinearLayout;
|
||||||
private lateinit var _buttonDev: MaterialButton;
|
private lateinit var _buttonDev: MaterialButton;
|
||||||
|
|
||||||
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);
|
||||||
@@ -30,11 +43,17 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
_buttonBack = findViewById(R.id.button_back);
|
_buttonBack = findViewById(R.id.button_back);
|
||||||
_buttonDev = findViewById(R.id.button_dev);
|
_buttonDev = findViewById(R.id.button_dev);
|
||||||
_devSets = findViewById(R.id.dev_settings);
|
_devSets = findViewById(R.id.dev_settings);
|
||||||
|
_loader = findViewById(R.id.loader);
|
||||||
|
|
||||||
_form.fromObject(Settings.instance);
|
|
||||||
_form.onChanged.subscribe { field, value ->
|
_form.onChanged.subscribe { field, value ->
|
||||||
|
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();
|
||||||
@@ -44,18 +63,28 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
startActivity(Intent(this, DeveloperActivity::class.java));
|
startActivity(Intent(this, DeveloperActivity::class.java));
|
||||||
}
|
}
|
||||||
|
|
||||||
var devCounter = 0;
|
|
||||||
_form.findField("code")?.assume<ReadOnlyTextField>()?.setOnClickListener {
|
|
||||||
devCounter++;
|
|
||||||
if(devCounter > 5) {
|
|
||||||
devCounter = 0;
|
|
||||||
SettingsDev.instance.developerMode = true;
|
|
||||||
SettingsDev.instance.save();
|
|
||||||
updateDevMode();
|
|
||||||
UIDialogs.toast(this, "You are now in developer mode");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
_lastActivity = this;
|
_lastActivity = this;
|
||||||
|
|
||||||
|
reloadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reloadSettings() {
|
||||||
|
_loader.start();
|
||||||
|
_form.fromObject(lifecycleScope, Settings.instance) {
|
||||||
|
_loader.stop();
|
||||||
|
|
||||||
|
var devCounter = 0;
|
||||||
|
_form.findField("code")?.assume<ReadOnlyTextField>()?.setOnClickListener {
|
||||||
|
devCounter++;
|
||||||
|
if(devCounter > 5) {
|
||||||
|
devCounter = 0;
|
||||||
|
SettingsDev.instance.developerMode = true;
|
||||||
|
SettingsDev.instance.save();
|
||||||
|
updateDevMode();
|
||||||
|
UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -78,6 +107,28 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
|
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
|
||||||
|
private var requestCode: Int? = -1;
|
||||||
|
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
result: ActivityResult ->
|
||||||
|
val handler = synchronized(resultLauncherMap) {
|
||||||
|
resultLauncherMap.remove(requestCode);
|
||||||
|
}
|
||||||
|
if(handler != null)
|
||||||
|
handler(result);
|
||||||
|
};
|
||||||
|
override fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit) {
|
||||||
|
synchronized(resultLauncherMap) {
|
||||||
|
resultLauncherMap[code] = handler;
|
||||||
|
}
|
||||||
|
requestCode = code;
|
||||||
|
resultLauncher.launch(intent);
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
//TODO: Temporary for solving Settings issues
|
//TODO: Temporary for solving Settings issues
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -28,7 +29,11 @@ open class ManagedHttpClient {
|
|||||||
|
|
||||||
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
|
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
|
||||||
_builderTemplate = builder;
|
_builderTemplate = builder;
|
||||||
client = builder.build();
|
client = builder.addNetworkInterceptor { chain ->
|
||||||
|
val request = beforeRequest(chain.request());
|
||||||
|
val response = afterRequest(chain.proceed(request));
|
||||||
|
return@addNetworkInterceptor response;
|
||||||
|
}.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun clone(): ManagedHttpClient {
|
open fun clone(): ManagedHttpClient {
|
||||||
@@ -116,7 +121,7 @@ open class ManagedHttpClient {
|
|||||||
fun execute(request : Request) : Response {
|
fun execute(request : Request) : Response {
|
||||||
ensureNotMainThread();
|
ensureNotMainThread();
|
||||||
|
|
||||||
beforeRequest(request);
|
//beforeRequest(request);
|
||||||
|
|
||||||
Logger.v(TAG, "HTTP Request [${request.method}] ${request.url} - [${if(request.body != null) request.body.size else 0}]");
|
Logger.v(TAG, "HTTP Request [${request.method}] ${request.url} - [${if(request.body != null) request.body.size else 0}]");
|
||||||
|
|
||||||
@@ -156,23 +161,16 @@ open class ManagedHttpClient {
|
|||||||
if(true)
|
if(true)
|
||||||
Logger.v(TAG, "HTTP Response [${request.method}] ${request.url} - [${time}ms]");
|
Logger.v(TAG, "HTTP Response [${request.method}] ${request.url} - [${time}ms]");
|
||||||
|
|
||||||
afterRequest(request, resp);
|
//afterRequest(request, resp);
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Set Listeners
|
//Set Listeners
|
||||||
fun setOnBeforeRequest(listener : (Request)->Unit) {
|
open fun beforeRequest(request: okhttp3.Request): okhttp3.Request {
|
||||||
this.onBeforeRequest = listener;
|
return request;
|
||||||
}
|
}
|
||||||
fun setOnAfterRequest(listener : (Request, Response)->Unit) {
|
open fun afterRequest(resp: okhttp3.Response): okhttp3.Response {
|
||||||
this.onAfterRequest = listener;
|
return resp;
|
||||||
}
|
|
||||||
|
|
||||||
open fun beforeRequest(request: Request) {
|
|
||||||
onBeforeRequest?.invoke(request);
|
|
||||||
}
|
|
||||||
open fun afterRequest(request: Request, resp: Response) {
|
|
||||||
onAfterRequest?.invoke(request, resp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
|
|
||||||
Logger.i(TAG, "Started ${port}. \n" + getAddresses().map { it.hostAddress }.joinToString("\n"));
|
Logger.i(TAG, "Started HTTP Server ${port}. \n" + getAddresses().map { it.hostAddress }.joinToString("\n"));
|
||||||
}
|
}
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun stop() {
|
fun stop() {
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -94,7 +94,10 @@ class LiveChatManager {
|
|||||||
if(_pager is JSLiveEventPager)
|
if(_pager is JSLiveEventPager)
|
||||||
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
|
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
|
||||||
|
|
||||||
Logger.i(TAG, "New Live Events (${newEvents.size}) [${newEvents.map { it.type.name }.joinToString(", ")}]");
|
if(newEvents.size > 0)
|
||||||
|
Logger.i(TAG, "New Live Events (${newEvents.size}) [${newEvents.map { it.type.name }.joinToString(", ")}]");
|
||||||
|
else
|
||||||
|
Logger.v(TAG, "No new Live Events");
|
||||||
|
|
||||||
_scope.launch(Dispatchers.Main) {
|
_scope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,17 +6,20 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
|||||||
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.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
|
||||||
class PlatformClientPool {
|
class PlatformClientPool {
|
||||||
private val _parent: JSClient;
|
private val _parent: JSClient;
|
||||||
private val _pool: HashMap<JSClient, Int> = hashMapOf();
|
private val _pool: HashMap<JSClient, Int> = hashMapOf();
|
||||||
private var _poolCounter = 0;
|
private var _poolCounter = 0;
|
||||||
|
private val _poolName: String?;
|
||||||
|
|
||||||
var isDead: Boolean = false
|
var isDead: Boolean = false
|
||||||
private set;
|
private set;
|
||||||
val onDead = Event2<JSClient, PlatformClientPool>();
|
val onDead = Event2<JSClient, PlatformClientPool>();
|
||||||
|
|
||||||
constructor(parentClient: IPlatformClient) {
|
constructor(parentClient: IPlatformClient, name: String? = null) {
|
||||||
|
_poolName = name;
|
||||||
if(parentClient !is JSClient)
|
if(parentClient !is JSClient)
|
||||||
throw IllegalArgumentException("Pooling only supported for JSClients right now");
|
throw IllegalArgumentException("Pooling only supported for JSClients right now");
|
||||||
Logger.i(TAG, "Pool for ${parentClient.name} was started");
|
Logger.i(TAG, "Pool for ${parentClient.name} was started");
|
||||||
@@ -47,8 +50,13 @@ class PlatformClientPool {
|
|||||||
_poolCounter++;
|
_poolCounter++;
|
||||||
reserved = _pool.keys.find { !it.isBusy };
|
reserved = _pool.keys.find { !it.isBusy };
|
||||||
if(reserved == null && _pool.size < capacity) {
|
if(reserved == null && _pool.size < capacity) {
|
||||||
Logger.i(TAG, "Started additional [${_parent.name}] client in pool (${_pool.size + 1}/${capacity})");
|
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
|
||||||
reserved = _parent.getCopy();
|
reserved = _parent.getCopy();
|
||||||
|
|
||||||
|
reserved?.onCaptchaException?.subscribe { client, ex ->
|
||||||
|
StateApp.instance.handleCaptchaException(client, ex);
|
||||||
|
};
|
||||||
|
|
||||||
reserved?.initialize();
|
reserved?.initialize();
|
||||||
_pool[reserved!!] = _poolCounter;
|
_pool[reserved!!] = _poolCounter;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.futo.platformplayer.getOrDefault
|
|||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.getOrThrowNullable
|
import com.futo.platformplayer.getOrThrowNullable
|
||||||
import com.futo.polycentric.core.combineHashCodes
|
import com.futo.polycentric.core.combineHashCodes
|
||||||
|
import okhttp3.internal.platform.Platform
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class PlatformID {
|
class PlatformID {
|
||||||
@@ -40,6 +41,8 @@ class PlatformID {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
val NONE = PlatformID("Unknown", null);
|
||||||
|
|
||||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID {
|
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformID {
|
||||||
val contextName = "PlatformID";
|
val contextName = "PlatformID";
|
||||||
return PlatformID(
|
return PlatformID(
|
||||||
@@ -49,5 +52,9 @@ class PlatformID {
|
|||||||
value.getOrDefault(config, "claimType", contextName, 0) ?: 0,
|
value.getOrDefault(config, "claimType", contextName, 0) ?: 0,
|
||||||
value.getOrDefault(config, "claimFieldType", contextName, -1) ?: -1);
|
value.getOrDefault(config, "claimFieldType", contextName, -1) ?: -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun asUrlID(url: String): PlatformID {
|
||||||
|
return PlatformID("URL", url, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.futo.platformplayer.api.media
|
||||||
|
|
||||||
|
class PlatformMultiClientPool {
|
||||||
|
private val _name: String;
|
||||||
|
private val _maxCap: Int;
|
||||||
|
private val _clientPools: HashMap<IPlatformClient, PlatformClientPool> = hashMapOf();
|
||||||
|
|
||||||
|
private var _isFake = false;
|
||||||
|
|
||||||
|
constructor(name: String, maxCap: Int = -1) {
|
||||||
|
_name = name;
|
||||||
|
_maxCap = if(maxCap > 0)
|
||||||
|
maxCap
|
||||||
|
else 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getClientPooled(parentClient: IPlatformClient, capacity: Int = _maxCap): IPlatformClient {
|
||||||
|
if(_isFake)
|
||||||
|
return parentClient;
|
||||||
|
val pool = synchronized(_clientPools) {
|
||||||
|
if(!_clientPools.containsKey(parentClient))
|
||||||
|
_clientPools[parentClient] = PlatformClientPool(parentClient, _name).apply {
|
||||||
|
this.onDead.subscribe { client, pool ->
|
||||||
|
synchronized(_clientPools) {
|
||||||
|
if(_clientPools[parentClient] == pool)
|
||||||
|
_clientPools.remove(parentClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_clientPools[parentClient]!!;
|
||||||
|
};
|
||||||
|
return pool.getClient(capacity.coerceAtMost(_maxCap));
|
||||||
|
}
|
||||||
|
|
||||||
|
//Allows for testing disabling pooling without changing callers
|
||||||
|
fun asFake(): PlatformMultiClientPool {
|
||||||
|
_isFake = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ class ResultCapabilities(
|
|||||||
const val TYPE_VIDEOS = "VIDEOS";
|
const val TYPE_VIDEOS = "VIDEOS";
|
||||||
const val TYPE_STREAMS = "STREAMS";
|
const val TYPE_STREAMS = "STREAMS";
|
||||||
const val TYPE_LIVE = "LIVE";
|
const val TYPE_LIVE = "LIVE";
|
||||||
|
const val TYPE_POSTS = "POSTS";
|
||||||
const val TYPE_MIXED = "MIXED";
|
const val TYPE_MIXED = "MIXED";
|
||||||
|
|
||||||
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
@@ -39,4 +39,8 @@ class PolycentricPlatformComment : IPlatformComment {
|
|||||||
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
|
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
|
||||||
return PolycentricPlatformComment(contextUrl, author, message, rating, date, reference, replyCount);
|
return PolycentricPlatformComment(contextUrl, author, message, rating, date, reference, replyCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val MAX_COMMENT_SIZE = 2000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -4,7 +4,7 @@ import com.futo.platformplayer.api.media.PlatformID
|
|||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
class PlatformContentPlaceholder(pluginId: String): IPlatformContent {
|
class PlatformContentPlaceholder(pluginId: String, exception: Throwable? = null): IPlatformContent {
|
||||||
override val contentType: ContentType = ContentType.PLACEHOLDER;
|
override val contentType: ContentType = ContentType.PLACEHOLDER;
|
||||||
override val id: PlatformID = PlatformID("", null, pluginId);
|
override val id: PlatformID = PlatformID("", null, pluginId);
|
||||||
override val name: String = "";
|
override val name: String = "";
|
||||||
@@ -12,4 +12,5 @@ class PlatformContentPlaceholder(pluginId: String): IPlatformContent {
|
|||||||
override val shareUrl: String = "";
|
override val shareUrl: String = "";
|
||||||
override val datetime: OffsetDateTime? = null;
|
override val datetime: OffsetDateTime? = null;
|
||||||
override val author: PlatformAuthorLink = PlatformAuthorLink(PlatformID("", pluginId), "", "", null, null);
|
override val author: PlatformAuthorLink = PlatformAuthorLink(PlatformID("", pluginId), "", "", null, null);
|
||||||
|
val error: Throwable? = exception
|
||||||
}
|
}
|
||||||
+1
-1
@@ -30,7 +30,7 @@ open class SerializedPlatformNestedContent(
|
|||||||
override val contentProvider: String?,
|
override val contentProvider: String?,
|
||||||
override val contentThumbnails: Thumbnails
|
override val contentThumbnails: Thumbnails
|
||||||
) : IPlatformNestedContent, SerializedPlatformContent {
|
) : IPlatformNestedContent, SerializedPlatformContent {
|
||||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
final override val contentType: ContentType get() = ContentType.NESTED_VIDEO;
|
||||||
|
|
||||||
override val contentPlugin: String? = StatePlatform.instance.getContentClientOrNull(contentUrl)?.id;
|
override val contentPlugin: String? = StatePlatform.instance.getContentClientOrNull(contentUrl)?.id;
|
||||||
override val contentSupported: Boolean get() = contentPlugin != null;
|
override val contentSupported: Boolean get() = contentPlugin != null;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class DevJSClient : JSClient {
|
class DevJSClient : JSClient {
|
||||||
@@ -15,29 +16,44 @@ class DevJSClient : JSClient {
|
|||||||
|
|
||||||
private val _devScript: String;
|
private val _devScript: String;
|
||||||
private var _auth: SourceAuth? = null;
|
private var _auth: SourceAuth? = null;
|
||||||
|
private var _captcha: SourceCaptchaData? = null;
|
||||||
|
|
||||||
val devID: String;
|
val devID: String;
|
||||||
|
|
||||||
constructor(context: Context, config: SourcePluginConfig, script: String, auth: SourceAuth? = null, devID: String? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), listOf("DEV")), null, script) {
|
constructor(context: Context, config: SourcePluginConfig, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, devID: String? = null): super(context, SourcePluginDescriptor(config, auth?.toEncrypted(), captcha?.toEncrypted(), listOf("DEV")), null, script) {
|
||||||
_devScript = script;
|
_devScript = script;
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
|
_captcha = captcha;
|
||||||
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
||||||
|
|
||||||
|
onCaptchaException.subscribe { client, captcha ->
|
||||||
|
StateApp.instance.handleCaptchaException(client, captcha);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
constructor(context: Context, descriptor: SourcePluginDescriptor, script: String, auth: SourceAuth? = null, savedState: String? = null, devID: String? = null): super(context, descriptor, savedState, script) {
|
//TODO: Misisng auth/captcha pass on purpose?
|
||||||
|
constructor(context: Context, descriptor: SourcePluginDescriptor, script: String, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, savedState: String? = null, devID: String? = null): super(context, descriptor, savedState, script) {
|
||||||
_devScript = script;
|
_devScript = script;
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
|
_captcha = captcha;
|
||||||
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
||||||
|
|
||||||
|
onCaptchaException.subscribe { client, captcha ->
|
||||||
|
StateApp.instance.handleCaptchaException(client, captcha);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setCaptcha(captcha: SourceCaptchaData? = null) {
|
||||||
|
_captcha = captcha;
|
||||||
|
}
|
||||||
fun setAuth(auth: SourceAuth? = null) {
|
fun setAuth(auth: SourceAuth? = null) {
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
}
|
}
|
||||||
fun recreate(context: Context): DevJSClient {
|
fun recreate(context: Context): DevJSClient {
|
||||||
return DevJSClient(context, config, _devScript, _auth, devID);
|
return DevJSClient(context, config, _devScript, _auth, _captcha, devID);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCopy(): JSClient {
|
override fun getCopy(): JSClient {
|
||||||
return DevJSClient(_context, descriptor, _script, _auth, saveState(), devID);
|
return DevJSClient(_context, descriptor, _script, _auth, _captcha, saveState(), devID);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initialize() {
|
override fun initialize() {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
import com.caoccao.javet.values.primitive.V8ValueBoolean
|
import com.caoccao.javet.values.primitive.V8ValueBoolean
|
||||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||||
|
import com.caoccao.javet.values.primitive.V8ValueNull
|
||||||
import com.caoccao.javet.values.primitive.V8ValueString
|
import com.caoccao.javet.values.primitive.V8ValueString
|
||||||
import com.caoccao.javet.values.reference.V8ValueArray
|
import com.caoccao.javet.values.reference.V8ValueArray
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
@@ -14,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
|
||||||
@@ -23,9 +25,14 @@ import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
|||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.*
|
import com.futo.platformplayer.api.media.platforms.js.internal.*
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.*
|
import com.futo.platformplayer.api.media.platforms.js.models.*
|
||||||
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.PluginEngineStoppedException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -59,6 +66,7 @@ open class JSClient : IPlatformClient {
|
|||||||
private var _enabled: Boolean = false;
|
private var _enabled: Boolean = false;
|
||||||
|
|
||||||
private val _auth: SourceAuth?;
|
private val _auth: SourceAuth?;
|
||||||
|
private val _captcha: SourceCaptchaData?;
|
||||||
|
|
||||||
private val _injectedSaveState: String?;
|
private val _injectedSaveState: String?;
|
||||||
|
|
||||||
@@ -84,7 +92,21 @@ 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>();
|
||||||
|
|
||||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String? = null) {
|
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String? = null) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
@@ -93,10 +115,11 @@ open class JSClient : IPlatformClient {
|
|||||||
this.descriptor = descriptor;
|
this.descriptor = descriptor;
|
||||||
_injectedSaveState = saveState;
|
_injectedSaveState = saveState;
|
||||||
_auth = descriptor.getAuth();
|
_auth = descriptor.getAuth();
|
||||||
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
_client = JSHttpClient(this);
|
_client = JSHttpClient(this, null, _captcha);
|
||||||
_clientAuth = JSHttpClient(this, _auth);
|
_clientAuth = JSHttpClient(this, _auth, _captcha);
|
||||||
_plugin = V8Plugin(context, descriptor.config, null, _client, _clientAuth);
|
_plugin = V8Plugin(context, descriptor.config, null, _client, _clientAuth);
|
||||||
_plugin.withDependency(context, "scripts/polyfil.js");
|
_plugin.withDependency(context, "scripts/polyfil.js");
|
||||||
_plugin.withDependency(context, "scripts/source.js");
|
_plugin.withDependency(context, "scripts/source.js");
|
||||||
@@ -108,6 +131,11 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
throw IllegalStateException("Script for plugin [${descriptor.config.name}] was not available");
|
throw IllegalStateException("Script for plugin [${descriptor.config.name}] was not available");
|
||||||
|
|
||||||
|
_plugin.onScriptException.subscribe {
|
||||||
|
if(it is ScriptCaptchaRequiredException)
|
||||||
|
onCaptchaException.emit(this, it);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
@@ -116,15 +144,21 @@ open class JSClient : IPlatformClient {
|
|||||||
this.descriptor = descriptor;
|
this.descriptor = descriptor;
|
||||||
_injectedSaveState = saveState;
|
_injectedSaveState = saveState;
|
||||||
_auth = descriptor.getAuth();
|
_auth = descriptor.getAuth();
|
||||||
|
_captcha = descriptor.getCaptchaData();
|
||||||
flags = descriptor.flags.toTypedArray();
|
flags = descriptor.flags.toTypedArray();
|
||||||
|
|
||||||
_client = JSHttpClient(this);
|
_client = JSHttpClient(this, null, _captcha);
|
||||||
_clientAuth = JSHttpClient(this, _auth);
|
_clientAuth = JSHttpClient(this, _auth, _captcha);
|
||||||
_plugin = V8Plugin(context, descriptor.config, script, _client, _clientAuth);
|
_plugin = V8Plugin(context, descriptor.config, script, _client, _clientAuth);
|
||||||
_plugin.withDependency(context, "scripts/polyfil.js");
|
_plugin.withDependency(context, "scripts/polyfil.js");
|
||||||
_plugin.withDependency(context, "scripts/source.js");
|
_plugin.withDependency(context, "scripts/source.js");
|
||||||
_plugin.withScript(script);
|
_plugin.withScript(script);
|
||||||
_script = script;
|
_script = script;
|
||||||
|
|
||||||
|
_plugin.onScriptException.subscribe {
|
||||||
|
if(it is ScriptCaptchaRequiredException)
|
||||||
|
onCaptchaException.emit(this, it);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getCopy(): JSClient {
|
open fun getCopy(): JSClient {
|
||||||
@@ -161,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 {
|
||||||
@@ -394,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)")
|
||||||
@@ -413,8 +459,11 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSDocsParameter("url", "A content url (this platform)")
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith {
|
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSCommentPager(config, plugin,
|
val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})");
|
||||||
plugin.executeTyped("source.getComments(${Json.encodeToString(url)})"));
|
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
|
||||||
|
return@isBusyWith EmptyPager<IPlatformComment>();
|
||||||
|
}
|
||||||
|
return@isBusyWith JSCommentPager(config, plugin, pager);
|
||||||
}
|
}
|
||||||
@JSDocs(17, "source.getSubComments(comment)", "Gets replies for a given comment")
|
@JSDocs(17, "source.getSubComments(comment)", "Gets replies for a given comment")
|
||||||
@JSDocsParameter("comment", "Comment object that was returned by getComments")
|
@JSDocsParameter("comment", "Comment object that was returned by getComments")
|
||||||
@@ -535,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]!!);
|
||||||
}
|
}
|
||||||
@@ -545,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 {
|
||||||
@@ -561,11 +627,13 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun announcePluginUnhandledException(method: String, ex: Throwable) {
|
private fun announcePluginUnhandledException(method: String, ex: Throwable) {
|
||||||
|
if(ex is PluginEngineException)
|
||||||
|
return;
|
||||||
try {
|
try {
|
||||||
StateAnnouncement.instance.registerAnnouncement("PluginUnhandled_${config.id}_${method}",
|
StateAnnouncement.instance.registerAnnouncement("PluginUnhandled_${config.id}_${method}",
|
||||||
"Plugin ${config.name} encountered an error in [${method}]",
|
"Plugin ${config.name} encountered an error in [${method}]",
|
||||||
"${ex.message}\nPlease contact the plugin developer",
|
"${ex.message}\nPlease contact the plugin developer",
|
||||||
AnnouncementType.RECURRING,
|
AnnouncementType.SESSION_RECURRING,
|
||||||
OffsetDateTime.now());
|
OffsetDateTime.now());
|
||||||
}
|
}
|
||||||
catch(_: Throwable) {}
|
catch(_: Throwable) {}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
data class SourceCaptchaData(val cookieMap: HashMap<String, HashMap<String, String>>? = null, val headers: Map<String, Map<String, String>> = mapOf()) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "(headers: '$headers', cookieString: '$cookieMap')";
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toEncrypted(): String{
|
||||||
|
return SourceEncrypted.fromDecrypted { serialize() }.toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun serialize(): String {
|
||||||
|
return Json.encodeToString(SerializedCaptchaData(cookieMap, headers));
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG = "SourceCaptchaData";
|
||||||
|
|
||||||
|
fun fromEncrypted(encrypted: String?): SourceCaptchaData? {
|
||||||
|
return SourceEncrypted.decryptEncrypted(encrypted) { deserialize(it) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deserialize(str: String): SourceCaptchaData {
|
||||||
|
val data = Json.decodeFromString<SerializedCaptchaData>(str);
|
||||||
|
return SourceCaptchaData(data.cookieMap, data.headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SerializedCaptchaData(val cookieMap: HashMap<String, HashMap<String, String>>?,
|
||||||
|
val headers: Map<String, Map<String, String>> = mapOf())
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SourcePluginCaptchaConfig(
|
||||||
|
val captchaUrl: String? = null,
|
||||||
|
val completionUrl: String? = null,
|
||||||
|
val cookiesToFind: List<String>? = null,
|
||||||
|
val userAgent: String? = null,
|
||||||
|
val cookiesExclOthers: Boolean = true
|
||||||
|
)
|
||||||
+19
-2
@@ -4,6 +4,7 @@ import android.net.Uri
|
|||||||
import com.futo.platformplayer.SignatureProvider
|
import com.futo.platformplayer.SignatureProvider
|
||||||
import com.futo.platformplayer.api.media.Serializer
|
import com.futo.platformplayer.api.media.Serializer
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -34,14 +35,18 @@ class SourcePluginConfig(
|
|||||||
|
|
||||||
val settings: List<Setting> = listOf(),
|
val settings: List<Setting> = listOf(),
|
||||||
|
|
||||||
|
var captcha: SourcePluginCaptchaConfig? = null,
|
||||||
val authentication: SourcePluginAuthConfig? = null,
|
val authentication: SourcePluginAuthConfig? = null,
|
||||||
var sourceUrl: String? = null,
|
var sourceUrl: String? = null,
|
||||||
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 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);
|
||||||
@@ -78,6 +83,15 @@ class SourcePluginConfig(
|
|||||||
fun getWarnings(scriptToCheck: String? = null) : List<Pair<String,String>> {
|
fun getWarnings(scriptToCheck: String? = null) : List<Pair<String,String>> {
|
||||||
val list = mutableListOf<Pair<String,String>>();
|
val list = mutableListOf<Pair<String,String>>();
|
||||||
|
|
||||||
|
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
|
||||||
|
if (currentlyInstalledPlugin != null) {
|
||||||
|
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey) {
|
||||||
|
list.add(Pair(
|
||||||
|
"Different Author",
|
||||||
|
"This plugin was signed by a different author. Please ensure that this is correct and that the plugin was not provided by a malicious actor."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(scriptPublicKey.isNullOrEmpty() || scriptSignature.isNullOrEmpty())
|
if(scriptPublicKey.isNullOrEmpty() || scriptSignature.isNullOrEmpty())
|
||||||
list.add(Pair(
|
list.add(Pair(
|
||||||
"Missing Signature",
|
"Missing Signature",
|
||||||
@@ -130,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;
|
||||||
|
|||||||
+44
-6
@@ -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
|
||||||
@@ -13,22 +15,28 @@ class SourcePluginDescriptor {
|
|||||||
|
|
||||||
var appSettings: AppPluginSettings = AppPluginSettings();
|
var appSettings: AppPluginSettings = AppPluginSettings();
|
||||||
|
|
||||||
var authEncrypted: String?
|
var authEncrypted: String? = null
|
||||||
|
private set;
|
||||||
|
var captchaEncrypted: String? = null
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
val flags: List<String>;
|
val flags: List<String>;
|
||||||
|
|
||||||
@kotlinx.serialization.Transient
|
@kotlinx.serialization.Transient
|
||||||
val onAuthChanged = Event0();
|
val onAuthChanged = Event0();
|
||||||
|
@kotlinx.serialization.Transient
|
||||||
|
val onCaptchaChanged = Event0();
|
||||||
|
|
||||||
constructor(config :SourcePluginConfig, authEncrypted: String? = null) {
|
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.authEncrypted = authEncrypted;
|
this.authEncrypted = authEncrypted;
|
||||||
|
this.captchaEncrypted = captchaEncrypted;
|
||||||
this.flags = listOf();
|
this.flags = listOf();
|
||||||
}
|
}
|
||||||
constructor(config :SourcePluginConfig, authEncrypted: String? = null, flags: List<String>) {
|
constructor(config :SourcePluginConfig, authEncrypted: String? = null, captchaEncrypted: String? = null, flags: List<String>) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.authEncrypted = authEncrypted;
|
this.authEncrypted = authEncrypted;
|
||||||
|
this.captchaEncrypted = captchaEncrypted;
|
||||||
this.flags = flags;
|
this.flags = flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +49,13 @@ class SourcePluginDescriptor {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateCaptcha(captcha: SourceCaptchaData?) {
|
||||||
|
captchaEncrypted = captcha?.toEncrypted();
|
||||||
|
onCaptchaChanged.emit();
|
||||||
|
}
|
||||||
|
fun getCaptchaData(): SourceCaptchaData? {
|
||||||
|
return SourceCaptchaData.fromEncrypted(captchaEncrypted);
|
||||||
|
}
|
||||||
|
|
||||||
fun updateAuth(str: SourceAuth?) {
|
fun updateAuth(str: SourceAuth?) {
|
||||||
authEncrypted = str?.toEncrypted();
|
authEncrypted = str?.toEncrypted();
|
||||||
@@ -53,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) {
|
||||||
|
|||||||
+69
-45
@@ -5,90 +5,108 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
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.SourceAuth
|
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
||||||
import com.futo.platformplayer.matchesDomain
|
import com.futo.platformplayer.matchesDomain
|
||||||
|
|
||||||
class JSHttpClient : ManagedHttpClient {
|
class JSHttpClient : ManagedHttpClient {
|
||||||
private val _jsClient: JSClient?;
|
private val _jsClient: JSClient?;
|
||||||
private val _auth: SourceAuth?;
|
private val _auth: SourceAuth?;
|
||||||
|
private val _captcha: SourceCaptchaData?;
|
||||||
|
|
||||||
var doUpdateCookies: Boolean = true;
|
var doUpdateCookies: Boolean = true;
|
||||||
var doApplyCookies: Boolean = true;
|
var doApplyCookies: Boolean = true;
|
||||||
var doAllowNewCookies: Boolean = true;
|
var doAllowNewCookies: Boolean = true;
|
||||||
val isLoggedIn: Boolean get() = _auth != null;
|
val isLoggedIn: Boolean get() = _auth != null;
|
||||||
|
|
||||||
private var _currentCookieMap: HashMap<String, HashMap<String, String>>?;
|
private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
|
||||||
|
|
||||||
constructor(jsClient: JSClient?, auth: SourceAuth? = null) : super() {
|
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null) : super() {
|
||||||
_jsClient = jsClient;
|
_jsClient = jsClient;
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
|
_captcha = captcha;
|
||||||
|
|
||||||
|
_currentCookieMap = hashMapOf();
|
||||||
if(!auth?.cookieMap.isNullOrEmpty()) {
|
if(!auth?.cookieMap.isNullOrEmpty()) {
|
||||||
_currentCookieMap = hashMapOf();
|
|
||||||
for(domainCookies in auth!!.cookieMap!!)
|
for(domainCookies in auth!!.cookieMap!!)
|
||||||
_currentCookieMap!!.put(domainCookies.key, HashMap(domainCookies.value));
|
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
|
||||||
}
|
}
|
||||||
else _currentCookieMap = null;
|
if(!captcha?.cookieMap.isNullOrEmpty()) {
|
||||||
|
for(domainCookies in captcha!!.cookieMap!!) {
|
||||||
|
if(_currentCookieMap.containsKey(domainCookies.key))
|
||||||
|
_currentCookieMap[domainCookies.key]?.putAll(domainCookies.value);
|
||||||
|
else
|
||||||
|
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clone(): ManagedHttpClient {
|
override fun clone(): ManagedHttpClient {
|
||||||
val newClient = JSHttpClient(_jsClient, _auth);
|
val newClient = JSHttpClient(_jsClient, _auth);
|
||||||
newClient._currentCookieMap = if(_currentCookieMap != null)
|
newClient._currentCookieMap = if(_currentCookieMap != null)
|
||||||
HashMap(_currentCookieMap!!.toList().associate { Pair(it.first, HashMap(it.second)) })
|
HashMap(_currentCookieMap.toList().associate { Pair(it.first, HashMap(it.second)) })
|
||||||
else
|
else
|
||||||
null;
|
hashMapOf();
|
||||||
return newClient;
|
return newClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun beforeRequest(request: Request) {
|
override fun beforeRequest(request: okhttp3.Request): okhttp3.Request {
|
||||||
|
val domain = request.url.host.lowercase();
|
||||||
val auth = _auth;
|
val auth = _auth;
|
||||||
if (auth != null) {
|
|
||||||
val domain = Uri.parse(request.url).host!!.lowercase();
|
|
||||||
|
|
||||||
|
val newBuilder = if(auth != null || doApplyCookies)
|
||||||
|
request.newBuilder();
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
if (auth != null) {
|
||||||
//TODO: Possibly add doApplyHeaders
|
//TODO: Possibly add doApplyHeaders
|
||||||
for (header in auth.headers.filter { domain.matchesDomain(it.key) }.flatMap { it.value.entries })
|
for (header in auth.headers.filter { domain.matchesDomain(it.key) }.flatMap { it.value.entries })
|
||||||
request.headers[header.key] = header.value;
|
newBuilder?.header(header.key, header.value);
|
||||||
|
}
|
||||||
|
|
||||||
if(doApplyCookies) {
|
if(doApplyCookies) {
|
||||||
if (!_currentCookieMap.isNullOrEmpty()) {
|
if (!_currentCookieMap.isNullOrEmpty()) {
|
||||||
val cookiesToApply = hashMapOf<String, String>();
|
val cookiesToApply = hashMapOf<String, String>();
|
||||||
synchronized(_currentCookieMap!!) {
|
synchronized(_currentCookieMap!!) {
|
||||||
for(cookie in _currentCookieMap!!
|
for(cookie in _currentCookieMap!!
|
||||||
.filter { domain.matchesDomain(it.key) }
|
.filter { domain.matchesDomain(it.key) }
|
||||||
.flatMap { it.value.toList() })
|
.flatMap { it.value.toList() })
|
||||||
cookiesToApply[cookie.first] = cookie.second;
|
cookiesToApply[cookie.first] = cookie.second;
|
||||||
};
|
};
|
||||||
|
|
||||||
if(cookiesToApply.size > 0) {
|
if(cookiesToApply.size > 0) {
|
||||||
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
|
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
|
||||||
request.headers["Cookie"] = cookieString;
|
|
||||||
}
|
val existingCookies = request.headers["Cookie"];
|
||||||
//printTestCode(request.url, request.body, auth.headers, cookieString, request.headers.filter { !auth.headers.containsKey(it.key) });
|
if(!existingCookies.isNullOrEmpty())
|
||||||
|
newBuilder?.header("Cookie", existingCookies.trim(';') + "; " + cookieString);
|
||||||
|
else
|
||||||
|
newBuilder?.header("Cookie", cookieString);
|
||||||
}
|
}
|
||||||
|
//printTestCode(request.url, request.body, auth.headers, cookieString, request.headers.filter { !auth.headers.containsKey(it.key) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_jsClient?.validateUrlOrThrow(request.url);
|
_jsClient?.validateUrlOrThrow(request.url.toString());
|
||||||
super.beforeRequest(request)
|
return newBuilder?.let { it.build() } ?: request;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun afterRequest(request: Request, resp: Response) {
|
override fun afterRequest(resp: okhttp3.Response): okhttp3.Response {
|
||||||
super.afterRequest(request, resp)
|
|
||||||
|
|
||||||
if(doUpdateCookies) {
|
if(doUpdateCookies) {
|
||||||
val domain = Uri.parse(request.url).host!!.lowercase();
|
val domain = resp.request.url.host.lowercase();
|
||||||
val domainParts = domain!!.split(".");
|
val domainParts = domain.split(".");
|
||||||
val defaultCookieDomain =
|
val defaultCookieDomain =
|
||||||
"." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
"." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
||||||
for (header in resp.headers) {
|
for (header in resp.headers) {
|
||||||
if (_currentCookieMap != null && header.key.lowercase() == "set-cookie") {
|
if ((_auth != null || _currentCookieMap.isNotEmpty()) && header.first.lowercase() == "set-cookie") {
|
||||||
val newCookies = cookieStringToMap(header.value);
|
//val newCookies = cookieStringToMap(header.second.split("; "));
|
||||||
for (cookie in newCookies) {
|
val cookie = cookieStringToPair(header.second);
|
||||||
val endIndex = cookie.value.indexOf(";");
|
//for (cookie in newCookies) {
|
||||||
var cookieValue = cookie.value;
|
var cookieValue = cookie.second;
|
||||||
var domainToUse = domain;
|
var domainToUse = domain;
|
||||||
|
|
||||||
if (endIndex > 0) {
|
if (!cookie.first.isNullOrEmpty() && !cookie.second.isNullOrEmpty()) {
|
||||||
val cookieParts = cookie.value.split(";");
|
val cookieParts = cookie.second.split(";");
|
||||||
if (cookieParts.size == 0)
|
if (cookieParts.size == 0)
|
||||||
continue;
|
continue;
|
||||||
cookieValue = cookieParts[0].trim();
|
cookieValue = cookieParts[0].trim();
|
||||||
@@ -114,24 +132,29 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
_currentCookieMap!!.put(domainToUse, newMap)
|
_currentCookieMap!!.put(domainToUse, newMap)
|
||||||
newMap;
|
newMap;
|
||||||
}
|
}
|
||||||
if(cookieMap.containsKey(cookie.key) || doAllowNewCookies)
|
if(cookieMap.containsKey(cookie.first) || doAllowNewCookies)
|
||||||
cookieMap.put(cookie.key, cookieValue);
|
cookieMap.put(cookie.first, cookieValue);
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun cookieStringToMap(parts: List<String>): Map<String, String> {
|
private fun cookieStringToMap(parts: List<String>): Map<String, String> {
|
||||||
val map = hashMapOf<String, String>();
|
val map = hashMapOf<String, String>();
|
||||||
for(cookie in parts) {
|
for(cookie in parts) {
|
||||||
val cookieKey = cookie.substring(0, cookie.indexOf("="));
|
val pair = cookieStringToPair(cookie)
|
||||||
val cookieVal = cookie.substring(cookie.indexOf("=") + 1);
|
map.put(pair.first, pair.second);
|
||||||
map.put(cookieKey.trim(), cookieVal.trim());
|
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
private fun cookieStringToPair(cookie: String): Pair<String, String> {
|
||||||
|
val cookieKey = cookie.substring(0, cookie.indexOf("="));
|
||||||
|
val cookieVal = cookie.substring(cookie.indexOf("=") + 1);
|
||||||
|
return Pair(cookieKey.trim(), cookieVal.trim());
|
||||||
|
}
|
||||||
|
|
||||||
//Prints out code for test reproduction..
|
//Prints out code for test reproduction..
|
||||||
fun printTestCode(url: String, body: ByteArray?, headers: Map<String, String>, cookieString: String, allHeaders: Map<String, String>? = null) {
|
fun printTestCode(url: String, body: ByteArray?, headers: Map<String, String>, cookieString: String, allHeaders: Map<String, String>? = null) {
|
||||||
@@ -155,4 +178,5 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
|
|
||||||
Logger.i("Testing", code);
|
Logger.i("Testing", code);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,6 @@ open class JSContent : IPlatformContent, IPluginSourced {
|
|||||||
|
|
||||||
id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
|
id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
|
||||||
name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
|
name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
|
||||||
Logger.i("JSContent", "name=$name");
|
|
||||||
author = PlatformAuthorLink.fromV8(_pluginConfig, _content.getOrThrow(config, "author", contextName));
|
author = PlatformAuthorLink.fromV8(_pluginConfig, _content.getOrThrow(config, "author", contextName));
|
||||||
|
|
||||||
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
|
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js.models
|
package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
|
import android.os.Looper
|
||||||
import com.caoccao.javet.values.reference.V8ValueArray
|
import com.caoccao.javet.values.reference.V8ValueArray
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
import com.futo.platformplayer.warnIfMainThread
|
||||||
|
|
||||||
abstract class JSPager<T> : IPager<T> {
|
abstract class JSPager<T> : IPager<T> {
|
||||||
protected val plugin: V8Plugin;
|
protected val plugin: V8Plugin;
|
||||||
@@ -24,7 +28,7 @@ abstract class JSPager<T> : IPager<T> {
|
|||||||
this.pager = pager;
|
this.pager = pager;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
||||||
_hasMorePages = pager.getOrThrow(config, "hasMore", "Pager");
|
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
||||||
getResults();
|
getResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,10 +41,12 @@ abstract class JSPager<T> : IPager<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun nextPage() {
|
override fun nextPage() {
|
||||||
|
warnIfMainThread("JSPager.nextPage");
|
||||||
|
|
||||||
pager = plugin.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
|
pager = plugin.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
|
||||||
pager.invoke("nextPage", arrayOf<Any>());
|
pager.invoke("nextPage", arrayOf<Any>());
|
||||||
};
|
};
|
||||||
_hasMorePages = pager.getOrThrow(config, "hasMore", "Pager");
|
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
||||||
_resultChanged = true;
|
_resultChanged = true;
|
||||||
/*
|
/*
|
||||||
try {
|
try {
|
||||||
@@ -53,6 +59,8 @@ abstract class JSPager<T> : IPager<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getResults(): List<T> {
|
override fun getResults(): List<T> {
|
||||||
|
warnIfMainThread("JSPager.getResults");
|
||||||
|
|
||||||
val previousResults = _lastResults?.let {
|
val previousResults = _lastResults?.let {
|
||||||
if(!_resultChanged)
|
if(!_resultChanged)
|
||||||
return@let it;
|
return@let it;
|
||||||
|
|||||||
+4
@@ -6,6 +6,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
|
|||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.warnIfMainThread
|
||||||
|
|
||||||
class JSPlaybackTracker: IPlaybackTracker {
|
class JSPlaybackTracker: IPlaybackTracker {
|
||||||
private val _config: IV8PluginConfig;
|
private val _config: IV8PluginConfig;
|
||||||
@@ -20,6 +21,7 @@ class JSPlaybackTracker: IPlaybackTracker {
|
|||||||
private set;
|
private set;
|
||||||
|
|
||||||
constructor(config: IV8PluginConfig, obj: V8ValueObject) {
|
constructor(config: IV8PluginConfig, obj: V8ValueObject) {
|
||||||
|
warnIfMainThread("JSPlaybackTracker.constructor");
|
||||||
if(!obj.has("onProgress"))
|
if(!obj.has("onProgress"))
|
||||||
throw ScriptImplementationException(config, "Missing onProgress on PlaybackTracker");
|
throw ScriptImplementationException(config, "Missing onProgress on PlaybackTracker");
|
||||||
if(!obj.has("nextRequest"))
|
if(!obj.has("nextRequest"))
|
||||||
@@ -31,6 +33,7 @@ class JSPlaybackTracker: IPlaybackTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onInit(seconds: Double) {
|
override fun onInit(seconds: Double) {
|
||||||
|
warnIfMainThread("JSPlaybackTracker.onInit");
|
||||||
synchronized(_obj) {
|
synchronized(_obj) {
|
||||||
if(_hasCalledInit)
|
if(_hasCalledInit)
|
||||||
return;
|
return;
|
||||||
@@ -44,6 +47,7 @@ class JSPlaybackTracker: IPlaybackTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onProgress(seconds: Double, isPlaying: Boolean) {
|
override fun onProgress(seconds: Double, isPlaying: Boolean) {
|
||||||
|
warnIfMainThread("JSPlaybackTracker.onProgress");
|
||||||
synchronized(_obj) {
|
synchronized(_obj) {
|
||||||
if(!_hasCalledInit && _hasInit)
|
if(!_hasCalledInit && _hasInit)
|
||||||
onInit(seconds);
|
onInit(seconds);
|
||||||
|
|||||||
+6
-2
@@ -1,6 +1,7 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js.models
|
package com.futo.platformplayer.api.media.platforms.js.models
|
||||||
|
|
||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
|
import com.caoccao.javet.values.primitive.V8ValueNull
|
||||||
import com.caoccao.javet.values.reference.V8ValueArray
|
import com.caoccao.javet.values.reference.V8ValueArray
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
@@ -99,8 +100,11 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
|||||||
return getCommentsJS(client);
|
return getCommentsJS(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? {
|
||||||
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
val commentPager = _content.invoke<V8Value>("getComments", arrayOf<Any>());
|
||||||
|
if (commentPager !is V8ValueObject) //TODO: Maybe handle this better?
|
||||||
|
return null;
|
||||||
|
|
||||||
return JSCommentPager(_pluginConfig, client.getUnderlyingPlugin(), commentPager);
|
return JSCommentPager(_pluginConfig, client.getUnderlyingPlugin(), commentPager);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-4
@@ -9,7 +9,7 @@ import com.futo.platformplayer.getOrNull
|
|||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.orNull
|
import com.futo.platformplayer.orNull
|
||||||
|
|
||||||
class JSHLSManifestAudioSource : IAudioUrlSource, IHLSManifestAudioSource, JSSource {
|
class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
||||||
override val container : String get() = "application/vnd.apple.mpegurl";
|
override val container : String get() = "application/vnd.apple.mpegurl";
|
||||||
override val codec: String = "HLS";
|
override val codec: String = "HLS";
|
||||||
override val name : String;
|
override val name : String;
|
||||||
@@ -31,9 +31,6 @@ class JSHLSManifestAudioSource : IAudioUrlSource, IHLSManifestAudioSource, JSSou
|
|||||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAudioUrl(): String {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8HLSNullable(config: IV8PluginConfig, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(config, it as V8ValueObject) };
|
fun fromV8HLSNullable(config: IV8PluginConfig, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(config, it as V8ValueObject) };
|
||||||
|
|||||||
+1
-5
@@ -7,7 +7,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
|
|||||||
import com.futo.platformplayer.getOrNull
|
import com.futo.platformplayer.getOrNull
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
class JSHLSManifestSource : IVideoUrlSource, IHLSManifestSource, JSSource {
|
class JSHLSManifestSource : IHLSManifestSource, JSSource {
|
||||||
override val width : Int = 0;
|
override val width : Int = 0;
|
||||||
override val height : Int = 0;
|
override val height : Int = 0;
|
||||||
override val container : String get() = "application/vnd.apple.mpegurl";
|
override val container : String get() = "application/vnd.apple.mpegurl";
|
||||||
@@ -28,8 +28,4 @@ class JSHLSManifestSource : IVideoUrlSource, IHLSManifestSource, JSSource {
|
|||||||
|
|
||||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getVideoUrl(): String {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,8 @@ class DedupContentPager : IPager<IPlatformContent>, IAsyncPager<IPlatformContent
|
|||||||
_currentResults = dedupResults(_basePager.getResults());
|
_currentResults = dedupResults(_basePager.getResults());
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hasMorePages(): Boolean = _basePager.hasMorePages();
|
override fun hasMorePages(): Boolean =
|
||||||
|
_basePager.hasMorePages();
|
||||||
override fun nextPage() {
|
override fun nextPage() {
|
||||||
_basePager.nextPage()
|
_basePager.nextPage()
|
||||||
_currentResults = dedupResults(_basePager.getResults());
|
_currentResults = dedupResults(_basePager.getResults());
|
||||||
@@ -74,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()));
|
||||||
|
|||||||
+2
-1
@@ -7,7 +7,8 @@ import java.util.stream.IntStream
|
|||||||
* A PlatformContent MultiPager that orders the results of a page based on the datetime of a content item
|
* A PlatformContent MultiPager that orders the results of a page based on the datetime of a content item
|
||||||
*/
|
*/
|
||||||
class MultiChronoContentPager : MultiPager<IPlatformContent> {
|
class MultiChronoContentPager : MultiPager<IPlatformContent> {
|
||||||
constructor(pagers : Array<IPager<IPlatformContent>>, allowFailure: Boolean = false) : super(pagers.map { it }.toList(), allowFailure) {}
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ abstract class MultiPager<T> : IPager<T> {
|
|||||||
protected val _subSinglePagers : MutableList<SingleItemPager<T>>;
|
protected val _subSinglePagers : MutableList<SingleItemPager<T>>;
|
||||||
protected val _failedPagers: ArrayList<IPager<T>> = arrayListOf();
|
protected val _failedPagers: ArrayList<IPager<T>> = arrayListOf();
|
||||||
|
|
||||||
private val _pageSize : Int = 9;
|
private var _pageSize : Int = 9;
|
||||||
|
|
||||||
private var _didInitialize = false;
|
private var _didInitialize = false;
|
||||||
|
|
||||||
@@ -27,7 +27,8 @@ abstract class MultiPager<T> : IPager<T> {
|
|||||||
|
|
||||||
val totalPagers: Int get() = _pagers.size;
|
val totalPagers: Int get() = _pagers.size;
|
||||||
|
|
||||||
constructor(pagers : List<IPager<T>>, allowFailure: Boolean = false) {
|
constructor(pagers : List<IPager<T>>, allowFailure: Boolean = false, pageSize: Int = 9) {
|
||||||
|
this._pageSize = pageSize;
|
||||||
this.allowFailure = allowFailure;
|
this.allowFailure = allowFailure;
|
||||||
_pagers = pagers.toMutableList();
|
_pagers = pagers.toMutableList();
|
||||||
_subSinglePagers = _pagers.map { SingleItemPager(it) }.toMutableList();
|
_subSinglePagers = _pagers.map { SingleItemPager(it) }.toMutableList();
|
||||||
|
|||||||
+2
-2
@@ -137,11 +137,11 @@ abstract class MultiParallelPager<T> : IPager<T>, IAsyncPager<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Logger.i(TAG, "Pager prepare in ${timeForPage}ms");
|
Logger.v(TAG, "Pager prepare in ${timeForPage}ms");
|
||||||
val timeAwait = measureTimeMillis {
|
val timeAwait = measureTimeMillis {
|
||||||
_currentResults = results.map { it.await() }.mapNotNull { it };
|
_currentResults = results.map { it.await() }.mapNotNull { it };
|
||||||
};
|
};
|
||||||
Logger.i(TAG, "Pager load in ${timeAwait}ms");
|
Logger.v(TAG, "Pager load in ${timeAwait}ms");
|
||||||
|
|
||||||
_currentResultExceptions = exceptions;
|
_currentResultExceptions = exceptions;
|
||||||
return _currentResults;
|
return _currentResults;
|
||||||
|
|||||||
+25
-4
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.api.media.structures
|
package com.futo.platformplayer.api.media.structures
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.PlatformContentPlaceholder
|
||||||
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
|
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -37,8 +38,12 @@ abstract class MultiRefreshPager<T>: IRefreshPager<T>, IPager<T> {
|
|||||||
synchronized(_pending) {
|
synchronized(_pending) {
|
||||||
_pending.remove(pendingPager);
|
_pending.remove(pendingPager);
|
||||||
}
|
}
|
||||||
if(error != null)
|
if(error != null) {
|
||||||
onPagerError.emit(error);
|
onPagerError.emit(error);
|
||||||
|
val replacing = _placeHolderPagersPaired[pendingPager];
|
||||||
|
if(replacing != null)
|
||||||
|
updatePager(null, replacing, error);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
updatePager(pendingPager.getCompleted());
|
updatePager(pendingPager.getCompleted());
|
||||||
}
|
}
|
||||||
@@ -60,10 +65,26 @@ abstract class MultiRefreshPager<T>: IRefreshPager<T>, IPager<T> {
|
|||||||
override fun nextPage() = synchronized(_pagersReusable){ _currentPager.nextPage() };
|
override fun nextPage() = synchronized(_pagersReusable){ _currentPager.nextPage() };
|
||||||
override fun getResults(): List<T> = synchronized(_pagersReusable){ _currentPager.getResults() };
|
override fun getResults(): List<T> = synchronized(_pagersReusable){ _currentPager.getResults() };
|
||||||
|
|
||||||
private fun updatePager(pagerToAdd: IPager<T>?) {
|
private fun updatePager(pagerToAdd: IPager<T>?, toReplacePager: IPager<T>? = null, error: Throwable? = null) {
|
||||||
if(pagerToAdd == null)
|
|
||||||
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());
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|||||||
* A placeholder pager simply generates PlatformContent by some creator function.
|
* A placeholder pager simply generates PlatformContent by some creator function.
|
||||||
*/
|
*/
|
||||||
class PlaceholderPager : IPager<IPlatformContent> {
|
class PlaceholderPager : IPager<IPlatformContent> {
|
||||||
private val _creator: ()->IPlatformContent;
|
val placeholderFactory: ()->IPlatformContent;
|
||||||
private val _pageSize: Int;
|
private val _pageSize: Int;
|
||||||
|
|
||||||
constructor(pageSize: Int, placeholderCreator: ()->IPlatformContent) {
|
constructor(pageSize: Int, placeholderCreator: ()->IPlatformContent) {
|
||||||
_creator = placeholderCreator;
|
placeholderFactory = placeholderCreator;
|
||||||
_pageSize = pageSize;
|
_pageSize = pageSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ class PlaceholderPager : IPager<IPlatformContent> {
|
|||||||
override fun getResults(): List<IPlatformContent> {
|
override fun getResults(): List<IPlatformContent> {
|
||||||
val pages = ArrayList<IPlatformContent>();
|
val pages = ArrayList<IPlatformContent>();
|
||||||
for(item in 1.._pageSize)
|
for(item in 1.._pageSize)
|
||||||
pages.add(_creator());
|
pages.add(placeholderFactory());
|
||||||
return pages;
|
return pages;
|
||||||
}
|
}
|
||||||
override fun hasMorePages(): Boolean = true;
|
override fun hasMorePages(): Boolean = true;
|
||||||
|
|||||||
+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> {
|
||||||
@@ -111,10 +155,10 @@ class ChannelContentCache {
|
|||||||
init {
|
init {
|
||||||
val results = pager.getResults();
|
val results = pager.getResults();
|
||||||
|
|
||||||
Logger.i(TAG, "Caching ${results.size} subscription initial results");
|
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
|
||||||
@@ -64,7 +65,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun serviceResolved(event: ServiceEvent) {
|
override fun serviceResolved(event: ServiceEvent) {
|
||||||
Logger.i(TAG, "ChromeCast service resolved: " + event.info);
|
Logger.v(TAG, "ChromeCast service resolved: " + event.info);
|
||||||
addOrUpdateDevice(event);
|
addOrUpdateDevice(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.constructs
|
package com.futo.platformplayer.constructs
|
||||||
|
|
||||||
|
import android.provider.Settings.Global
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
@@ -39,8 +40,7 @@ class BatchedTaskHandler<TParameter, TResult> {
|
|||||||
|
|
||||||
//Cached
|
//Cached
|
||||||
if(result != null)
|
if(result != null)
|
||||||
//TODO: Replace with some kind of constant Deferred<IPlatformStreamVideo>
|
return CompletableDeferred(result as TResult);
|
||||||
return _scope.async { result as TResult }
|
|
||||||
//Already requesting
|
//Already requesting
|
||||||
if(taskResult != null)
|
if(taskResult != null)
|
||||||
return taskResult as Deferred<TResult>;
|
return taskResult as Deferred<TResult>;
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ abstract class EventBase<Handler, ConditionalHandler>: IEvent {
|
|||||||
protected val _conditionalListeners = mutableListOf<TaggedHandler<ConditionalHandler>>();
|
protected val _conditionalListeners = mutableListOf<TaggedHandler<ConditionalHandler>>();
|
||||||
protected val _listeners = mutableListOf<TaggedHandler<Handler>>();
|
protected val _listeners = mutableListOf<TaggedHandler<Handler>>();
|
||||||
|
|
||||||
|
fun hasListeners(): Boolean =
|
||||||
|
synchronized(_listeners){_listeners.isNotEmpty()} ||
|
||||||
|
synchronized(_conditionalListeners){_conditionalListeners.isNotEmpty()};
|
||||||
|
|
||||||
fun subscribeConditional(listener: ConditionalHandler) {
|
fun subscribeConditional(listener: ConditionalHandler) {
|
||||||
synchronized(_conditionalListeners) {
|
synchronized(_conditionalListeners) {
|
||||||
_conditionalListeners.add(TaggedHandler(listener));
|
_conditionalListeners.add(TaggedHandler(listener));
|
||||||
@@ -65,10 +69,7 @@ abstract class EventBase<Handler, ConditionalHandler>: IEvent {
|
|||||||
|
|
||||||
class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
|
class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
|
||||||
fun emit() : Boolean {
|
fun emit() : Boolean {
|
||||||
var handled: Boolean;
|
var handled = false;
|
||||||
synchronized(_listeners) {
|
|
||||||
handled = _listeners.isNotEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_conditionalListeners) {
|
synchronized(_conditionalListeners) {
|
||||||
for (conditional in _conditionalListeners)
|
for (conditional in _conditionalListeners)
|
||||||
@@ -76,6 +77,7 @@ class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
synchronized(_listeners) {
|
synchronized(_listeners) {
|
||||||
|
handled = handled || _listeners.isNotEmpty();
|
||||||
for (handler in _listeners)
|
for (handler in _listeners)
|
||||||
handler.handler.invoke();
|
handler.handler.invoke();
|
||||||
}
|
}
|
||||||
@@ -85,17 +87,14 @@ class Event0() : EventBase<(()->Unit), (()->Boolean)>() {
|
|||||||
}
|
}
|
||||||
class Event1<T1>() : EventBase<((T1)->Unit), ((T1)->Boolean)>() {
|
class Event1<T1>() : EventBase<((T1)->Unit), ((T1)->Boolean)>() {
|
||||||
fun emit(value : T1): Boolean {
|
fun emit(value : T1): Boolean {
|
||||||
var handled: Boolean;
|
var handled = false;
|
||||||
synchronized(_listeners) {
|
|
||||||
handled = _listeners.isNotEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_conditionalListeners) {
|
synchronized(_conditionalListeners) {
|
||||||
for (conditional in _conditionalListeners)
|
for (conditional in _conditionalListeners)
|
||||||
handled = handled || conditional.handler.invoke(value);
|
handled = handled || conditional.handler.invoke(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized(_listeners) {
|
synchronized(_listeners) {
|
||||||
|
handled = handled || _listeners.isNotEmpty();
|
||||||
for (handler in _listeners)
|
for (handler in _listeners)
|
||||||
handler.handler.invoke(value);
|
handler.handler.invoke(value);
|
||||||
}
|
}
|
||||||
@@ -105,10 +104,7 @@ class Event1<T1>() : EventBase<((T1)->Unit), ((T1)->Boolean)>() {
|
|||||||
}
|
}
|
||||||
class Event2<T1, T2>() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() {
|
class Event2<T1, T2>() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() {
|
||||||
fun emit(value1 : T1, value2 : T2): Boolean {
|
fun emit(value1 : T1, value2 : T2): Boolean {
|
||||||
var handled: Boolean;
|
var handled = false;
|
||||||
synchronized(_listeners) {
|
|
||||||
handled = _listeners.isNotEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_conditionalListeners) {
|
synchronized(_conditionalListeners) {
|
||||||
for (conditional in _conditionalListeners)
|
for (conditional in _conditionalListeners)
|
||||||
@@ -116,6 +112,7 @@ class Event2<T1, T2>() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
synchronized(_listeners) {
|
synchronized(_listeners) {
|
||||||
|
handled = handled || _listeners.isNotEmpty();
|
||||||
for (handler in _listeners)
|
for (handler in _listeners)
|
||||||
handler.handler.invoke(value1, value2);
|
handler.handler.invoke(value1, value2);
|
||||||
}
|
}
|
||||||
@@ -126,10 +123,7 @@ class Event2<T1, T2>() : EventBase<((T1, T2)->Unit), ((T1, T2)->Boolean)>() {
|
|||||||
|
|
||||||
class Event3<T1, T2, T3>() : EventBase<((T1, T2, T3)->Unit), ((T1, T2, T3)->Boolean)>() {
|
class Event3<T1, T2, T3>() : EventBase<((T1, T2, T3)->Unit), ((T1, T2, T3)->Boolean)>() {
|
||||||
fun emit(value1 : T1, value2 : T2, value3 : T3): Boolean {
|
fun emit(value1 : T1, value2 : T2, value3 : T3): Boolean {
|
||||||
var handled: Boolean;
|
var handled = false;
|
||||||
synchronized(_listeners) {
|
|
||||||
handled = _listeners.isNotEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_conditionalListeners) {
|
synchronized(_conditionalListeners) {
|
||||||
for (conditional in _conditionalListeners)
|
for (conditional in _conditionalListeners)
|
||||||
@@ -137,6 +131,7 @@ class Event3<T1, T2, T3>() : EventBase<((T1, T2, T3)->Unit), ((T1, T2, T3)->Bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
synchronized(_listeners) {
|
synchronized(_listeners) {
|
||||||
|
handled = handled || _listeners.isNotEmpty();
|
||||||
for (handler in _listeners)
|
for (handler in _listeners)
|
||||||
handler.handler.invoke(value1, value2, value3);
|
handler.handler.invoke(value1, value2, value3);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class TaskHandler<TParameter, TResult> {
|
|||||||
fun run(parameter: TParameter) {
|
fun run(parameter: TParameter) {
|
||||||
val id = ++_idGenerator;
|
val id = ++_idGenerator;
|
||||||
|
|
||||||
|
var handled = false;
|
||||||
_scope().launch(_dispatcher) {
|
_scope().launch(_dispatcher) {
|
||||||
if (id != _idGenerator)
|
if (id != _idGenerator)
|
||||||
return@launch;
|
return@launch;
|
||||||
@@ -67,35 +68,53 @@ class TaskHandler<TParameter, TResult> {
|
|||||||
return@launch;
|
return@launch;
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (id != _idGenerator)
|
if (id != _idGenerator) {
|
||||||
|
handled = true;
|
||||||
return@withContext;
|
return@withContext;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
onSuccess.emit(result);
|
onSuccess.emit(result);
|
||||||
|
handled = true;
|
||||||
}
|
}
|
||||||
catch (e: Throwable) {
|
catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Handled exception in TaskHandler onSuccess.", e);
|
Logger.w(TAG, "Handled exception in TaskHandler onSuccess.", e);
|
||||||
onError.emit(e, parameter);
|
onError.emit(e, parameter);
|
||||||
|
handled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e: Throwable) {
|
catch (e: Throwable) {
|
||||||
Log.i("TaskHandler", "TaskHandler.run in exception: " + e.message);
|
Log.i("TaskHandler", "TaskHandler.run in exception: " + e.message);
|
||||||
if (id != _idGenerator)
|
if (id != _idGenerator) {
|
||||||
|
handled = true;
|
||||||
return@launch;
|
return@launch;
|
||||||
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
handled = true;
|
||||||
if (id != _idGenerator)
|
if (id != _idGenerator)
|
||||||
return@withContext;
|
return@withContext;
|
||||||
|
|
||||||
if (!onError.emit(e, parameter)) {
|
if (!onError.emit(e, parameter)) {
|
||||||
Logger.e(TAG, "Uncaught exception handled by TaskHandler.", e);
|
Logger.e(TAG, "Uncaught exception handled by TaskHandler.", e);
|
||||||
} else {
|
} else {
|
||||||
Logger.w(TAG, "Handled exception in TaskHandler invoke.", e);
|
//Logger.w(TAG, "Handled exception in TaskHandler invoke.", e); (Prevents duplicate logs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}/*.invokeOnCompletion { //Commented for now, because it doesn't fix the bug it was intended to fix, but might want it later anyway
|
||||||
|
if(!handled) {
|
||||||
|
if(it is CancellationException) {
|
||||||
|
Logger.w(TAG, "Detected unhandled TaskHandler due to cancellation, forwarding cancellation");
|
||||||
|
onError.emit(it, parameter);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//TODO: Forward exception?
|
||||||
|
Logger.w(TAG, "Detected unhandled TaskHandler due to [${it}]", it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
|||||||
@@ -5,6 +5,16 @@ import com.google.android.exoplayer2.util.Log
|
|||||||
class Stopwatch {
|
class Stopwatch {
|
||||||
var startTime = System.nanoTime()
|
var startTime = System.nanoTime()
|
||||||
|
|
||||||
|
val elapsedMs: Double get() {
|
||||||
|
val now = System.nanoTime()
|
||||||
|
val diff = now - startTime
|
||||||
|
return diff / 1000000.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
startTime = System.nanoTime()
|
||||||
|
}
|
||||||
|
|
||||||
fun logAndNext(tag: String, message: String): Long {
|
fun logAndNext(tag: String, message: String): Long {
|
||||||
val now = System.nanoTime()
|
val now = System.nanoTime()
|
||||||
val diff = now - startTime
|
val diff = now - startTime
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.futo.platformplayer.developer
|
package com.futo.platformplayer.developer
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.futo.platformplayer.activities.CaptchaActivity
|
||||||
import com.futo.platformplayer.activities.LoginActivity
|
import com.futo.platformplayer.activities.LoginActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.http.server.HttpContext
|
import com.futo.platformplayer.api.http.server.HttpContext
|
||||||
@@ -201,6 +202,28 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
context.respondCode(500, (ex::class.simpleName + ":" + ex.message) ?: "", "text/plain")
|
context.respondCode(500, (ex::class.simpleName + ":" + ex.message) ?: "", "text/plain")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@HttpPOST("/plugin/captchaTestPlugin")
|
||||||
|
fun pluginCaptchaTestPlugin(context: HttpContext) {
|
||||||
|
val config = _testPlugin?.config as SourcePluginConfig;
|
||||||
|
val url = context.query.get("url")
|
||||||
|
val html = context.readContentString();
|
||||||
|
try {
|
||||||
|
val captchaConfig = config.captcha;
|
||||||
|
if(captchaConfig == null) {
|
||||||
|
context.respondCode(403, "This plugin doesn't support captcha");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CaptchaActivity.showCaptcha(StateApp.instance.context, config, url, html) {
|
||||||
|
_testPluginVariables.clear();
|
||||||
|
_testPlugin = V8Plugin(StateApp.instance.context, config, null, JSHttpClient(null, null, it), JSHttpClient(null, null, it));
|
||||||
|
|
||||||
|
};
|
||||||
|
context.respondCode(200, "Captcha started");
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
context.respondCode(500, (ex::class.simpleName + ":" + ex.message) ?: "", "text/plain")
|
||||||
|
}
|
||||||
|
}
|
||||||
@HttpGET("/plugin/loginTestPlugin")
|
@HttpGET("/plugin/loginTestPlugin")
|
||||||
fun pluginLoginTestPlugin(context: HttpContext) {
|
fun pluginLoginTestPlugin(context: HttpContext) {
|
||||||
val config = _testPlugin?.config as SourcePluginConfig;
|
val config = _testPlugin?.config as SourcePluginConfig;
|
||||||
@@ -416,7 +439,7 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
val resp = _client.get(body.url!!, body.headers);
|
val resp = _client.get(body.url!!, body.headers);
|
||||||
|
|
||||||
context.respondCode(200,
|
context.respondCode(200,
|
||||||
Json.encodeToString(PackageHttp.BridgeHttpResponse(resp.code, resp.body?.string())),
|
Json.encodeToString(PackageHttp.BridgeHttpResponse(resp.url, resp.code, resp.body?.string())),
|
||||||
context.query.getOrDefault("CT", "text/plain"));
|
context.query.getOrDefault("CT", "text/plain"));
|
||||||
}
|
}
|
||||||
catch(ex: Exception) {
|
catch(ex: Exception) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateBackup
|
import com.futo.platformplayer.states.StateBackup
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
|
|
||||||
@@ -58,13 +59,13 @@ class AutomaticBackupDialog(context: Context) : AlertDialog(context) {
|
|||||||
}
|
}
|
||||||
clearFocus();
|
clearFocus();
|
||||||
dismiss();
|
dismiss();
|
||||||
|
|
||||||
Logger.i(TAG, "Set AutoBackupPassword");
|
Logger.i(TAG, "Set AutoBackupPassword");
|
||||||
Settings.instance.backup.autoBackupPassword = _editPassword.text.toString();
|
Settings.instance.backup.autoBackupPassword = _editPassword.text.toString();
|
||||||
Settings.instance.backup.didAskAutoBackup = true;
|
Settings.instance.backup.didAskAutoBackup = true;
|
||||||
Settings.instance.save();
|
Settings.instance.save();
|
||||||
|
|
||||||
UIDialogs.toast(context, "AutoBackup enabled");
|
UIDialogs.toast(context, "AutoBackup enabled");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
StateBackup.startAutomaticBackup(true);
|
StateBackup.startAutomaticBackup(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,17 @@ package com.futo.platformplayer.dialogs
|
|||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.Editable
|
||||||
|
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
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
@@ -32,6 +37,8 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
private lateinit var _buttonCancel: MaterialButton;
|
private lateinit var _buttonCancel: MaterialButton;
|
||||||
private lateinit var _editComment: EditText;
|
private lateinit var _editComment: EditText;
|
||||||
private lateinit var _inputMethodManager: InputMethodManager;
|
private lateinit var _inputMethodManager: InputMethodManager;
|
||||||
|
private lateinit var _textCharacterCount: TextView;
|
||||||
|
private lateinit var _textCharacterCountMax: TextView;
|
||||||
|
|
||||||
val onCommentAdded = Event1<IPlatformComment>();
|
val onCommentAdded = Event1<IPlatformComment>();
|
||||||
|
|
||||||
@@ -42,6 +49,26 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
_buttonCancel = findViewById(R.id.button_cancel);
|
_buttonCancel = findViewById(R.id.button_cancel);
|
||||||
_buttonCreate = findViewById(R.id.button_create);
|
_buttonCreate = findViewById(R.id.button_create);
|
||||||
_editComment = findViewById(R.id.edit_comment);
|
_editComment = findViewById(R.id.edit_comment);
|
||||||
|
_textCharacterCount = findViewById(R.id.character_count);
|
||||||
|
_textCharacterCountMax = findViewById(R.id.character_count_max);
|
||||||
|
|
||||||
|
_editComment.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun afterTextChanged(s: Editable?) = Unit
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||||
|
_textCharacterCount.text = count.toString();
|
||||||
|
|
||||||
|
if (count > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
|
||||||
|
_textCharacterCount.setTextColor(Color.RED);
|
||||||
|
_textCharacterCountMax.setTextColor(Color.RED);
|
||||||
|
_buttonCreate.alpha = 0.4f;
|
||||||
|
} else {
|
||||||
|
_textCharacterCount.setTextColor(Color.WHITE);
|
||||||
|
_textCharacterCountMax.setTextColor(Color.WHITE);
|
||||||
|
_buttonCreate.alpha = 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
||||||
|
|
||||||
@@ -53,13 +80,20 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
_buttonCreate.setOnClickListener {
|
_buttonCreate.setOnClickListener {
|
||||||
clearFocus();
|
clearFocus();
|
||||||
|
|
||||||
|
if (_editComment.text.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
|
||||||
|
UIDialogs.toast(context, "Comment should be less than 5000 characters");
|
||||||
|
return@setOnClickListener;
|
||||||
|
}
|
||||||
|
|
||||||
val comment = _editComment.text.toString();
|
val comment = _editComment.text.toString();
|
||||||
val processHandle = StatePolycentric.instance.processHandle!!
|
val processHandle = StatePolycentric.instance.processHandle!!
|
||||||
val eventPointer = processHandle.post(comment, null, ref)
|
val eventPointer = processHandle.post(comment, null, ref)
|
||||||
|
|
||||||
StateApp.instance.scopeGetter().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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
package com.futo.platformplayer.downloads
|
package com.futo.platformplayer.downloads
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
data class PlaylistDownloadDescriptor(
|
data class PlaylistDownloadDescriptor(
|
||||||
val id: String,
|
val id: String,
|
||||||
val targetPxCount: Long?,
|
val targetPxCount: Long?,
|
||||||
val targetBitrate: Long?
|
val targetBitrate: Long?
|
||||||
);
|
) {
|
||||||
|
var preventDownload: MutableList<String> = arrayListOf();
|
||||||
|
|
||||||
|
fun getPreventDownloadList(): List<String> = synchronized(preventDownload){ preventDownload };
|
||||||
|
fun shouldDownload(video: IPlatformVideo): Boolean {
|
||||||
|
synchronized(preventDownload) {
|
||||||
|
return !preventDownload.contains(video.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,12 +13,16 @@ 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.models.video.SerializedPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.exceptions.DownloadException
|
||||||
|
import com.futo.platformplayer.hasAnySource
|
||||||
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import com.futo.platformplayer.isDownloadable
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
import com.futo.platformplayer.toHumanBitrate
|
import com.futo.platformplayer.toHumanBitrate
|
||||||
import com.futo.platformplayer.toHumanBytesSpeed
|
import com.futo.platformplayer.toHumanBytesSpeed
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
@@ -27,7 +31,6 @@ import java.io.File
|
|||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.CancellationException
|
|
||||||
import java.util.concurrent.ForkJoinPool
|
import java.util.concurrent.ForkJoinPool
|
||||||
import java.util.concurrent.ForkJoinTask
|
import java.util.concurrent.ForkJoinTask
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
@@ -147,27 +150,37 @@ class VideoDownload {
|
|||||||
if(original !is IPlatformVideoDetails)
|
if(original !is IPlatformVideoDetails)
|
||||||
throw IllegalStateException("Original content is not media?");
|
throw IllegalStateException("Original content is not media?");
|
||||||
|
|
||||||
|
if(original.video.hasAnySource() && !original.isDownloadable()) {
|
||||||
|
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
|
||||||
|
throw DownloadException("Unsupported video for downloading", false);
|
||||||
|
}
|
||||||
|
|
||||||
videoDetails = SerializedPlatformVideoDetails.fromVideo(original, if (subtitleSource != null) listOf(subtitleSource!!) else listOf());
|
videoDetails = SerializedPlatformVideoDetails.fromVideo(original, if (subtitleSource != null) listOf(subtitleSource!!) else listOf());
|
||||||
if(videoSource == null && targetPixelCount != null) {
|
if(videoSource == null && targetPixelCount != null) {
|
||||||
val vsource = VideoHelper.selectBestVideoSource(videoDetails!!.video, targetPixelCount!!.toInt(), arrayOf())
|
val vsource = VideoHelper.selectBestVideoSource(videoDetails!!.video, targetPixelCount!!.toInt(), arrayOf())
|
||||||
?: throw IllegalStateException("Could not find a valid video source for video");
|
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
||||||
if(vsource is IVideoUrlSource)
|
if(vsource != null) {
|
||||||
videoSource = VideoUrlSource.fromUrlSource(vsource);
|
if (vsource is IVideoUrlSource)
|
||||||
else
|
videoSource = VideoUrlSource.fromUrlSource(vsource);
|
||||||
throw IllegalStateException("Download video source is not a url source");
|
else
|
||||||
|
throw DownloadException("Video source is not supported for downloading (yet)", false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(audioSource == null && targetBitrate != null) {
|
if(audioSource == null && targetBitrate != null) {
|
||||||
val asource = VideoHelper.selectBestAudioSource(videoDetails!!.video, arrayOf(), null, targetPixelCount)
|
val asource = VideoHelper.selectBestAudioSource(videoDetails!!.video, arrayOf(), null, targetPixelCount)
|
||||||
?: if(videoSource != null ) null
|
?: if(videoSource != null ) null
|
||||||
else throw IllegalStateException("Could not find a valid audio source for video");
|
else throw DownloadException("Could not find a valid video or audio source for download")
|
||||||
if(asource == null)
|
if(asource == null)
|
||||||
audioSource = null;
|
audioSource = null;
|
||||||
else if(asource is IAudioUrlSource)
|
else if(asource is IAudioUrlSource)
|
||||||
audioSource = AudioUrlSource.fromUrlSource(asource);
|
audioSource = AudioUrlSource.fromUrlSource(asource);
|
||||||
else
|
else
|
||||||
throw IllegalStateException("Download audio source is not a url source");
|
throw DownloadException("Audio source is not supported for downloading (yet)", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(videoSource == null && audioSource == null)
|
||||||
|
throw DownloadException("No valid sources found for video/audio");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
suspend fun download(client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
|
suspend fun download(client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
|
||||||
@@ -358,7 +371,7 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isCancelled)
|
if (isCancelled)
|
||||||
throw IllegalStateException("Cancelled");
|
throw CancellationException("Cancelled");
|
||||||
} while (read > 0);
|
} while (read > 0);
|
||||||
|
|
||||||
lastSpeed = 0;
|
lastSpeed = 0;
|
||||||
@@ -410,7 +423,7 @@ class VideoDownload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(isCancelled)
|
if(isCancelled)
|
||||||
throw IllegalStateException("Cancelled");
|
throw CancellationException("Cancelled", null);
|
||||||
}
|
}
|
||||||
onProgress(sourceLength, totalRead, 0);
|
onProgress(sourceLength, totalRead, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,24 +18,41 @@ import com.futo.platformplayer.engine.internal.V8Converter
|
|||||||
import com.futo.platformplayer.engine.packages.*
|
import com.futo.platformplayer.engine.packages.*
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateAssets
|
import com.futo.platformplayer.states.StateAssets
|
||||||
import kotlinx.coroutines.*
|
|
||||||
|
|
||||||
class V8Plugin {
|
class V8Plugin {
|
||||||
val config: IV8PluginConfig;
|
val config: IV8PluginConfig;
|
||||||
private val _client: ManagedHttpClient;
|
private val _client: ManagedHttpClient;
|
||||||
private val _clientAuth: ManagedHttpClient;
|
private val _clientAuth: ManagedHttpClient;
|
||||||
|
|
||||||
|
|
||||||
val httpClient: ManagedHttpClient get() = _client;
|
val httpClient: ManagedHttpClient get() = _client;
|
||||||
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
|
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
|
||||||
|
|
||||||
|
private val _runtimeLock = Object();
|
||||||
var _runtime : V8Runtime? = null;
|
var _runtime : V8Runtime? = null;
|
||||||
|
|
||||||
private val _deps : LinkedHashMap<String, String> = LinkedHashMap();
|
private val _deps : LinkedHashMap<String, String> = LinkedHashMap();
|
||||||
private val _depsPackages : MutableList<V8Package> = mutableListOf();
|
private val _depsPackages : MutableList<V8Package> = mutableListOf();
|
||||||
private var _script : String? = null;
|
private var _script : String? = null;
|
||||||
|
|
||||||
|
var isStopped = true;
|
||||||
val onStopped = Event1<V8Plugin>();
|
val onStopped = Event1<V8Plugin>();
|
||||||
|
|
||||||
|
//TODO: Implement a more universal isBusy system for plugins + JSClient + pooling? TBD if propagation would be beneficial
|
||||||
|
private val _busyCounterLock = Object();
|
||||||
|
private var _busyCounter = 0;
|
||||||
|
val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before a busy counter is about to be removed.
|
||||||
|
* Is primarily used to prevent additional calls to dead runtimes.
|
||||||
|
*
|
||||||
|
* Parameter is the busy count after this execution
|
||||||
|
*/
|
||||||
|
val afterBusy = Event1<Int>();
|
||||||
|
|
||||||
|
val onScriptException = Event1<ScriptException>();
|
||||||
|
|
||||||
constructor(context: Context, config: IV8PluginConfig, script: String? = null, client: ManagedHttpClient = ManagedHttpClient(), clientAuth: ManagedHttpClient = ManagedHttpClient()) {
|
constructor(context: Context, config: IV8PluginConfig, script: String? = null, client: ManagedHttpClient = ManagedHttpClient(), clientAuth: ManagedHttpClient = ManagedHttpClient()) {
|
||||||
this._client = client;
|
this._client = client;
|
||||||
this._clientAuth = clientAuth;
|
this._clientAuth = clientAuth;
|
||||||
@@ -78,7 +95,7 @@ class V8Plugin {
|
|||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
val script = _script ?: throw IllegalStateException("Attempted to start V8 without script");
|
val script = _script ?: throw IllegalStateException("Attempted to start V8 without script");
|
||||||
synchronized(this) {
|
synchronized(_runtimeLock) {
|
||||||
if (_runtime != null)
|
if (_runtime != null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -118,32 +135,79 @@ class V8Plugin {
|
|||||||
catchScriptErrors("Plugin[${config.name}]") {
|
catchScriptErrors("Plugin[${config.name}]") {
|
||||||
it.getExecutor(script).executeVoid()
|
it.getExecutor(script).executeVoid()
|
||||||
};
|
};
|
||||||
|
isStopped = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun stop(){
|
fun stop(){
|
||||||
Logger.i(TAG, "Stopping plugin [${config.name}]");
|
Logger.i(TAG, "Stopping plugin [${config.name}]");
|
||||||
synchronized(this) {
|
isStopped = true;
|
||||||
_runtime?.let {
|
whenNotBusy {
|
||||||
_runtime = null;
|
synchronized(_runtimeLock) {
|
||||||
if(!it.isClosed && !it.isDead)
|
isStopped = true;
|
||||||
it.close();
|
_runtime?.let {
|
||||||
};
|
_runtime = null;
|
||||||
|
if(!it.isClosed && !it.isDead)
|
||||||
|
it.close();
|
||||||
|
Logger.i(TAG, "Stopped plugin [${config.name}]");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
onStopped.emit(this);
|
||||||
}
|
}
|
||||||
onStopped.emit(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun execute(js: String) : V8Value {
|
fun execute(js: String) : V8Value {
|
||||||
return executeTyped<V8Value>(js);
|
return executeTyped<V8Value>(js);
|
||||||
}
|
}
|
||||||
fun <T : V8Value> executeTyped(js: String) : T {
|
fun <T : V8Value> executeTyped(js: String) : T {
|
||||||
|
warnIfMainThread("V8Plugin.executeTyped");
|
||||||
|
if(isStopped)
|
||||||
|
throw PluginEngineStoppedException(config, "Instance is stopped", js);
|
||||||
|
|
||||||
|
synchronized(_busyCounterLock) {
|
||||||
|
_busyCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
|
val runtime = _runtime ?: throw IllegalStateException("JSPlugin not started yet");
|
||||||
return catchScriptErrors("Plugin[${config.name}]", js) { runtime.getExecutor(js).execute() };
|
try {
|
||||||
|
return catchScriptErrors("Plugin[${config.name}]", js) {
|
||||||
|
runtime.getExecutor(js).execute()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
synchronized(_busyCounterLock) {
|
||||||
|
//Free busy *after* afterBusy calls are done to prevent calls on dead runtimes
|
||||||
|
try {
|
||||||
|
afterBusy.emit(_busyCounter - 1);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Unhandled V8Plugin.afterBusy", ex);
|
||||||
|
}
|
||||||
|
_busyCounter--;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fun executeBoolean(js: String) : Boolean? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueBoolean>(js).value };
|
fun executeBoolean(js: String) : Boolean? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueBoolean>(js).value };
|
||||||
fun executeString(js: String) : String? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueString>(js).value };
|
fun executeString(js: String) : String? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueString>(js).value };
|
||||||
fun executeInteger(js: String) : Int? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueInteger>(js).value };
|
fun executeInteger(js: String) : Int? = catchScriptErrors("Plugin[${config.name}]") { executeTyped<V8ValueInteger>(js).value };
|
||||||
|
|
||||||
|
fun whenNotBusy(handler: (V8Plugin)->Unit) {
|
||||||
|
synchronized(_busyCounterLock) {
|
||||||
|
if(_busyCounter == 0)
|
||||||
|
handler(this);
|
||||||
|
else {
|
||||||
|
val tag = Object();
|
||||||
|
afterBusy.subscribe(tag) {
|
||||||
|
if(it == 0) {
|
||||||
|
Logger.w(TAG, "V8Plugin afterBusy handled");
|
||||||
|
afterBusy.remove(tag);
|
||||||
|
handler(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getPackage(context: Context, packageName: String): V8Package {
|
private fun getPackage(context: Context, packageName: String): V8Package {
|
||||||
//TODO: Auto get all package types?
|
//TODO: Auto get all package types?
|
||||||
return when(packageName) {
|
return when(packageName) {
|
||||||
@@ -155,7 +219,13 @@ class V8Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun <T : Any> catchScriptErrors(context: String, code: String? = null, handle: ()->T): T {
|
fun <T : Any> catchScriptErrors(context: String, code: String? = null, handle: ()->T): T {
|
||||||
return catchScriptErrors(this.config, context, code, handle);
|
try {
|
||||||
|
return catchScriptErrors(this.config, context, code, handle);
|
||||||
|
}
|
||||||
|
catch(ex: ScriptException) {
|
||||||
|
onScriptException.emit(ex);
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -180,7 +250,7 @@ class V8Plugin {
|
|||||||
if(result is V8ValueObject) {
|
if(result is V8ValueObject) {
|
||||||
val type = result.getString("plugin_type");
|
val type = result.getString("plugin_type");
|
||||||
if(type != null && type.endsWith("Exception"))
|
if(type != null && type.endsWith("Exception"))
|
||||||
Companion.throwExceptionFromV8(
|
throwExceptionFromV8(
|
||||||
config,
|
config,
|
||||||
result.getOrThrow(config, "plugin_type", "V8Plugin"),
|
result.getOrThrow(config, "plugin_type", "V8Plugin"),
|
||||||
result.getOrThrow(config, "message", "V8Plugin"),
|
result.getOrThrow(config, "message", "V8Plugin"),
|
||||||
@@ -197,19 +267,28 @@ class V8Plugin {
|
|||||||
throw ScriptCompilationException(config, "Compilation: ${scriptEx.message}\n(${scriptEx.scriptingError.lineNumber})[${scriptEx.scriptingError.startColumn}-${scriptEx.scriptingError.endColumn}]: ${scriptEx.scriptingError.sourceLine}", null, codeStripped);
|
throw ScriptCompilationException(config, "Compilation: ${scriptEx.message}\n(${scriptEx.scriptingError.lineNumber})[${scriptEx.scriptingError.startColumn}-${scriptEx.scriptingError.endColumn}]: ${scriptEx.scriptingError.sourceLine}", null, codeStripped);
|
||||||
}
|
}
|
||||||
catch(executeEx: JavetExecutionException) {
|
catch(executeEx: JavetExecutionException) {
|
||||||
val exMessage = extractJSExceptionMessage(executeEx);
|
if(executeEx.scriptingError?.context?.containsKey("plugin_type") == true) {
|
||||||
|
val pluginType = executeEx.scriptingError.context["plugin_type"].toString();
|
||||||
|
|
||||||
if(executeEx.scriptingError?.context?.containsKey("plugin_type") == true)
|
//Captcha
|
||||||
|
if (pluginType == "CaptchaRequiredException") {
|
||||||
|
throw ScriptCaptchaRequiredException(config,
|
||||||
|
executeEx.scriptingError.context["url"]?.toString(),
|
||||||
|
executeEx.scriptingError.context["body"]?.toString(),
|
||||||
|
executeEx, executeEx.scriptingError?.stack, codeStripped);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Others
|
||||||
throwExceptionFromV8(
|
throwExceptionFromV8(
|
||||||
config,
|
config,
|
||||||
executeEx.scriptingError.context["plugin_type"].toString(),
|
pluginType,
|
||||||
(exMessage ?: ""),
|
(extractJSExceptionMessage(executeEx) ?: ""),
|
||||||
executeEx,
|
executeEx,
|
||||||
executeEx.scriptingError?.stack,
|
executeEx.scriptingError?.stack,
|
||||||
codeStripped
|
codeStripped
|
||||||
);
|
);
|
||||||
|
}
|
||||||
throw ScriptExecutionException(config, "${exMessage}", null, executeEx.scriptingError?.stack, codeStripped);
|
throw ScriptExecutionException(config, extractJSExceptionMessage(executeEx) ?: "", null, executeEx.scriptingError?.stack, codeStripped);
|
||||||
}
|
}
|
||||||
catch(ex: Exception) {
|
catch(ex: Exception) {
|
||||||
throw ex;
|
throw ex;
|
||||||
@@ -219,6 +298,7 @@ class V8Plugin {
|
|||||||
private fun throwExceptionFromV8(config: IV8PluginConfig, pluginType: String, msg: String, innerEx: Exception? = null, stack: String? = null, code: String? = null) {
|
private fun throwExceptionFromV8(config: IV8PluginConfig, pluginType: String, msg: String, innerEx: Exception? = null, stack: String? = null, code: String? = null) {
|
||||||
when(pluginType) {
|
when(pluginType) {
|
||||||
"ScriptException" -> throw ScriptException(config, msg, innerEx, stack, code);
|
"ScriptException" -> throw ScriptException(config, msg, innerEx, stack, code);
|
||||||
|
"CriticalException" -> throw ScriptCriticalException(config, msg, innerEx, stack, code);
|
||||||
"AgeException" -> throw ScriptAgeException(config, msg, innerEx, stack, code);
|
"AgeException" -> throw ScriptAgeException(config, msg, innerEx, stack, code);
|
||||||
"UnavailableException" -> throw ScriptUnavailableException(config, msg, innerEx, stack, code);
|
"UnavailableException" -> throw ScriptUnavailableException(config, msg, innerEx, stack, code);
|
||||||
"ScriptExecutionException" -> throw ScriptExecutionException(config, msg, innerEx, stack, code);
|
"ScriptExecutionException" -> throw ScriptExecutionException(config, msg, innerEx, stack, code);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.futo.platformplayer.engine.exceptions
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
|
||||||
|
open class PluginEngineException(config: IV8PluginConfig, error: String, code: String? = null) : PluginException(config, error, null, code) {
|
||||||
|
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package com.futo.platformplayer.engine.exceptions
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
|
||||||
|
class PluginEngineStoppedException(config: IV8PluginConfig, error: String, code: String? = null) : PluginEngineException(config, error, code) {
|
||||||
|
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.futo.platformplayer.engine.exceptions
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
class ScriptCaptchaRequiredException(config: IV8PluginConfig, val url: String?, val body: String?, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, "Captcha required", ex, stack, code) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
|
||||||
|
val contextName = "ScriptCaptchaRequiredException";
|
||||||
|
return ScriptCaptchaRequiredException(config,
|
||||||
|
obj.getOrDefault<String>(config, "url", contextName, null),
|
||||||
|
obj.getOrDefault<String>(config, "body", contextName, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package com.futo.platformplayer.engine.exceptions
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
open class ScriptCriticalException(config: IV8PluginConfig, error: String, ex: Exception? = null, stack: String? = null, code: String? = null) : ScriptException(config, error, ex, stack, code) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromV8(config: IV8PluginConfig, obj: V8ValueObject) : ScriptException {
|
||||||
|
return ScriptCriticalException(config, obj.getOrThrow(config, "message", "ScriptCriticalException"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,11 +108,12 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class BridgeHttpResponse(val code: Int, val body: String?, val headers: Map<String, List<String>>? = null) : IV8Convertable {
|
class BridgeHttpResponse(val url: String, val code: Int, val body: String?, val headers: Map<String, List<String>>? = null) : IV8Convertable {
|
||||||
val isOk = code >= 200 && code < 300;
|
val isOk = code >= 200 && code < 300;
|
||||||
|
|
||||||
override fun toV8(runtime: V8Runtime): V8Value? {
|
override fun toV8(runtime: V8Runtime): V8Value? {
|
||||||
val obj = runtime.createV8ValueObject();
|
val obj = runtime.createV8ValueObject();
|
||||||
|
obj.set("url", url);
|
||||||
obj.set("code", code);
|
obj.set("code", code);
|
||||||
obj.set("body", body);
|
obj.set("body", body);
|
||||||
obj.set("headers", headers);
|
obj.set("headers", headers);
|
||||||
@@ -227,7 +228,7 @@ class PackageHttp: V8Package {
|
|||||||
val resp = client.requestMethod(method, url, headers);
|
val resp = client.requestMethod(method, url, headers);
|
||||||
val responseBody = resp.body?.string();
|
val responseBody = resp.body?.string();
|
||||||
logResponse(method, url, resp.code, resp.headers, responseBody);
|
logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -241,7 +242,7 @@ class PackageHttp: V8Package {
|
|||||||
val resp = client.requestMethod(method, url, body, headers);
|
val resp = client.requestMethod(method, url, body, headers);
|
||||||
val responseBody = resp.body?.string();
|
val responseBody = resp.body?.string();
|
||||||
logResponse(method, url, resp.code, resp.headers, responseBody);
|
logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -256,7 +257,7 @@ class PackageHttp: V8Package {
|
|||||||
val resp = client.get(url, headers);
|
val resp = client.get(url, headers);
|
||||||
val responseBody = resp.body?.string();
|
val responseBody = resp.body?.string();
|
||||||
logResponse("GET", url, resp.code, resp.headers, responseBody);
|
logResponse("GET", url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -270,7 +271,7 @@ class PackageHttp: V8Package {
|
|||||||
val resp = client.post(url, body, headers);
|
val resp = client.post(url, body, headers);
|
||||||
val responseBody = resp.body?.string();
|
val responseBody = resp.body?.string();
|
||||||
logResponse("POST", url, resp.code, resp.headers, responseBody);
|
logResponse("POST", url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -367,7 +368,7 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
//Forward timeouts
|
//Forward timeouts
|
||||||
catch(ex: SocketTimeoutException) {
|
catch(ex: SocketTimeoutException) {
|
||||||
return BridgeHttpResponse(408, null);
|
return BridgeHttpResponse("", 408, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,7 +462,7 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
//Forward timeouts
|
//Forward timeouts
|
||||||
catch(ex: SocketTimeoutException) {
|
catch(ex: SocketTimeoutException) {
|
||||||
return BridgeHttpResponse(408, null);
|
return BridgeHttpResponse("", 408, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.futo.platformplayer.exceptions
|
||||||
|
|
||||||
|
class DownloadException : Throwable {
|
||||||
|
val isRetryable: Boolean;
|
||||||
|
|
||||||
|
constructor(innerException: Throwable, retryable: Boolean = true): super(innerException) {
|
||||||
|
isRetryable = retryable;
|
||||||
|
}
|
||||||
|
constructor(msg: String, retryable: Boolean = true): super(msg) {
|
||||||
|
isRetryable = retryable;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.futo.platformplayer.exceptions
|
||||||
|
|
||||||
|
class RateLimitException : Throwable {
|
||||||
|
val pluginIds: List<String>;
|
||||||
|
|
||||||
|
constructor(pluginIds: List<String>): super() {
|
||||||
|
this.pluginIds = pluginIds ?: listOf();
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user