mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
+3
-2
@@ -4,6 +4,7 @@ variables:
|
||||
stages:
|
||||
- buildAndDeployApkUnstable
|
||||
- buildAndDeployApkStable
|
||||
- buildAndDeployPlaystore
|
||||
|
||||
buildAndDeployApkUnstable:
|
||||
stage: buildAndDeployApkUnstable
|
||||
@@ -25,8 +26,8 @@ buildAndDeployApkStable:
|
||||
- branches
|
||||
when: manual
|
||||
|
||||
buildAndDeployApkStable:
|
||||
stage: buildAndDeployApkStable
|
||||
buildAndDeployPlaystore:
|
||||
stage: buildAndDeployPlaystore
|
||||
script:
|
||||
- sh deploy-playstore.sh
|
||||
only:
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk 29
|
||||
minSdk 28
|
||||
targetSdk 33
|
||||
versionCode gitVersionCode
|
||||
versionName gitVersionName
|
||||
|
||||
@@ -217,6 +217,9 @@ function pluginUpdateTestPlugin(config) {
|
||||
}
|
||||
function pluginLoginTestPlugin() {
|
||||
return syncGET("/plugin/loginTestPlugin", {});
|
||||
}//captchaLoginTestPlugin
|
||||
function pluginCaptchaTestPlugin(url, html) {
|
||||
return syncPOST("/plugin/captchaTestPlugin?url=" + url, {}, html);
|
||||
}
|
||||
function pluginLogoutTestPlugin() {
|
||||
return syncGET("/plugin/logoutTestPlugin", {});
|
||||
|
||||
@@ -681,6 +681,9 @@
|
||||
});
|
||||
}, 1000);
|
||||
},
|
||||
captchaTestPlugin() {
|
||||
captchaLoginTestPlugin();
|
||||
},
|
||||
logoutTestPlugin() {
|
||||
pluginLogoutTestPlugin();
|
||||
},
|
||||
@@ -838,6 +841,12 @@
|
||||
this.Testing.lastResultError = "";
|
||||
}
|
||||
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);
|
||||
this.Testing.lastResult = ""
|
||||
if(ex.message)
|
||||
|
||||
@@ -72,6 +72,11 @@ class CaptchaRequiredException extends Error {
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
class CriticalException extends ScriptException {
|
||||
constructor(msg) {
|
||||
super("CriticalException", msg);
|
||||
}
|
||||
}
|
||||
class UnavailableException extends ScriptException {
|
||||
constructor(msg) {
|
||||
super("UnavailableException", msg);
|
||||
|
||||
@@ -4,7 +4,9 @@ import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.webkit.CookieManager
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.activities.*
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
@@ -43,7 +45,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
|
||||
@FormField(
|
||||
"Manage Polycentric identity", FieldForm.BUTTON,
|
||||
"Manage your Polycentric identity", -2
|
||||
"Manage your Polycentric identity", -3
|
||||
)
|
||||
fun managePolycentricIdentity() {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
@@ -55,6 +57,19 @@ class Settings : FragmentedStorageFileJson() {
|
||||
}
|
||||
}
|
||||
|
||||
@FormField(
|
||||
"Open FAQ", FieldForm.BUTTON,
|
||||
"Get answers to common questions", -2
|
||||
)
|
||||
fun openFAQ() {
|
||||
try {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://grayjay.app/faq.html"))
|
||||
SettingsActivity.getActivity()?.startActivity(browserIntent);
|
||||
} catch (e: Throwable) {
|
||||
//Ignored
|
||||
}
|
||||
}
|
||||
|
||||
@FormField(
|
||||
"Submit feedback", FieldForm.BUTTON,
|
||||
"Give feedback on the application", -1
|
||||
@@ -63,7 +78,8 @@ class Settings : FragmentedStorageFileJson() {
|
||||
try {
|
||||
val i = Intent(Intent.ACTION_VIEW);
|
||||
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));
|
||||
i.data = data;
|
||||
|
||||
@@ -140,7 +156,11 @@ class Settings : FragmentedStorageFileJson() {
|
||||
return FeedStyle.THUMBNAIL;
|
||||
}
|
||||
|
||||
@FormField("Background Update", FieldForm.DROPDOWN, "Experimental background update for subscriptions cache (requires restart)", 6)
|
||||
@FormField("Fetch on app boot", FieldForm.TOGGLE, "Shortly after opening the app, start fetching subscriptions.", 6)
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var fetchOnAppBoot: Boolean = true;
|
||||
|
||||
@FormField("Background Update", FieldForm.DROPDOWN, "Experimental background update for subscriptions cache (requires restart)", 7)
|
||||
@DropdownFieldOptionsId(R.array.background_interval)
|
||||
var subscriptionsBackgroundUpdateInterval: Int = 0;
|
||||
|
||||
@@ -156,7 +176,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
};
|
||||
|
||||
|
||||
@FormField("Subscription Concurrency", FieldForm.DROPDOWN, "Specify how many threads are used to fetch channels (requires restart)", 7)
|
||||
@FormField("Subscription Concurrency", FieldForm.DROPDOWN, "Specify how many threads are used to fetch channels (requires restart)", 8)
|
||||
@DropdownFieldOptionsId(R.array.thread_count)
|
||||
var subscriptionConcurrency: Int = 3;
|
||||
|
||||
@@ -213,7 +233,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
|
||||
fun isAutoRotate() = autoRotate == 1 || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate());
|
||||
|
||||
@FormField("Auto-Rotate Dead Zone", FieldForm.DROPDOWN, "Auto-rotate deadzone in degrees", 5)
|
||||
@FormField("Auto-Rotate Dead Zone", FieldForm.DROPDOWN, "This prevents the device from rotating within the given amount of degrees.", 5)
|
||||
@DropdownFieldOptionsId(R.array.auto_rotate_dead_zone)
|
||||
var autoRotateDeadZone: Int = 0;
|
||||
|
||||
|
||||
@@ -75,10 +75,10 @@ class AddSourceActivity : AppCompatActivity() {
|
||||
_buttonInstall = findViewById(R.id.button_install);
|
||||
|
||||
_buttonBack.setOnClickListener {
|
||||
onBackPressed();
|
||||
finish();
|
||||
};
|
||||
_buttonCancel.setOnClickListener {
|
||||
onBackPressed();
|
||||
finish();
|
||||
}
|
||||
_buttonInstall.setOnClickListener {
|
||||
_config?.let {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.futo.platformplayer.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.*
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
@@ -14,6 +17,31 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
||||
lateinit var _buttonQR: BigButton;
|
||||
lateinit var _buttonURL: 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, "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, "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?) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_add_source_options);
|
||||
@@ -37,8 +65,9 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
||||
integrator.setBeepEnabled(false)
|
||||
integrator.setBarcodeImageEnabled(true)
|
||||
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
||||
integrator.initiateScan()
|
||||
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||
}
|
||||
|
||||
_buttonURL.onClick.subscribe {
|
||||
UIDialogs.toast(this, "Not implemented yet..");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.futo.platformplayer.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
@@ -40,7 +41,8 @@ class ExceptionActivity : AppCompatActivity() {
|
||||
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: "Unknown Context";
|
||||
val stack = intent.getStringExtra(EXTRA_STACK) ?: "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");
|
||||
try {
|
||||
val file = File(filesDir, "log.txt");
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.futo.platformplayer.activities
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -68,9 +70,15 @@ class LoginActivity : AppCompatActivity() {
|
||||
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.webChromeClient = object: WebChromeClient() {
|
||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||
Logger.w(TAG, "Login Console: " + consoleMessage?.message());
|
||||
return super.onConsoleMessage(consoleMessage);
|
||||
}
|
||||
}*/
|
||||
_webView.webViewClient = webViewClient;
|
||||
_webView.loadUrl(authConfig.loginUrl);
|
||||
}
|
||||
|
||||
+25
-16
@@ -5,6 +5,7 @@ import android.os.Bundle
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
@@ -14,6 +15,7 @@ import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.polycentric.core.*
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import com.journeyapps.barcodescanner.CaptureActivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -27,6 +29,16 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
private lateinit var _buttonImportProfile: LinearLayout;
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_polycentric_import_profile);
|
||||
@@ -45,10 +57,15 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
};
|
||||
|
||||
_buttonScanProfile.setOnClickListener {
|
||||
val integrator = IntentIntegrator(this);
|
||||
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE);
|
||||
integrator.setPrompt("Scan a QR code");
|
||||
integrator.initiateScan();
|
||||
val integrator = IntentIntegrator(this)
|
||||
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||
integrator.setPrompt("Scan a QR code")
|
||||
integrator.setOrientationLocked(true);
|
||||
integrator.setCameraId(0)
|
||||
integrator.setBeepEnabled(false)
|
||||
integrator.setBarcodeImageEnabled(true)
|
||||
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
||||
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||
};
|
||||
|
||||
_buttonImportProfile.setOnClickListener {
|
||||
@@ -66,18 +83,6 @@ 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) {
|
||||
if (!url.startsWith("polycentric://")) {
|
||||
UIDialogs.toast(this, "Not a valid URL");
|
||||
@@ -126,4 +131,8 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private const val TAG = "PolycentricImportProfileActivity";
|
||||
}
|
||||
|
||||
class QRCaptureActivity: CaptureActivity() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -52,17 +52,6 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
||||
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;
|
||||
|
||||
reloadSettings();
|
||||
@@ -72,6 +61,18 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
||||
_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, "You are now in developer mode");
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.futo.platformplayer.logging.Logger
|
||||
import okhttp3.Call
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
@@ -28,7 +29,11 @@ open class ManagedHttpClient {
|
||||
|
||||
constructor(builder: OkHttpClient.Builder = OkHttpClient.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 {
|
||||
@@ -116,7 +121,7 @@ open class ManagedHttpClient {
|
||||
fun execute(request : Request) : Response {
|
||||
ensureNotMainThread();
|
||||
|
||||
beforeRequest(request);
|
||||
//beforeRequest(request);
|
||||
|
||||
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)
|
||||
Logger.v(TAG, "HTTP Response [${request.method}] ${request.url} - [${time}ms]");
|
||||
|
||||
afterRequest(request, resp);
|
||||
//afterRequest(request, resp);
|
||||
return resp;
|
||||
}
|
||||
|
||||
//Set Listeners
|
||||
fun setOnBeforeRequest(listener : (Request)->Unit) {
|
||||
this.onBeforeRequest = listener;
|
||||
open fun beforeRequest(request: okhttp3.Request): okhttp3.Request {
|
||||
return request;
|
||||
}
|
||||
fun setOnAfterRequest(listener : (Request, Response)->Unit) {
|
||||
this.onAfterRequest = listener;
|
||||
}
|
||||
|
||||
open fun beforeRequest(request: Request) {
|
||||
onBeforeRequest?.invoke(request);
|
||||
}
|
||||
open fun afterRequest(request: Request, resp: Response) {
|
||||
onAfterRequest?.invoke(request, resp);
|
||||
open fun afterRequest(resp: okhttp3.Response): okhttp3.Response {
|
||||
return resp;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
|
||||
class PlatformClientPool {
|
||||
private val _parent: JSClient;
|
||||
@@ -51,6 +52,11 @@ class PlatformClientPool {
|
||||
if(reserved == null && _pool.size < capacity) {
|
||||
Logger.i(TAG, "Started additional [${_parent.name}] client in pool [${_poolName}] (${_pool.size + 1}/${capacity})");
|
||||
reserved = _parent.getCopy();
|
||||
|
||||
reserved?.onCaptchaException?.subscribe { client, ex ->
|
||||
StateApp.instance.handleCaptchaException(client, ex);
|
||||
};
|
||||
|
||||
reserved?.initialize();
|
||||
_pool[reserved!!] = _poolCounter;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ class ResultCapabilities(
|
||||
const val TYPE_VIDEOS = "VIDEOS";
|
||||
const val TYPE_STREAMS = "STREAMS";
|
||||
const val TYPE_LIVE = "LIVE";
|
||||
const val TYPE_POSTS = "POSTS";
|
||||
const val TYPE_MIXED = "MIXED";
|
||||
|
||||
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
||||
|
||||
+4
@@ -39,4 +39,8 @@ class PolycentricPlatformComment : IPlatformComment {
|
||||
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
|
||||
return PolycentricPlatformComment(contextUrl, author, message, rating, date, reference, replyCount);
|
||||
}
|
||||
|
||||
companion object {
|
||||
val MAX_COMMENT_SIZE = 2000
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -30,7 +30,7 @@ open class SerializedPlatformNestedContent(
|
||||
override val contentProvider: String?,
|
||||
override val contentThumbnails: Thumbnails
|
||||
) : 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 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.IPlatformContentDetails
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import java.util.*
|
||||
|
||||
class DevJSClient : JSClient {
|
||||
@@ -24,6 +25,10 @@ class DevJSClient : JSClient {
|
||||
_auth = auth;
|
||||
_captcha = captcha;
|
||||
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
||||
|
||||
onCaptchaException.subscribe { client, captcha ->
|
||||
StateApp.instance.handleCaptchaException(client, captcha);
|
||||
}
|
||||
}
|
||||
//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) {
|
||||
@@ -31,6 +36,10 @@ class DevJSClient : JSClient {
|
||||
_auth = auth;
|
||||
_captcha = captcha;
|
||||
this.devID = devID ?: UUID.randomUUID().toString().substring(0, 5);
|
||||
|
||||
onCaptchaException.subscribe { client, captcha ->
|
||||
StateApp.instance.handleCaptchaException(client, captcha);
|
||||
}
|
||||
}
|
||||
|
||||
fun setCaptcha(captcha: SourceCaptchaData? = null) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.primitive.V8ValueBoolean
|
||||
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.reference.V8ValueArray
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
@@ -23,6 +24,7 @@ import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
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.models.*
|
||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
@@ -431,8 +433,11 @@ open class JSClient : IPlatformClient {
|
||||
@JSDocsParameter("url", "A content url (this platform)")
|
||||
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSCommentPager(config, plugin,
|
||||
plugin.executeTyped("source.getComments(${Json.encodeToString(url)})"));
|
||||
val pager = plugin.executeTyped<V8Value>("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")
|
||||
@JSDocsParameter("comment", "Comment object that was returned by getComments")
|
||||
|
||||
@@ -41,6 +41,7 @@ class SourcePluginConfig(
|
||||
val constants: HashMap<String, String> = hashMapOf(),
|
||||
|
||||
//TODO: These should be vals...but prob for serialization reasons cannot be changed.
|
||||
var subscriptionRateLimit: Int? = null,
|
||||
var enableInSearch: Boolean = true,
|
||||
var enableInHome: Boolean = true,
|
||||
var supportedClaimTypes: List<Int> = listOf()
|
||||
|
||||
+43
-27
@@ -31,8 +31,12 @@ class JSHttpClient : ManagedHttpClient {
|
||||
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
|
||||
}
|
||||
if(!captcha?.cookieMap.isNullOrEmpty()) {
|
||||
for(domainCookies in captcha!!.cookieMap!!)
|
||||
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
|
||||
for(domainCookies in captcha!!.cookieMap!!) {
|
||||
if(_currentCookieMap.containsKey(domainCookies.key))
|
||||
_currentCookieMap[domainCookies.key]?.putAll(domainCookies.value);
|
||||
else
|
||||
_currentCookieMap.put(domainCookies.key, HashMap(domainCookies.value));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -46,14 +50,18 @@ class JSHttpClient : ManagedHttpClient {
|
||||
return newClient;
|
||||
}
|
||||
|
||||
override fun beforeRequest(request: Request) {
|
||||
val domain = Uri.parse(request.url).host!!.lowercase();
|
||||
|
||||
override fun beforeRequest(request: okhttp3.Request): okhttp3.Request {
|
||||
val domain = request.url.host.lowercase();
|
||||
val auth = _auth;
|
||||
|
||||
val newBuilder = if(auth != null || doApplyCookies)
|
||||
request.newBuilder();
|
||||
else
|
||||
null;
|
||||
if (auth != null) {
|
||||
//TODO: Possibly add doApplyHeaders
|
||||
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) {
|
||||
@@ -68,34 +76,37 @@ class JSHttpClient : ManagedHttpClient {
|
||||
|
||||
if(cookiesToApply.size > 0) {
|
||||
val cookieString = cookiesToApply.map { it.key + "=" + it.value }.joinToString("; ");
|
||||
request.headers["Cookie"] = cookieString;
|
||||
|
||||
val existingCookies = request.headers["Cookie"];
|
||||
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);
|
||||
super.beforeRequest(request)
|
||||
_jsClient?.validateUrlOrThrow(request.url.toString());
|
||||
return newBuilder?.let { it.build() } ?: request;
|
||||
}
|
||||
|
||||
override fun afterRequest(request: Request, resp: Response) {
|
||||
super.afterRequest(request, resp)
|
||||
|
||||
override fun afterRequest(resp: okhttp3.Response): okhttp3.Response {
|
||||
if(doUpdateCookies) {
|
||||
val domain = Uri.parse(request.url).host!!.lowercase();
|
||||
val domainParts = domain!!.split(".");
|
||||
val domain = resp.request.url.host.lowercase();
|
||||
val domainParts = domain.split(".");
|
||||
val defaultCookieDomain =
|
||||
"." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
||||
for (header in resp.headers) {
|
||||
if ((_auth != null || _currentCookieMap.isNotEmpty()) && header.key.lowercase() == "set-cookie") {
|
||||
val newCookies = cookieStringToMap(header.value);
|
||||
for (cookie in newCookies) {
|
||||
val endIndex = cookie.value.indexOf(";");
|
||||
var cookieValue = cookie.value;
|
||||
if ((_auth != null || _currentCookieMap.isNotEmpty()) && header.first.lowercase() == "set-cookie") {
|
||||
//val newCookies = cookieStringToMap(header.second.split("; "));
|
||||
val cookie = cookieStringToPair(header.second);
|
||||
//for (cookie in newCookies) {
|
||||
var cookieValue = cookie.second;
|
||||
var domainToUse = domain;
|
||||
|
||||
if (endIndex > 0) {
|
||||
val cookieParts = cookie.value.split(";");
|
||||
if (!cookie.first.isNullOrEmpty() && !cookie.second.isNullOrEmpty()) {
|
||||
val cookieParts = cookie.second.split(";");
|
||||
if (cookieParts.size == 0)
|
||||
continue;
|
||||
cookieValue = cookieParts[0].trim();
|
||||
@@ -121,24 +132,29 @@ class JSHttpClient : ManagedHttpClient {
|
||||
_currentCookieMap!!.put(domainToUse, newMap)
|
||||
newMap;
|
||||
}
|
||||
if(cookieMap.containsKey(cookie.key) || doAllowNewCookies)
|
||||
cookieMap.put(cookie.key, cookieValue);
|
||||
}
|
||||
if(cookieMap.containsKey(cookie.first) || doAllowNewCookies)
|
||||
cookieMap.put(cookie.first, cookieValue);
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
|
||||
private fun cookieStringToMap(parts: List<String>): Map<String, String> {
|
||||
val map = hashMapOf<String, String>();
|
||||
for(cookie in parts) {
|
||||
val cookieKey = cookie.substring(0, cookie.indexOf("="));
|
||||
val cookieVal = cookie.substring(cookie.indexOf("=") + 1);
|
||||
map.put(cookieKey.trim(), cookieVal.trim());
|
||||
val pair = cookieStringToPair(cookie)
|
||||
map.put(pair.first, pair.second);
|
||||
}
|
||||
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..
|
||||
fun printTestCode(url: String, body: ByteArray?, headers: Map<String, String>, cookieString: String, allHeaders: Map<String, String>? = null) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.futo.platformplayer.BuildConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.warnIfMainThread
|
||||
|
||||
@@ -27,7 +28,7 @@ abstract class JSPager<T> : IPager<T> {
|
||||
this.pager = pager;
|
||||
this.config = config;
|
||||
|
||||
_hasMorePages = pager.getOrThrow(config, "hasMore", "Pager");
|
||||
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
||||
getResults();
|
||||
}
|
||||
|
||||
@@ -45,7 +46,7 @@ abstract class JSPager<T> : IPager<T> {
|
||||
pager = plugin.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage()") {
|
||||
pager.invoke("nextPage", arrayOf<Any>());
|
||||
};
|
||||
_hasMorePages = pager.getOrThrow(config, "hasMore", "Pager");
|
||||
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
||||
_resultChanged = true;
|
||||
/*
|
||||
try {
|
||||
|
||||
+6
-2
@@ -1,6 +1,7 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js.models
|
||||
|
||||
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.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
@@ -99,8 +100,11 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
return getCommentsJS(client);
|
||||
}
|
||||
|
||||
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
||||
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
||||
private fun getCommentsJS(client: JSClient): IPager<IPlatformComment>? {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,8 @@ class DedupContentPager : IPager<IPlatformContent>, IAsyncPager<IPlatformContent
|
||||
_currentResults = dedupResults(_basePager.getResults());
|
||||
}
|
||||
|
||||
override fun hasMorePages(): Boolean = _basePager.hasMorePages();
|
||||
override fun hasMorePages(): Boolean =
|
||||
_basePager.hasMorePages();
|
||||
override fun nextPage() {
|
||||
_basePager.nextPage()
|
||||
_currentResults = dedupResults(_basePager.getResults());
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import java.util.stream.IntStream
|
||||
* A PlatformContent MultiPager that orders the results of a page based on the datetime of a content item
|
||||
*/
|
||||
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) {}
|
||||
|
||||
@Synchronized
|
||||
override fun selectItemIndex(options: Array<SelectionOption<IPlatformContent>>): Int {
|
||||
|
||||
@@ -16,7 +16,7 @@ abstract class MultiPager<T> : IPager<T> {
|
||||
protected val _subSinglePagers : MutableList<SingleItemPager<T>>;
|
||||
protected val _failedPagers: ArrayList<IPager<T>> = arrayListOf();
|
||||
|
||||
private val _pageSize : Int = 9;
|
||||
private var _pageSize : Int = 9;
|
||||
|
||||
private var _didInitialize = false;
|
||||
|
||||
@@ -27,7 +27,8 @@ abstract class MultiPager<T> : IPager<T> {
|
||||
|
||||
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;
|
||||
_pagers = pagers.toMutableList();
|
||||
_subSinglePagers = _pagers.map { SingleItemPager(it) }.toMutableList();
|
||||
|
||||
+10
-2
@@ -69,9 +69,17 @@ abstract class MultiRefreshPager<T>: IRefreshPager<T>, IPager<T> {
|
||||
if(pagerToAdd == null) {
|
||||
if(toReplacePager != null && toReplacePager is PlaceholderPager && error != null) {
|
||||
val pluginId = toReplacePager.placeholderFactory.invoke().id?.pluginId ?: "";
|
||||
_currentPager = PlaceholderPager(5) {
|
||||
|
||||
_pagersReusable.add((PlaceholderPager(5) {
|
||||
return@PlaceholderPager PlatformContentPlaceholder(pluginId, error)
|
||||
} as IPager<T>;
|
||||
} 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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.futo.platformplayer.developer
|
||||
|
||||
import android.content.Context
|
||||
import com.futo.platformplayer.activities.CaptchaActivity
|
||||
import com.futo.platformplayer.activities.LoginActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
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")
|
||||
}
|
||||
}
|
||||
@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")
|
||||
fun pluginLoginTestPlugin(context: HttpContext) {
|
||||
val config = _testPlugin?.config as SourcePluginConfig;
|
||||
|
||||
@@ -2,12 +2,16 @@ package com.futo.platformplayer.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
@@ -32,6 +36,8 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
||||
private lateinit var _buttonCancel: MaterialButton;
|
||||
private lateinit var _editComment: EditText;
|
||||
private lateinit var _inputMethodManager: InputMethodManager;
|
||||
private lateinit var _textCharacterCount: TextView;
|
||||
private lateinit var _textCharacterCountMax: TextView;
|
||||
|
||||
val onCommentAdded = Event1<IPlatformComment>();
|
||||
|
||||
@@ -42,6 +48,26 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
||||
_buttonCancel = findViewById(R.id.button_cancel);
|
||||
_buttonCreate = findViewById(R.id.button_create);
|
||||
_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;
|
||||
|
||||
@@ -53,6 +79,11 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
||||
_buttonCreate.setOnClickListener {
|
||||
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 processHandle = StatePolycentric.instance.processHandle!!
|
||||
val eventPointer = processHandle.post(comment, null, ref)
|
||||
|
||||
@@ -298,6 +298,7 @@ class V8Plugin {
|
||||
private fun throwExceptionFromV8(config: IV8PluginConfig, pluginType: String, msg: String, innerEx: Exception? = null, stack: String? = null, code: String? = null) {
|
||||
when(pluginType) {
|
||||
"ScriptException" -> throw ScriptException(config, msg, innerEx, stack, code);
|
||||
"CriticalException" -> throw ScriptCriticalException(config, msg, innerEx, stack, code);
|
||||
"AgeException" -> throw ScriptAgeException(config, msg, innerEx, stack, code);
|
||||
"UnavailableException" -> throw ScriptUnavailableException(config, msg, innerEx, stack, code);
|
||||
"ScriptExecutionException" -> throw ScriptExecutionException(config, msg, innerEx, stack, code);
|
||||
|
||||
+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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -77,7 +77,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
|
||||
};
|
||||
_textName?.text = channel.name;
|
||||
|
||||
val metadata = "${channel.subscribers.toHumanNumber()} subscribers";
|
||||
val metadata = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} subscribers" else "";
|
||||
_textMetadata?.text = metadata;
|
||||
_lastChannel = channel;
|
||||
setLinks(channel.links, channel.name);
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.widget.TextView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.futopay.PaymentConfigurations
|
||||
import com.futo.futopay.PaymentManager
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -68,9 +69,12 @@ class BuyFragment : MainFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
_buttonBuy.setOnClickListener {
|
||||
buy();
|
||||
}
|
||||
if(!BuildConfig.IS_PLAYSTORE_BUILD)
|
||||
_buttonBuy.setOnClickListener {
|
||||
buy();
|
||||
}
|
||||
else
|
||||
_buttonBuy.visibility = View.GONE;
|
||||
_buttonPaid.setOnClickListener {
|
||||
paid();
|
||||
}
|
||||
|
||||
+1
-1
@@ -361,7 +361,7 @@ class ChannelFragment : MainFragment() {
|
||||
|
||||
_buttonSubscribe.setSubscribeChannel(channel);
|
||||
_textChannel.text = channel.name;
|
||||
_textChannelSub.text = "${channel.subscribers.toHumanNumber()} subscribers";
|
||||
_textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} subscribers" else "";
|
||||
|
||||
_creatorThumbnail.setThumbnail(channel.thumbnail, true);
|
||||
Glide.with(_imageBanner)
|
||||
|
||||
@@ -39,6 +39,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
private val _spinnerSortBy: Spinner;
|
||||
private val _containerSortBy: LinearLayout;
|
||||
private val _tagsView: TagsView;
|
||||
private val _textCentered: TextView;
|
||||
|
||||
protected val _toolbarContentView: LinearLayout;
|
||||
|
||||
@@ -68,6 +69,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
this.fragment = fragment;
|
||||
inflater.inflate(R.layout.fragment_feed, this);
|
||||
|
||||
_textCentered = findViewById(R.id.text_centered);
|
||||
_progress_bar = findViewById(R.id.progress_bar);
|
||||
_progress_bar.inactiveColor = Color.TRANSPARENT;
|
||||
|
||||
@@ -169,6 +171,10 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
_recyclerResults.addOnScrollListener(_scrollListener);
|
||||
}
|
||||
|
||||
protected fun setTextCentered(text: String?) {
|
||||
_textCentered.text = text;
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
//Reload the pager if the plugin was killed
|
||||
val pager = recyclerData.pager;
|
||||
|
||||
+15
@@ -69,6 +69,8 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
private var _currentLoadIndex = 0;
|
||||
|
||||
private var _taskLoadChannel: TaskHandler<String, IPlatformChannel>;
|
||||
private var _counter: Int = 0;
|
||||
private var _limitToastShown = false;
|
||||
|
||||
constructor(fragment: ImportSubscriptionsFragment, inflater: LayoutInflater) : super(inflater.context) {
|
||||
_fragment = fragment;
|
||||
@@ -104,6 +106,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
setLoading(false);
|
||||
|
||||
_taskLoadChannel = TaskHandler<String, IPlatformChannel>({_fragment.lifecycleScope}, { link ->
|
||||
_counter++;
|
||||
val channel: IPlatformChannel = StatePlatform.instance.getChannelLive(link, false);
|
||||
return@TaskHandler channel;
|
||||
}).success {
|
||||
@@ -124,6 +127,8 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
}
|
||||
|
||||
fun onShown(parameter: Any ?, isBack: Boolean) {
|
||||
_counter = 0;
|
||||
_limitToastShown = false;
|
||||
updateSelected();
|
||||
|
||||
val itemsRemoved = _items.size;
|
||||
@@ -157,6 +162,15 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
|
||||
private fun load() {
|
||||
setLoading(true);
|
||||
if (_counter >= MAXIMUM_BATCH_SIZE) {
|
||||
if (!_limitToastShown) {
|
||||
_limitToastShown = true;
|
||||
UIDialogs.toast(context, "Stopped after $MAXIMUM_BATCH_SIZE to avoid rate limit, re-enter to import rest");
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
_taskLoadChannel.run(_links[_currentLoadIndex]);
|
||||
}
|
||||
|
||||
@@ -196,6 +210,7 @@ class ImportSubscriptionsFragment : MainFragment() {
|
||||
|
||||
companion object {
|
||||
val TAG = "ImportSubscriptionsFragment";
|
||||
private const val MAXIMUM_BATCH_SIZE = 75;
|
||||
fun newInstance() = ImportSubscriptionsFragment().apply {}
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -346,24 +346,24 @@ class PostDetailFragment : MainFragment {
|
||||
|
||||
_rating.visibility = VISIBLE;
|
||||
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
|
||||
_rating.onLikeDislikeUpdated.subscribe(this) { processHandle, newHasLiked, newHasDisliked ->
|
||||
if (newHasLiked) {
|
||||
processHandle.opinion(ref, Opinion.like);
|
||||
} else if (newHasDisliked) {
|
||||
processHandle.opinion(ref, Opinion.dislike);
|
||||
_rating.onLikeDislikeUpdated.subscribe(this) { args ->
|
||||
if (args.hasLiked) {
|
||||
args.processHandle.opinion(ref, Opinion.like);
|
||||
} else if (args.hasDisliked) {
|
||||
args.processHandle.opinion(ref, Opinion.dislike);
|
||||
} else {
|
||||
processHandle.opinion(ref, Opinion.neutral);
|
||||
args.processHandle.opinion(ref, Opinion.neutral);
|
||||
}
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
processHandle.fullyBackfillServers();
|
||||
args.processHandle.fullyBackfillServers();
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to backfill servers", e)
|
||||
}
|
||||
}
|
||||
|
||||
StatePolycentric.instance.updateLikeMap(ref, newHasLiked, newHasDisliked)
|
||||
StatePolycentric.instance.updateLikeMap(ref, args.hasLiked, args.hasDisliked)
|
||||
};
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
@@ -601,7 +601,7 @@ class PostDetailFragment : MainFragment {
|
||||
val subscribers = value?.author?.subscribers;
|
||||
if(subscribers != null && subscribers > 0) {
|
||||
_channelMeta.visibility = View.VISIBLE;
|
||||
_channelMeta.text = value.author.subscribers!!.toHumanNumber() + " subscribers";
|
||||
_channelMeta.text = if((value.author?.subscribers ?: 0) > 0) value.author.subscribers!!.toHumanNumber() + " subscribers" else "";
|
||||
} else {
|
||||
_channelMeta.visibility = View.GONE;
|
||||
_channelMeta.text = "";
|
||||
|
||||
+15
-1
@@ -258,11 +258,25 @@ class SourceDetailFragment : MainFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
val clientIfExists = StatePlugins.instance.getPlugin(config.id);
|
||||
groups.add(
|
||||
BigButtonGroup(c, "Management",
|
||||
BigButton(c, "Uninstall", "Removes the plugin from the app", R.drawable.ic_block) {
|
||||
uninstallSource();
|
||||
}.withBackground(R.drawable.background_big_button_red)
|
||||
}.withBackground(R.drawable.background_big_button_red).apply {
|
||||
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||
};
|
||||
},
|
||||
if(clientIfExists?.captchaEncrypted != null)
|
||||
BigButton(c, "Delete Captcha", "Deletes stored captcha answer for this plugin", R.drawable.ic_block) {
|
||||
clientIfExists?.updateCaptcha(null);
|
||||
}.apply {
|
||||
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||
};
|
||||
}.withBackground(R.drawable.background_big_button_red)
|
||||
else null
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+43
-1
@@ -12,13 +12,17 @@ import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.cache.ChannelContentCache
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.exceptions.ChannelException
|
||||
import com.futo.platformplayer.exceptions.RateLimitException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||
@@ -31,6 +35,7 @@ import com.futo.platformplayer.views.subscriptions.SubscriptionBar
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -160,8 +165,17 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
private val _filterLock = Object();
|
||||
private val _filterSettings = FragmentedStorage.get<FeedFilterSettings>("subFeedFilter");
|
||||
|
||||
private var _bypassRateLimit = false;
|
||||
private val _lastExceptions: List<Throwable>? = null;
|
||||
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh ->
|
||||
if(!_bypassRateLimit) {
|
||||
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
|
||||
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.config.subscriptionRateLimit}" }.joinToString("\n");
|
||||
val rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true }
|
||||
Logger.w(TAG, "Refreshing subscriptions with requests:\n" + reqCountStr);
|
||||
if(rateLimitPlugins.any())
|
||||
throw RateLimitException(rateLimitPlugins.map { it.key.id });
|
||||
}
|
||||
val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh);
|
||||
|
||||
val currentExs = StateSubscriptions.instance.globalSubscriptionExceptions;
|
||||
@@ -171,6 +185,29 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
return@TaskHandler resp;
|
||||
})
|
||||
.success { loadedResult(it); }
|
||||
.exception<RateLimitException> {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val subs = StateSubscriptions.instance.getSubscriptions();
|
||||
val subsByLimited = it.pluginIds.map{ StatePlatform.instance.getClientOrNull(it) }
|
||||
.filterIsInstance<JSClient>()
|
||||
.associateWith { client -> subs.filter { it.getClient() == client } }
|
||||
.map { Pair(it.key, it.value) }
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.showDialog(context, R.drawable.ic_security_pred,
|
||||
"Rate Limit Warning", "This is a temporary measure to prevent people from hitting rate limit until we have better support for lots of subscriptions." +
|
||||
"\n\nYou have too many subscriptions for the following plugins:\n",
|
||||
subsByLimited.map { "${it.first.config.name}: ${it.second.size} Subscriptions" } .joinToString("\n"), 0, UIDialogs.Action("Refresh Anyway", {
|
||||
_bypassRateLimit = true;
|
||||
loadResults();
|
||||
}, UIDialogs.ActionStyle.DANGEROUS_TEXT),
|
||||
UIDialogs.Action("OK", {
|
||||
finishRefreshLayoutLoader();
|
||||
setLoading(false);
|
||||
}, UIDialogs.ActionStyle.PRIMARY));
|
||||
}
|
||||
}
|
||||
}
|
||||
.exception<Throwable> {
|
||||
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
||||
if(it !is CancellationException)
|
||||
@@ -244,8 +281,12 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
Logger.i(TAG, "Subscriptions load");
|
||||
if(recyclerData.results.size == 0) {
|
||||
val cachePager = ChannelContentCache.instance.getSubscriptionCachePager();
|
||||
Logger.i(TAG, "Subscription show cache (${cachePager.getResults().size})");
|
||||
val results = cachePager.getResults();
|
||||
Logger.i(TAG, "Subscription show cache (${results.size})");
|
||||
setTextCentered(if (results.isEmpty()) "No results found\nSwipe down to refresh" else null);
|
||||
setPager(cachePager);
|
||||
} else {
|
||||
setTextCentered(null);
|
||||
}
|
||||
_taskGetPager.run(withRefetch);
|
||||
}
|
||||
@@ -258,6 +299,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
finishRefreshLayoutLoader();
|
||||
setLoading(false);
|
||||
setPager(pager);
|
||||
setTextCentered(if (pager.getResults().isEmpty()) "No results found\nSwipe down to refresh" else null);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to finish loading", e)
|
||||
}
|
||||
|
||||
+13
-13
@@ -873,7 +873,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_channelName.text = video.author.name;
|
||||
_playWhenReady = true;
|
||||
if(video.author.subscribers != null) {
|
||||
_channelMeta.text = video.author.subscribers!!.toHumanNumber() + " subscribers";
|
||||
_channelMeta.text = if((video.author.subscribers ?: 0) > 0) video.author.subscribers!!.toHumanNumber() + " subscribers" else "";
|
||||
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0);
|
||||
} else {
|
||||
_channelMeta.text = "";
|
||||
@@ -982,7 +982,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_title.text = video.name;
|
||||
_channelName.text = video.author.name;
|
||||
if(video.author.subscribers != null) {
|
||||
_channelMeta.text = video.author.subscribers!!.toHumanNumber() + " subscribers";
|
||||
_channelMeta.text = if((video.author.subscribers ?: 0) > 0) video.author.subscribers!!.toHumanNumber() + " subscribers" else "";
|
||||
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0);
|
||||
} else {
|
||||
_channelMeta.text = "";
|
||||
@@ -1042,24 +1042,24 @@ class VideoDetailView : ConstraintLayout {
|
||||
withContext(Dispatchers.Main) {
|
||||
_rating.visibility = View.VISIBLE;
|
||||
_rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked);
|
||||
_rating.onLikeDislikeUpdated.subscribe(this) { processHandle, newHasLiked, newHasDisliked ->
|
||||
if (newHasLiked) {
|
||||
processHandle.opinion(ref, Opinion.like);
|
||||
} else if (newHasDisliked) {
|
||||
processHandle.opinion(ref, Opinion.dislike);
|
||||
_rating.onLikeDislikeUpdated.subscribe(this) { args ->
|
||||
if (args.hasLiked) {
|
||||
args.processHandle.opinion(ref, Opinion.like);
|
||||
} else if (args.hasDisliked) {
|
||||
args.processHandle.opinion(ref, Opinion.dislike);
|
||||
} else {
|
||||
processHandle.opinion(ref, Opinion.neutral);
|
||||
args.processHandle.opinion(ref, Opinion.neutral);
|
||||
}
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
processHandle.fullyBackfillServers();
|
||||
args.processHandle.fullyBackfillServers();
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to backfill servers", e)
|
||||
}
|
||||
}
|
||||
|
||||
StatePolycentric.instance.updateLikeMap(ref, newHasLiked, newHasDisliked)
|
||||
StatePolycentric.instance.updateLikeMap(ref, args.hasLiked, args.hasDisliked)
|
||||
};
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
@@ -1610,10 +1610,10 @@ class VideoDetailView : ConstraintLayout {
|
||||
_lastSubtitleSource = toSet;
|
||||
}
|
||||
|
||||
private fun handleUnavailableVideo() {
|
||||
private fun handleUnavailableVideo(msg: String? = null) {
|
||||
if (!nextVideo()) {
|
||||
if(video?.datetime == null || video?.datetime!! < OffsetDateTime.now().minusHours(1))
|
||||
UIDialogs.showDialog(context, R.drawable.ic_lock, "Unavailable video", "This video is unavailable.", null, 0,
|
||||
UIDialogs.showDialog(context, R.drawable.ic_lock, "Unavailable video", msg ?: "This video is unavailable.", null, 0,
|
||||
UIDialogs.Action("Back", {
|
||||
this@VideoDetailView.onClose.emit();
|
||||
}, UIDialogs.ActionStyle.PRIMARY));
|
||||
@@ -2092,7 +2092,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
.exception<ScriptUnavailableException> {
|
||||
Logger.w(TAG, "exception<ScriptUnavailableException>", it);
|
||||
handleUnavailableVideo();
|
||||
handleUnavailableVideo(it.message);
|
||||
}
|
||||
.exception<ScriptException> {
|
||||
Logger.w(TAG, "exception<ScriptException>", it)
|
||||
|
||||
@@ -61,9 +61,21 @@ public class PolycentricModelLoader implements ModelLoader<String, ByteBuffer> {
|
||||
_deferred.invokeOnCompletion(throwable -> {
|
||||
if (throwable != null) {
|
||||
callback.onLoadFailed(new Exception(throwable));
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
|
||||
Deferred<ByteBuffer> deferred = _deferred;
|
||||
if (deferred == null) {
|
||||
callback.onLoadFailed(new Exception("Deferred is null"));
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
|
||||
ByteBuffer completed = deferred.getCompleted();
|
||||
if (completed != null) {
|
||||
callback.onDataReady(completed);
|
||||
} else {
|
||||
callback.onLoadFailed(new Exception("Completed is null"));
|
||||
}
|
||||
final ByteBuffer completed = _deferred.getCompleted();
|
||||
callback.onDataReady(completed);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,24 +3,48 @@ package com.futo.platformplayer.models
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.getNowDiffDays
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class Subscription {
|
||||
var channel: SerializedChannel;
|
||||
|
||||
//Last found content
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||
var lastVideo : OffsetDateTime = OffsetDateTime.MAX;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||
var lastLiveStream : OffsetDateTime = OffsetDateTime.MAX;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||
var lastPost : OffsetDateTime = OffsetDateTime.MAX;
|
||||
|
||||
//Last update time
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||
var lastVideoUpdate : OffsetDateTime = OffsetDateTime.MIN;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||
var lastStreamUpdate : OffsetDateTime = OffsetDateTime.MIN;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||
var lastLiveStreamUpdate : OffsetDateTime = OffsetDateTime.MIN;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||
var lastPostUpdate : OffsetDateTime = OffsetDateTime.MIN;
|
||||
|
||||
//Last video interval
|
||||
var uploadInterval : Int = 0;
|
||||
var uploadPostInterval : Int = 0;
|
||||
|
||||
|
||||
constructor(channel : SerializedChannel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
fun shouldFetchStreams() = lastLiveStream.getNowDiffDays() < 7;
|
||||
fun shouldFetchLiveStreams() = lastLiveStream.getNowDiffDays() < 14;
|
||||
fun shouldFetchPosts() = lastPost.getNowDiffDays() < 2;
|
||||
|
||||
fun getClient() = StatePlatform.instance.getChannelClientOrNull(channel.url);
|
||||
|
||||
fun updateChannel(channel: IPlatformChannel) {
|
||||
this.channel = SerializedChannel.fromChannel(channel);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ data class Telemetry(
|
||||
val buildType: String,
|
||||
val debug: Boolean,
|
||||
val isUnstableBuild: Boolean,
|
||||
val time: Long,
|
||||
val brand: String,
|
||||
val manufacturer: String,
|
||||
val model: String
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.others
|
||||
|
||||
import android.net.Uri
|
||||
import android.webkit.*
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.Serializer
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
@@ -12,6 +13,7 @@ import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.matchesDomain
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class LoginWebViewClient : WebViewClient {
|
||||
private val LOG_VERBOSE = false;
|
||||
@@ -42,6 +44,8 @@ class LoginWebViewClient : WebViewClient {
|
||||
private var urlFound = false;
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
if(BuildConfig.DEBUG)
|
||||
Logger.i(TAG, "Login Url Page: " + url);
|
||||
super.onPageFinished(view, url);
|
||||
onPageLoaded.emit(view, url);
|
||||
}
|
||||
@@ -55,11 +59,29 @@ class LoginWebViewClient : WebViewClient {
|
||||
return null;
|
||||
}
|
||||
|
||||
var completionUrlExcludeQuery = false
|
||||
var completionUrlToCheck = if(urlFound) null else _authConfig.completionUrl;
|
||||
if(completionUrlToCheck != null) {
|
||||
if(completionUrlToCheck.endsWith("?*")) {
|
||||
completionUrlToCheck = completionUrlToCheck.substring(0, completionUrlToCheck.length - 2);
|
||||
completionUrlExcludeQuery = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val domain = request.url.host;
|
||||
val domainLower = request.url.host?.lowercase();
|
||||
val urlString = request.url.toString();
|
||||
if(_authConfig.completionUrl == null)
|
||||
urlFound = true;
|
||||
else urlFound = urlFound || request.url == Uri.parse(_authConfig.completionUrl);
|
||||
else urlFound = urlFound || (
|
||||
if(completionUrlExcludeQuery)
|
||||
(if(urlString.contains("?"))
|
||||
urlString.substring(0, urlString.indexOf("?")) == completionUrlToCheck
|
||||
else urlString == completionUrlToCheck)
|
||||
else
|
||||
request.url == Uri.parse(_authConfig.completionUrl)
|
||||
);
|
||||
|
||||
//HEADERS
|
||||
if(domainLower != null) {
|
||||
|
||||
@@ -20,10 +20,10 @@ class PlatformContentSerializer() : JsonContentPolymorphicSerializer<SerializedP
|
||||
if(obj?.jsonPrimitive?.isString ?: true)
|
||||
return when(obj?.jsonPrimitive?.contentOrNull) {
|
||||
"MEDIA" -> SerializedPlatformVideo.serializer();
|
||||
"NESTED" -> SerializedPlatformNestedContent.serializer();
|
||||
"NESTED_VIDEO" -> SerializedPlatformNestedContent.serializer();
|
||||
"ARTICLE" -> throw NotImplementedError("Articles not yet implemented");
|
||||
"POST" -> throw NotImplementedError("Post not yet implemented");
|
||||
else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.int}")
|
||||
else -> throw NotImplementedError("Unknown Content Type Value: ${obj?.jsonPrimitive?.contentOrNull}")
|
||||
};
|
||||
else
|
||||
return when(obj?.jsonPrimitive?.int) {
|
||||
|
||||
@@ -28,6 +28,8 @@ import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.activities.CaptchaActivity
|
||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.Serializer
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.background.BackgroundWorker
|
||||
@@ -44,7 +46,10 @@ import com.futo.platformplayer.receivers.AudioNoisyReceiver
|
||||
import com.futo.platformplayer.services.DownloadService
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import com.stripe.android.core.utils.encodeToJson
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.*
|
||||
@@ -91,7 +96,7 @@ class StateApp {
|
||||
onChanged?.invoke(getExternalGeneralDirectory(context));
|
||||
}
|
||||
else
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
scopeOrNull?.launch(Dispatchers.Main) {
|
||||
UIDialogs.toast("Failed to gain access to\n [${it?.lastPathSegment}]");
|
||||
};
|
||||
};
|
||||
@@ -103,10 +108,14 @@ class StateApp {
|
||||
return null;
|
||||
}
|
||||
fun changeExternalDownloadDirectory(context: IWithResultLauncher, onChanged: ((DocumentFile?)->Unit)? = null) {
|
||||
|
||||
scopeOrNull?.launch(Dispatchers.Main) {
|
||||
UIDialogs.toast("External download directory not yet used by export (WIP)");
|
||||
};
|
||||
if(context is Context)
|
||||
requestDirectoryAccess(context, "Download Exports", "This directory is used to export downloads to for external usage.", null) {
|
||||
if(it != null)
|
||||
context.contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION.or(Intent.FLAG_GRANT_WRITE_URI_PERMISSION.or(Intent.FLAG_GRANT_READ_URI_PERMISSION)));
|
||||
context.contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_WRITE_URI_PERMISSION.or(Intent.FLAG_GRANT_READ_URI_PERMISSION));
|
||||
if(it != null && isValidStorageUri(context, it)) {
|
||||
Logger.i(TAG, "Changed external download directory: ${it}");
|
||||
Settings.instance.storage.storage_general = it.toString();
|
||||
@@ -425,9 +434,19 @@ class StateApp {
|
||||
StatePlaylists.instance.toMigrateCheck()
|
||||
).flatten(), 0);
|
||||
|
||||
scope.launch {
|
||||
delay(5000);
|
||||
StateSubscriptions.instance.updateSubscriptionFeed(scope, false);
|
||||
if(Settings.instance.subscriptions.fetchOnAppBoot) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount();
|
||||
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.config.subscriptionRateLimit}" }.joinToString("\n");
|
||||
val isRateLimitReached = !subRequestCounts.any { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true };
|
||||
if (isRateLimitReached) {
|
||||
Logger.w(TAG, "Subscriptions request on boot, request counts:\n${reqCountStr}");
|
||||
delay(5000);
|
||||
StateSubscriptions.instance.updateSubscriptionFeed(scope, false);
|
||||
}
|
||||
else
|
||||
Logger.w(TAG, "Too many subscription requests required:\n${reqCountStr}");
|
||||
}
|
||||
}
|
||||
|
||||
val interval = Settings.instance.subscriptions.getSubscriptionsBackgroundIntervalMinutes();
|
||||
@@ -654,13 +673,20 @@ class StateApp {
|
||||
UIDialogs.showConfirmationDialog(context, "Captcha required\nPlugin [${client.config.name}]", {
|
||||
CaptchaActivity.showCaptcha(context, client.config, exception.url, exception.body) {
|
||||
hasCaptchaDialog = false;
|
||||
StatePlugins.instance.setPluginCaptcha(client.config.id, it);
|
||||
scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
StatePlatform.instance.reloadClient(context, client.config.id);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(SourceDetailFragment.TAG, "Failed to reload client.", e)
|
||||
return@launch;
|
||||
|
||||
if(client is DevJSClient) {
|
||||
client.setCaptcha(it);
|
||||
client.recreate(context);
|
||||
}
|
||||
else {
|
||||
StatePlugins.instance.setPluginCaptcha(client.config.id, it);
|
||||
scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
StatePlatform.instance.reloadClient(context, client.config.id);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(SourceDetailFragment.TAG, "Failed to reload client.", e)
|
||||
return@launch;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,19 +664,24 @@ class StatePlatform {
|
||||
toQuery.add(ResultCapabilities.TYPE_STREAMS);
|
||||
if(clientCapabilities.hasType(ResultCapabilities.TYPE_LIVE))
|
||||
toQuery.add(ResultCapabilities.TYPE_LIVE);
|
||||
if(clientCapabilities.hasType(ResultCapabilities.TYPE_POSTS))
|
||||
toQuery.add(ResultCapabilities.TYPE_POSTS);
|
||||
|
||||
if(isSubscriptionOptimized) {
|
||||
val sub = StateSubscriptions.instance.getSubscription(channelUrl);
|
||||
if(sub != null) {
|
||||
val daysSinceLiveStream = sub.lastLiveStream.getNowDiffDays()
|
||||
if(daysSinceLiveStream > 7) {
|
||||
Logger.i(TAG, "Subscription [${sub.channel.name}:${channelUrl}] Last livestream > 7 days, skipping live streams [${daysSinceLiveStream} days ago]");
|
||||
if(!sub.shouldFetchStreams()) {
|
||||
Logger.i(TAG, "Subscription [${sub.channel.name}:${channelUrl}] Last livestream > 7 days, skipping live streams [${sub.lastLiveStream.getNowDiffDays()} days ago]");
|
||||
toQuery.remove(ResultCapabilities.TYPE_LIVE);
|
||||
}
|
||||
if(daysSinceLiveStream > 14) {
|
||||
Logger.i(TAG, "Subscription [${sub.channel.name}:${channelUrl}] Last livestream > 15 days, skipping streams [${daysSinceLiveStream} days ago]");
|
||||
if(!sub.shouldFetchLiveStreams()) {
|
||||
Logger.i(TAG, "Subscription [${sub.channel.name}:${channelUrl}] Last livestream > 15 days, skipping streams [${sub.lastLiveStream.getNowDiffDays()} days ago]");
|
||||
toQuery.remove(ResultCapabilities.TYPE_STREAMS);
|
||||
}
|
||||
if(!sub.shouldFetchPosts()) {
|
||||
Logger.i(TAG, "Subscription [${sub.channel.name}:${channelUrl}] Last livestream > 5 days, skipping posts [${sub.lastPost.getNowDiffDays()} days ago]");
|
||||
toQuery.remove(ResultCapabilities.TYPE_POSTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -267,9 +267,10 @@ class StatePlaylists {
|
||||
return@map null;
|
||||
}
|
||||
catch(ex: NoPlatformClientException) {
|
||||
throw ReconstructionException(name, "No source enabled for [${it}]", ex);
|
||||
//TODO: Propagate this to dialog, and then back, allowing users to enable plugins...
|
||||
builder.messages.add("No source enabled for [${it}]");
|
||||
return@map null;
|
||||
//builder.messages.add("No source enabled for [${it}]");
|
||||
//return@map null;
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
throw ReconstructionException(name, "${name}:[${it}] ${ex.message}", ex);
|
||||
|
||||
@@ -302,7 +302,7 @@ class StatePolycentric {
|
||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
||||
subscribers = null
|
||||
),
|
||||
msg = post.content,
|
||||
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||
rating = RatingLikeDislikes(likes, dislikes),
|
||||
date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
||||
replyCount = replies.toInt(),
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.states
|
||||
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
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.SerializedChannel
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
@@ -15,9 +16,11 @@ import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
||||
import com.futo.platformplayer.exceptions.ChannelException
|
||||
import com.futo.platformplayer.findNonRuntimeException
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.getNowDiffDays
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
@@ -219,13 +222,38 @@ class StateSubscriptions {
|
||||
}
|
||||
}
|
||||
|
||||
fun getSubscriptionsFeed(allowFailure: Boolean = false): MultiChronoContentPager {
|
||||
fun getSubscriptionRequestCount(): Map<JSClient, Int> {
|
||||
val subs = getSubscriptions();
|
||||
val pluginReqCounts = mutableMapOf<JSClient, Int>();
|
||||
|
||||
for(sub in subs) {
|
||||
val client = StatePlatform.instance.getChannelClientOrNull(sub.channel.url);
|
||||
if(client !is JSClient)
|
||||
continue;
|
||||
|
||||
val channelCaps = client.getChannelCapabilities();
|
||||
if(!pluginReqCounts.containsKey(client))
|
||||
pluginReqCounts[client] = 1;
|
||||
else
|
||||
pluginReqCounts[client] = pluginReqCounts[client]!! + 1;
|
||||
|
||||
if(channelCaps.hasType(ResultCapabilities.TYPE_STREAMS) && sub.shouldFetchStreams())
|
||||
pluginReqCounts[client] = pluginReqCounts[client]!! + 1;
|
||||
if(channelCaps.hasType(ResultCapabilities.TYPE_LIVE) && sub.shouldFetchLiveStreams())
|
||||
pluginReqCounts[client] = pluginReqCounts[client]!! + 1;
|
||||
if(channelCaps.hasType(ResultCapabilities.TYPE_POSTS) && sub.shouldFetchPosts())
|
||||
pluginReqCounts[client] = pluginReqCounts[client]!! + 1;
|
||||
}
|
||||
return pluginReqCounts;
|
||||
}
|
||||
|
||||
fun getSubscriptionsFeed(allowFailure: Boolean = false): IPager<IPlatformContent> {
|
||||
val result = getSubscriptionsFeedWithExceptions(allowFailure, true);
|
||||
if(result.second.any())
|
||||
throw result.second.first();
|
||||
return result.first;
|
||||
}
|
||||
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope? = null, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null): Pair<MultiChronoContentPager, List<Throwable>> {
|
||||
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope? = null, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
||||
val subsPager: Array<IPager<IPlatformContent>>;
|
||||
val exs: ArrayList<Throwable> = arrayListOf();
|
||||
|
||||
@@ -293,7 +321,16 @@ class StateSubscriptions {
|
||||
synchronized(failedPlugins) {
|
||||
//Fail all subscription calls to plugin if it has a captcha issue
|
||||
if(ex.config is SourcePluginConfig && !failedPlugins.contains(ex.config.id)) {
|
||||
Logger.w(TAG, "Subscriptions fetch ignoring plugin [${ex.config.name}] due to Captcha");
|
||||
Logger.w(TAG, "Subscriptionsgnoring plugin [${ex.config.name}] due to Captcha");
|
||||
failedPlugins.add(ex.config.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(ex is ScriptCriticalException) {
|
||||
synchronized(failedPlugins) {
|
||||
//Fail all subscription calls to plugin if it has a critical issue
|
||||
if(ex.config is SourcePluginConfig && !failedPlugins.contains(ex.config.id)) {
|
||||
Logger.w(TAG, "Subscriptions ignoring plugin [${ex.config.name}] due to critical exception:\n" + ex.message);
|
||||
failedPlugins.add(ex.config.id);
|
||||
}
|
||||
}
|
||||
@@ -343,9 +380,10 @@ class StateSubscriptions {
|
||||
throw exs.first();
|
||||
|
||||
Logger.i(TAG, "Subscription pager with ${subsPager.size} channels");
|
||||
val pager = MultiChronoContentPager(subsPager, allowFailure);
|
||||
val pager = MultiChronoContentPager(subsPager, allowFailure, 15);
|
||||
pager.initialize();
|
||||
return Pair(pager, exs);
|
||||
//return Pair(pager, exs);
|
||||
return Pair(DedupContentPager(pager), exs);
|
||||
}
|
||||
|
||||
//New Migration
|
||||
|
||||
@@ -37,14 +37,13 @@ class StateTelemetry {
|
||||
BuildConfig.BUILD_TYPE,
|
||||
BuildConfig.DEBUG,
|
||||
BuildConfig.IS_UNSTABLE_BUILD,
|
||||
Instant.now().epochSecond,
|
||||
Build.BRAND,
|
||||
Build.MANUFACTURER,
|
||||
Build.MODEL
|
||||
);
|
||||
|
||||
val headers = hashMapOf(
|
||||
"Content-Type" to "text/plain"
|
||||
"Content-Type" to "application/json"
|
||||
);
|
||||
|
||||
val json = Json.encodeToString(telemetry);
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
@@ -35,12 +36,14 @@ class CommentViewHolder : ViewHolder {
|
||||
private val _buttonReplies: PillButton;
|
||||
private val _layoutRating: LinearLayout;
|
||||
private val _pillRatingLikesDislikes: PillRatingLikesDislikes;
|
||||
private val _layoutComment: ConstraintLayout;
|
||||
|
||||
var onClick = Event1<IPlatformComment>();
|
||||
var comment: IPlatformComment? = null
|
||||
private set;
|
||||
|
||||
constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_comment, viewGroup, false)) {
|
||||
_layoutComment = itemView.findViewById(R.id.layout_comment);
|
||||
_creatorThumbnail = itemView.findViewById(R.id.image_thumbnail);
|
||||
_textAuthor = itemView.findViewById(R.id.text_author);
|
||||
_textMetadata = itemView.findViewById(R.id.text_metadata);
|
||||
@@ -53,29 +56,31 @@ class CommentViewHolder : ViewHolder {
|
||||
_layoutRating = itemView.findViewById(R.id.layout_rating);
|
||||
_pillRatingLikesDislikes = itemView.findViewById(R.id.rating);
|
||||
|
||||
_pillRatingLikesDislikes.onLikeDislikeUpdated.subscribe { processHandle, hasLiked, hasDisliked ->
|
||||
_pillRatingLikesDislikes.onLikeDislikeUpdated.subscribe { args ->
|
||||
val c = comment
|
||||
if (c !is PolycentricPlatformComment) {
|
||||
throw Exception("Not implemented for non polycentric comments")
|
||||
}
|
||||
|
||||
if (hasLiked) {
|
||||
processHandle.opinion(c.reference, Opinion.like);
|
||||
} else if (hasDisliked) {
|
||||
processHandle.opinion(c.reference, Opinion.dislike);
|
||||
if (args.hasLiked) {
|
||||
args.processHandle.opinion(c.reference, Opinion.like);
|
||||
} else if (args.hasDisliked) {
|
||||
args.processHandle.opinion(c.reference, Opinion.dislike);
|
||||
} else {
|
||||
processHandle.opinion(c.reference, Opinion.neutral);
|
||||
args.processHandle.opinion(c.reference, Opinion.neutral);
|
||||
}
|
||||
|
||||
_layoutComment.alpha = if (args.dislikes > 2 && args.dislikes / (args.likes + args.dislikes) >= 0.7) 0.5f else 1.0f;
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
processHandle.fullyBackfillServers();
|
||||
args.processHandle.fullyBackfillServers();
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to backfill servers.", e)
|
||||
}
|
||||
}
|
||||
|
||||
StatePolycentric.instance.updateLikeMap(c.reference, hasLiked, hasDisliked)
|
||||
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
|
||||
};
|
||||
|
||||
_buttonReplies.onClick.subscribe {
|
||||
@@ -98,6 +103,13 @@ class CommentViewHolder : ViewHolder {
|
||||
_textMetadata.visibility = View.GONE;
|
||||
}
|
||||
|
||||
val rating = comment.rating;
|
||||
if (rating is RatingLikeDislikes) {
|
||||
_layoutComment.alpha = if (rating.dislikes > 0 && rating.dislikes / (rating.likes + rating.dislikes) >= 0.7) 0.5f else 1.0f;
|
||||
} else {
|
||||
_layoutComment.alpha = 1.0f;
|
||||
}
|
||||
|
||||
_textBody.text = comment.message.fixHtmlLinks();
|
||||
|
||||
if (readonly) {
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
|
||||
if(authorLink.subscribers == null || (authorLink.subscribers ?: 0) <= 0L)
|
||||
_textMetadata.visibility = View.GONE;
|
||||
else {
|
||||
_textMetadata.text = authorLink.subscribers!!.toHumanNumber() + " subscribers";
|
||||
_textMetadata.text = if(authorLink?.subscribers ?: 0 > 0) authorLink.subscribers!!.toHumanNumber() + " subscribers" else "";
|
||||
_textMetadata.visibility = View.VISIBLE;
|
||||
}
|
||||
_buttonSubscribe.setSubscribeChannel(authorLink.url);
|
||||
|
||||
@@ -14,13 +14,13 @@ class BigButtonGroup : LinearLayout {
|
||||
_header = findViewById(R.id.header_title);
|
||||
_buttons = findViewById(R.id.buttons);
|
||||
}
|
||||
constructor(context: Context, header: String, vararg buttons: BigButton) : super(context) {
|
||||
constructor(context: Context, header: String, vararg buttons: BigButton?) : super(context) {
|
||||
inflate(context, R.layout.big_button_group, this);
|
||||
_header = findViewById(R.id.header_title);
|
||||
_buttons = findViewById(R.id.buttons);
|
||||
|
||||
_header.text = header;
|
||||
for(button in buttons)
|
||||
for(button in buttons.filterNotNull())
|
||||
_buttons.addView(button);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,20 @@ import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event3
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.toHumanNumber
|
||||
import com.futo.polycentric.core.ProcessHandle
|
||||
|
||||
data class OnLikeDislikeUpdatedArgs(
|
||||
val processHandle: ProcessHandle,
|
||||
val likes: Long,
|
||||
val hasLiked: Boolean,
|
||||
val dislikes: Long,
|
||||
val hasDisliked: Boolean,
|
||||
);
|
||||
|
||||
class PillRatingLikesDislikes : LinearLayout {
|
||||
private val _textLikes: TextView;
|
||||
private val _textDislikes: TextView;
|
||||
@@ -29,7 +38,7 @@ class PillRatingLikesDislikes : LinearLayout {
|
||||
private var _dislikes = 0L;
|
||||
private var _hasDisliked = false;
|
||||
|
||||
val onLikeDislikeUpdated = Event3<ProcessHandle, Boolean, Boolean>();
|
||||
val onLikeDislikeUpdated = Event1<OnLikeDislikeUpdatedArgs>();
|
||||
|
||||
constructor(context : Context, attrs : AttributeSet?) : super(context, attrs) {
|
||||
LayoutInflater.from(context).inflate(R.layout.rating_likesdislikes, this, true);
|
||||
@@ -76,7 +85,7 @@ class PillRatingLikesDislikes : LinearLayout {
|
||||
|
||||
_textLikes.text = _likes.toHumanNumber();
|
||||
updateColors();
|
||||
onLikeDislikeUpdated.emit(processHandle, _hasLiked, _hasDisliked);
|
||||
onLikeDislikeUpdated.emit(OnLikeDislikeUpdatedArgs(processHandle, _likes, _hasLiked, _dislikes, _hasDisliked));
|
||||
}
|
||||
|
||||
fun dislike(processHandle: ProcessHandle) {
|
||||
@@ -96,7 +105,7 @@ class PillRatingLikesDislikes : LinearLayout {
|
||||
|
||||
_textDislikes.text = _dislikes.toHumanNumber();
|
||||
updateColors();
|
||||
onLikeDislikeUpdated.emit(processHandle, _hasLiked, _hasDisliked);
|
||||
onLikeDislikeUpdated.emit(OnLikeDislikeUpdatedArgs(processHandle, _likes, _hasLiked, _dislikes, _hasDisliked));
|
||||
}
|
||||
|
||||
private fun updateColors() {
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
android:layout_marginRight="30dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:inputType="textPassword"
|
||||
android:hint="Backup Password" />
|
||||
|
||||
<LinearLayout
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginRight="30dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:singleLine="true"
|
||||
android:hint="Backup Password" />
|
||||
|
||||
|
||||
@@ -35,6 +35,25 @@
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/character_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="0"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginStart="24dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/character_count_max"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="/2000"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular" />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -76,22 +76,37 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<com.futo.platformplayer.views.others.ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp" />
|
||||
android:layout_height="wrap_content">
|
||||
<com.futo.platformplayer.views.others.ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list_results"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical" />
|
||||
</LinearLayout>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list_results"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_centered"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/gray_ac"
|
||||
android:textSize="12dp" />
|
||||
</FrameLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
android:isScrollContainer="false"
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="13sp"
|
||||
android:maxLines="100"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_metadata"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
|
||||
Submodule app/src/stable/assets/sources/youtube updated: 1c34bb0163...eff873edf3
Submodule app/src/unstable/assets/sources/nebula updated: aa2a4f2970...8ea9393634
Submodule app/src/unstable/assets/sources/twitch updated: 7645b88a76...eb198a3d20
Submodule app/src/unstable/assets/sources/youtube updated: 1c34bb0163...239960b932
+1
-1
Submodule dep/futopay updated: 9e589c2311...2c608e7edd
+1
-1
Submodule dep/polycentricandroid updated: 636d17f0ad...1079dd394f
Reference in New Issue
Block a user