mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 13:02:39 +02:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cec1a8c49 | |||
| d4afba929b | |||
| 70939cbac6 | |||
| a3aa61df6d | |||
| e13ab5cb40 | |||
| d059947925 | |||
| d6c4b730de | |||
| 8241863170 | |||
| 31a758e4f3 | |||
| ca971a0e77 | |||
| a45a0f9a8a | |||
| c2dce52a5b | |||
| a2c63c59c5 | |||
| 7e54a2ce3d | |||
| 5b7fb2c818 | |||
| da0ac281e2 | |||
| 576b37f64c | |||
| 26c2db5023 | |||
| f344dbf35c | |||
| a04acbd4a5 | |||
| bd48aba8d3 | |||
| 12b73bb248 | |||
| c3ff897ef4 | |||
| 242728fbe7 |
@@ -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);
|
||||
|
||||
@@ -35,4 +35,8 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
|
||||
|
||||
fun Protocol.Claim.resolveChannelUrl(): String? {
|
||||
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) })
|
||||
}
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
@@ -21,6 +22,7 @@ import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
|
||||
import com.futo.platformplayer.views.fields.FormField
|
||||
import com.futo.platformplayer.views.fields.FieldForm
|
||||
import com.futo.platformplayer.views.fields.FormFieldButton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -44,8 +46,9 @@ class Settings : FragmentedStorageFileJson() {
|
||||
|
||||
@FormField(
|
||||
"Manage Polycentric identity", FieldForm.BUTTON,
|
||||
"Manage your Polycentric identity", -2
|
||||
"Manage your Polycentric identity", -4
|
||||
)
|
||||
@FormFieldButton(R.drawable.ic_person)
|
||||
fun managePolycentricIdentity() {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
if (StatePolycentric.instance.processHandle != null) {
|
||||
@@ -56,10 +59,38 @@ class Settings : FragmentedStorageFileJson() {
|
||||
}
|
||||
}
|
||||
|
||||
@FormField(
|
||||
"Show FAQ", FieldForm.BUTTON,
|
||||
"Get answers to common questions", -3
|
||||
)
|
||||
@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(
|
||||
"Show Issues", FieldForm.BUTTON,
|
||||
"A list of user-reported and self-reported issues", -2
|
||||
)
|
||||
@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(
|
||||
"Submit feedback", FieldForm.BUTTON,
|
||||
"Give feedback on the application", -1
|
||||
)
|
||||
@FormFieldButton(R.drawable.ic_bug)
|
||||
fun submitFeedback() {
|
||||
try {
|
||||
val i = Intent(Intent.ACTION_VIEW);
|
||||
@@ -79,6 +110,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
"Manage Tabs", FieldForm.BUTTON,
|
||||
"Change tabs visible on the home screen", -1
|
||||
)
|
||||
@FormFieldButton(R.drawable.ic_tabs)
|
||||
fun manageTabs() {
|
||||
try {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
@@ -601,6 +633,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "Settings";
|
||||
const val URL_FAQ = "https://grayjay.app/faq.html";
|
||||
|
||||
private var _isFirst = true;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -563,6 +568,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 {
|
||||
try {
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
+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;
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -220,6 +220,13 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
buttons.removeAt(buyIndex)
|
||||
buttons.add(0, button)
|
||||
}
|
||||
//Force faq to be second
|
||||
val faqIndex = buttons.indexOfFirst { b -> b.id == 97 };
|
||||
if (faqIndex != -1) {
|
||||
val button = buttons[faqIndex]
|
||||
buttons.removeAt(faqIndex)
|
||||
buttons.add(1, button)
|
||||
}
|
||||
|
||||
for (data in buttons) {
|
||||
val button = MenuButton(context, data, _fragment, true);
|
||||
@@ -289,6 +296,9 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
if (!StatePayment.instance.hasPaid) {
|
||||
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
|
||||
}
|
||||
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz, R.string.faq, canToggle = false, { false }, {
|
||||
it.navigate<BrowserFragment>(Settings.URL_FAQ);
|
||||
}))
|
||||
|
||||
//Add conditional buttons here, when you add a conditional button, be sure to add the register and unregister events for when the button needs to be updated
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
+71
-44
@@ -246,28 +246,45 @@ class ChannelFragment : MainFragment() {
|
||||
|
||||
if (parameter is String) {
|
||||
_buttonSubscribe.setSubscribeChannel(parameter);
|
||||
_textChannel.text = "";
|
||||
_textChannelSub.text = "";
|
||||
setPolycentricProfileOr(parameter) {
|
||||
_textChannel.text = "";
|
||||
_textChannelSub.text = "";
|
||||
_creatorThumbnail.setThumbnail(null, true);
|
||||
Glide.with(_imageBanner)
|
||||
.clear(_imageBanner);
|
||||
};
|
||||
|
||||
_url = parameter;
|
||||
loadChannel();
|
||||
} else if (parameter is SerializedChannel) {
|
||||
showChannel(parameter);
|
||||
_url = parameter.url;
|
||||
_creatorThumbnail.setThumbnail(parameter.url, false);
|
||||
loadChannel();
|
||||
} else if (parameter is IPlatformChannel)
|
||||
showChannel(parameter);
|
||||
else if (parameter is PlatformAuthorLink) {
|
||||
_textChannel.text = parameter.name;
|
||||
_textChannelSub.text = "";
|
||||
_creatorThumbnail.setThumbnail(parameter.url, false);
|
||||
setPolycentricProfileOr(parameter.url) {
|
||||
_textChannel.text = parameter.name;
|
||||
_textChannelSub.text = "";
|
||||
_creatorThumbnail.setThumbnail(parameter.thumbnail, true);
|
||||
Glide.with(_imageBanner)
|
||||
.clear(_imageBanner);
|
||||
|
||||
_taskLoadPolycentricProfile.run(parameter.id);
|
||||
};
|
||||
|
||||
_url = parameter.url;
|
||||
loadChannel();
|
||||
} else if (parameter is Subscription) {
|
||||
_textChannel.text = parameter.channel.name;
|
||||
_textChannelSub.text = "";
|
||||
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, false);
|
||||
setPolycentricProfileOr(parameter.channel.url) {
|
||||
_textChannel.text = parameter.channel.name;
|
||||
_textChannelSub.text = "";
|
||||
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true);
|
||||
Glide.with(_imageBanner)
|
||||
.clear(_imageBanner);
|
||||
|
||||
_taskLoadPolycentricProfile.run(parameter.channel.id);
|
||||
};
|
||||
|
||||
_url = parameter.channel.url;
|
||||
loadChannel();
|
||||
@@ -360,15 +377,8 @@ class ChannelFragment : MainFragment() {
|
||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons);
|
||||
|
||||
_buttonSubscribe.setSubscribeChannel(channel);
|
||||
_textChannel.text = channel.name;
|
||||
_textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} subscribers" else "";
|
||||
|
||||
_creatorThumbnail.setThumbnail(channel.thumbnail, true);
|
||||
Glide.with(_imageBanner)
|
||||
.load(channel.banner)
|
||||
.crossfade()
|
||||
.into(_imageBanner)
|
||||
|
||||
//TODO: Find a better way to access the adapter fragments..
|
||||
|
||||
(_viewPager.adapter as ChannelViewPagerAdapter?)?.let {
|
||||
@@ -381,51 +391,68 @@ class ChannelFragment : MainFragment() {
|
||||
|
||||
this.channel = channel;
|
||||
|
||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(channel.url);
|
||||
setPolycentricProfileOr(channel.url) {
|
||||
_textChannel.text = channel.name;
|
||||
_creatorThumbnail.setThumbnail(channel.thumbnail, true);
|
||||
Glide.with(_imageBanner)
|
||||
.load(channel.banner)
|
||||
.crossfade()
|
||||
.into(_imageBanner);
|
||||
|
||||
_taskLoadPolycentricProfile.run(channel.id);
|
||||
};
|
||||
}
|
||||
|
||||
private fun setPolycentricProfileOr(url: String, or: () -> Unit) {
|
||||
val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(it.url) };
|
||||
if (cachedProfile != null) {
|
||||
setPolycentricProfile(cachedProfile, animate = false);
|
||||
} else {
|
||||
setPolycentricProfile(null, animate = false);
|
||||
_taskLoadPolycentricProfile.run(channel.id);
|
||||
or();
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
Log.i(TAG, "setPolycentricProfile(cachedPolycentricProfile = $cachedPolycentricProfile, animate = $animate)")
|
||||
|
||||
val polycentricProfile = cachedPolycentricProfile?.profile;
|
||||
if (polycentricProfile != null) {
|
||||
_fragment.topBar?.onShown(polycentricProfile);
|
||||
val dp_35 = 35.dp(resources)
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (polycentricProfile.systemState.username.isNotBlank())
|
||||
_textChannel.text = polycentricProfile.systemState.username;
|
||||
if (avatar != null) {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
}
|
||||
|
||||
val dp_35 = 35.dp(resources)
|
||||
val avatar = polycentricProfile.systemState.avatar?.selectBestImage(dp_35 * dp_35)
|
||||
?.let { it.toURLInfoSystemLinkUrl(polycentricProfile.system.toProto(), it.process, polycentricProfile.systemState.servers.toList()) };
|
||||
val banner = profile?.systemState?.banner?.selectHighestResolutionImage()
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_creatorThumbnail.setThumbnail(avatar, true);
|
||||
} else {
|
||||
_creatorThumbnail.setHarborAvailable(true, true);
|
||||
}
|
||||
if (banner != null) {
|
||||
Glide.with(_imageBanner)
|
||||
.load(banner)
|
||||
.crossfade()
|
||||
.into(_imageBanner);
|
||||
} else {
|
||||
Glide.with(_imageBanner)
|
||||
.load(channel?.banner)
|
||||
.crossfade()
|
||||
.into(_imageBanner);
|
||||
}
|
||||
|
||||
val banner = polycentricProfile.systemState.banner?.selectHighestResolutionImage()
|
||||
?.let { it.toURLInfoSystemLinkUrl(polycentricProfile.system.toProto(), it.process, polycentricProfile.systemState.servers.toList()) };
|
||||
|
||||
if (banner != null) {
|
||||
Glide.with(_imageBanner)
|
||||
.load(banner)
|
||||
.crossfade()
|
||||
.into(_imageBanner);
|
||||
}
|
||||
if (profile != null) {
|
||||
_fragment.topBar?.onShown(profile);
|
||||
_textChannel.text = profile.systemState.username;
|
||||
}
|
||||
|
||||
(_viewPager.adapter as ChannelViewPagerAdapter?)?.let {
|
||||
it.getFragment<ChannelAboutFragment>().setPolycentricProfile(polycentricProfile, animate);
|
||||
it.getFragment<ChannelMonetizationFragment>().setPolycentricProfile(polycentricProfile, animate);
|
||||
it.getFragment<ChannelListFragment>().setPolycentricProfile(polycentricProfile, animate);
|
||||
it.getFragment<ChannelContentsFragment>().setPolycentricProfile(polycentricProfile, animate);
|
||||
it.getFragment<ChannelAboutFragment>().setPolycentricProfile(profile, animate);
|
||||
it.getFragment<ChannelMonetizationFragment>().setPolycentricProfile(profile, animate);
|
||||
it.getFragment<ChannelListFragment>().setPolycentricProfile(profile, animate);
|
||||
it.getFragment<ChannelContentsFragment>().setPolycentricProfile(profile, animate);
|
||||
//TODO: Call on other tabs as needed
|
||||
}
|
||||
}
|
||||
|
||||
+5
-7
@@ -101,14 +101,12 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||
|
||||
fun onShown(parameter: Any?, isBack: Boolean) {
|
||||
if(parameter is SuggestionsFragmentData) {
|
||||
if(!isBack) {
|
||||
setQuery(parameter.query, false);
|
||||
setChannelUrl(parameter.channelUrl, false);
|
||||
setQuery(parameter.query, false);
|
||||
setChannelUrl(parameter.channelUrl, false);
|
||||
|
||||
fragment.topBar?.apply {
|
||||
if (this is SearchTopBarFragment) {
|
||||
this.setText(parameter.query);
|
||||
}
|
||||
fragment.topBar?.apply {
|
||||
if (this is SearchTopBarFragment) {
|
||||
this.setText(parameter.query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-9
@@ -71,16 +71,14 @@ class CreatorSearchResultsFragment : MainFragment() {
|
||||
|
||||
fun onShown(parameter: Any?, isBack: Boolean) {
|
||||
if(parameter is String) {
|
||||
if(!isBack) {
|
||||
setQuery(parameter);
|
||||
setQuery(parameter);
|
||||
|
||||
fragment.topBar?.apply {
|
||||
if (this is SearchTopBarFragment) {
|
||||
setText(parameter);
|
||||
onSearch.subscribe(this) {
|
||||
setQuery(it);
|
||||
};
|
||||
}
|
||||
fragment.topBar?.apply {
|
||||
if (this is SearchTopBarFragment) {
|
||||
setText(parameter);
|
||||
onSearch.subscribe(this) {
|
||||
setQuery(it);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
+7
-9
@@ -73,16 +73,14 @@ class PlaylistSearchResultsFragment : MainFragment() {
|
||||
|
||||
fun onShown(parameter: Any?, isBack: Boolean) {
|
||||
if(parameter is String) {
|
||||
if(!isBack) {
|
||||
setQuery(parameter);
|
||||
setQuery(parameter);
|
||||
|
||||
fragment.topBar?.apply {
|
||||
if (this is SearchTopBarFragment) {
|
||||
setText(parameter);
|
||||
onSearch.subscribe(this) {
|
||||
setQuery(it);
|
||||
};
|
||||
}
|
||||
fragment.topBar?.apply {
|
||||
if (this is SearchTopBarFragment) {
|
||||
setText(parameter);
|
||||
onSearch.subscribe(this) {
|
||||
setQuery(it);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+7
-1
@@ -13,6 +13,7 @@ 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
|
||||
@@ -280,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);
|
||||
}
|
||||
@@ -294,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)
|
||||
}
|
||||
|
||||
+17
-7
@@ -870,7 +870,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
_commentsList.clear();
|
||||
_platform.setPlatformFromClientID(video.id.pluginId);
|
||||
_subTitle.text = subTitleSegments.joinToString(" • ");
|
||||
_channelName.text = video.author.name;
|
||||
_playWhenReady = true;
|
||||
if(video.author.subscribers != null) {
|
||||
_channelMeta.text = if((video.author.subscribers ?: 0) > 0) video.author.subscribers!!.toHumanNumber() + " subscribers" else "";
|
||||
@@ -897,6 +896,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
} else {
|
||||
setPolycentricProfile(null, animate = false);
|
||||
_taskLoadPolycentricProfile.run(video.author.id);
|
||||
_channelName.text = video.author.name;
|
||||
}
|
||||
|
||||
_player.clear();
|
||||
@@ -1971,14 +1971,24 @@ class VideoDetailView : ConstraintLayout {
|
||||
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
_polycentricProfile = cachedPolycentricProfile;
|
||||
|
||||
if (cachedPolycentricProfile?.profile == null) {
|
||||
_layoutMonetization.visibility = View.GONE;
|
||||
_creatorThumbnail.setHarborAvailable(false, animate);
|
||||
return;
|
||||
val dp_35 = 35.dp(context.resources)
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(video?.author?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
}
|
||||
|
||||
_layoutMonetization.visibility = View.VISIBLE;
|
||||
_creatorThumbnail.setHarborAvailable(true, animate);
|
||||
if (profile != null) {
|
||||
_channelName.text = cachedPolycentricProfile.profile.systemState.username;
|
||||
_layoutMonetization.visibility = View.VISIBLE;
|
||||
} else {
|
||||
_layoutMonetization.visibility = View.GONE;
|
||||
}
|
||||
}
|
||||
|
||||
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
|
||||
|
||||
+2
-4
@@ -57,10 +57,8 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
buttonPlayAll.setOnClickListener { onPlayAllClick(); };
|
||||
buttonShuffle.setOnClickListener { onShuffleClick(); };
|
||||
|
||||
if (canEdit())
|
||||
_buttonEdit.setOnClickListener { onEditClick(); };
|
||||
else
|
||||
_buttonEdit.visibility = View.GONE;
|
||||
_buttonEdit.setOnClickListener { onEditClick(); };
|
||||
setButtonDownloadVisible(canEdit());
|
||||
|
||||
videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged);
|
||||
videoListEditorView.onVideoRemoved.subscribe(::onVideoRemoved);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,12 +64,11 @@ class Logging {
|
||||
|
||||
val client = OkHttpClient()
|
||||
val response: Response = client.newCall(request).execute()
|
||||
if (response.isSuccessful) {
|
||||
val body = response.body?.string();
|
||||
return if (body != null) Json.decodeFromString<String>(body) else null;
|
||||
return if (response.isSuccessful) {
|
||||
response.body?.string();
|
||||
} else {
|
||||
Logger.e("Failed to submit log.") { "Failed to submit logs (${response.code}): ${response.body?.string()}" };
|
||||
return null;
|
||||
null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,6 @@ data class Telemetry(
|
||||
val isUnstableBuild: Boolean,
|
||||
val brand: String,
|
||||
val manufacturer: String,
|
||||
val model: String
|
||||
val model: String,
|
||||
val sdkVersion: Int
|
||||
) { }
|
||||
@@ -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
|
||||
@@ -43,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);
|
||||
}
|
||||
@@ -56,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) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.getNowDiffSeconds
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.resolveChannelUrl
|
||||
import com.futo.platformplayer.resolveChannelUrls
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||
import com.futo.platformplayer.stores.CachedPolycentricProfileStorage
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
@@ -37,7 +38,8 @@ class PolycentricCache {
|
||||
ContentType.AVATAR.value,
|
||||
ContentType.USERNAME.value,
|
||||
ContentType.DESCRIPTION.value,
|
||||
ContentType.STORE.value
|
||||
ContentType.STORE.value,
|
||||
ContentType.SERVER.value
|
||||
)
|
||||
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
||||
|
||||
@@ -88,8 +90,9 @@ class PolycentricCache {
|
||||
|
||||
if (result.profile != null) {
|
||||
for (claim in result.profile.ownedClaims) {
|
||||
val url = claim.claim.resolveChannelUrl() ?: continue;
|
||||
_profileUrlCache.map[url] = result;
|
||||
val urls = claim.claim.resolveChannelUrls();
|
||||
for (url in urls)
|
||||
_profileUrlCache.map[url] = result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ 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
|
||||
@@ -672,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -784,6 +784,15 @@ class StatePlatform {
|
||||
return null;
|
||||
}
|
||||
|
||||
fun resolveChannelUrlsByClaimTemplates(claimType: Int, claimValues: Map<Int, String>): List<String> {
|
||||
val urls = arrayListOf<String>();
|
||||
for(client in getClientsByClaimType(claimType).filter { it is JSClient }) {
|
||||
val res = (client as JSClient).resolveChannelUrlsByClaimTemplates(claimType, claimValues);
|
||||
urls.addAll(res);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
fun getPlaylistClientOrNull(url: String): IPlatformClient? = getEnabledClients().find { it.isPlaylistUrl(url) }
|
||||
fun getPlaylistClient(url: String): IPlatformClient = getEnabledClients().find { it.isPlaylistUrl(url) }
|
||||
?: throw NoPlatformClientException("No client enabled that supports this playlist url (${url})");
|
||||
|
||||
@@ -130,10 +130,7 @@ class StatePolycentric {
|
||||
//TODO: Currently abusing subscription concurrency for parallelism
|
||||
val concurrency = if (channelConcurrency == -1) Settings.instance.subscriptions.getSubscriptionsConcurrency() else channelConcurrency;
|
||||
val pagers = profile.ownedClaims.groupBy { it.claim.claimType }.mapNotNull {
|
||||
//TODO: Deduplicate once multiple urls in single claim is supported
|
||||
return@mapNotNull it.value.firstOrNull();
|
||||
}.mapNotNull {
|
||||
val url = it.claim.resolveChannelUrl() ?: return@mapNotNull null;
|
||||
val url = it.value.firstOrNull()?.claim?.resolveChannelUrl() ?: return@mapNotNull null;
|
||||
if (!StatePlatform.instance.hasEnabledChannelClient(url)) {
|
||||
return@mapNotNull null;
|
||||
}
|
||||
@@ -151,10 +148,7 @@ class StatePolycentric {
|
||||
val concurrency = if (channelConcurrency == -1) Settings.instance.subscriptions.getSubscriptionsConcurrency() else channelConcurrency;
|
||||
val deferred = profile.ownedClaims.groupBy { it.claim.claimType }
|
||||
.mapNotNull {
|
||||
//TODO: Deduplicate once multiple urls in single claim is supported
|
||||
return@mapNotNull it.value.firstOrNull();
|
||||
}.mapNotNull {
|
||||
val url = it.claim.resolveChannelUrl() ?: return@mapNotNull null;
|
||||
val url = it.value.firstOrNull()?.claim?.resolveChannelUrl() ?: return@mapNotNull null;
|
||||
val client = StatePlatform.instance.getChannelClientOrNull(url) ?: return@mapNotNull null;
|
||||
|
||||
return@mapNotNull Pair(client, scope.async(Dispatchers.IO) {
|
||||
|
||||
@@ -16,6 +16,7 @@ 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
|
||||
@@ -320,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ class StateTelemetry {
|
||||
BuildConfig.IS_UNSTABLE_BUILD,
|
||||
Build.BRAND,
|
||||
Build.MANUFACTURER,
|
||||
Build.MODEL
|
||||
Build.MODEL,
|
||||
Build.VERSION.SDK_INT
|
||||
);
|
||||
|
||||
val headers = hashMapOf(
|
||||
|
||||
@@ -70,7 +70,7 @@ class CommentViewHolder : ViewHolder {
|
||||
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;
|
||||
_layoutComment.alpha = if (args.dislikes > 2 && args.dislikes.toFloat() / (args.likes + args.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f;
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
@@ -93,6 +93,7 @@ class CommentViewHolder : ViewHolder {
|
||||
|
||||
fun bind(comment: IPlatformComment, readonly: Boolean) {
|
||||
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
|
||||
_creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false);
|
||||
_textAuthor.text = comment.author.name;
|
||||
|
||||
val date = comment.date;
|
||||
@@ -105,7 +106,7 @@ class CommentViewHolder : ViewHolder {
|
||||
|
||||
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;
|
||||
_layoutComment.alpha = if (rating.dislikes > 2 && rating.dislikes.toFloat() / (rating.likes + rating.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f;
|
||||
} else {
|
||||
_layoutComment.alpha = 1.0f;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
import com.futo.platformplayer.views.video.FutoThumbnailPlayer
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
|
||||
|
||||
open class PreviewVideoView : LinearLayout {
|
||||
@@ -58,11 +59,12 @@ open class PreviewVideoView : LinearLayout {
|
||||
|
||||
protected val _exoPlayer: PlayerManager?;
|
||||
|
||||
private val _taskLoadValidClaims = TaskHandler<PlatformID, PolycentricCache.CachedOwnedClaims>(StateApp.instance.scopeGetter,
|
||||
{ PolycentricCache.instance.getValidClaimsAsync(it).await() })
|
||||
.success { it -> updateClaimsLayout(it, animate = true) }
|
||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
||||
StateApp.instance.scopeGetter,
|
||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
||||
.success { it -> onProfileLoaded(it, true) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load claims.", it);
|
||||
Logger.w(TAG, "Failed to load profile.", it);
|
||||
};
|
||||
|
||||
val onVideoClicked = Event2<IPlatformVideo, Long>();
|
||||
@@ -145,15 +147,7 @@ open class PreviewVideoView : LinearLayout {
|
||||
|
||||
|
||||
open fun bind(content: IPlatformContent) {
|
||||
_taskLoadValidClaims.cancel();
|
||||
|
||||
val cachedClaims = PolycentricCache.instance.getCachedValidClaims(content.author.id);
|
||||
if (cachedClaims != null) {
|
||||
updateClaimsLayout(cachedClaims, animate = false);
|
||||
} else {
|
||||
updateClaimsLayout(null, animate = false);
|
||||
_taskLoadValidClaims.run(content.author.id);
|
||||
}
|
||||
_taskLoadProfile.cancel();
|
||||
|
||||
isClickable = true;
|
||||
|
||||
@@ -161,16 +155,25 @@ open class PreviewVideoView : LinearLayout {
|
||||
|
||||
stopPreview();
|
||||
|
||||
if(_imageChannel != null)
|
||||
Glide.with(_imageChannel)
|
||||
.load(content.author.thumbnail)
|
||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||
.into(_imageChannel);
|
||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(content.author.url, true);
|
||||
if (cachedProfile != null) {
|
||||
onProfileLoaded(cachedProfile, false);
|
||||
} else {
|
||||
_imageNeopassChannel?.visibility = View.GONE;
|
||||
_creatorThumbnail?.setThumbnail(content.author.thumbnail, false);
|
||||
_imageChannel?.let {
|
||||
Glide.with(_imageChannel)
|
||||
.load(content.author.thumbnail)
|
||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||
.into(_imageChannel);
|
||||
}
|
||||
_taskLoadProfile.run(content.author.id);
|
||||
_textChannelName.text = content.author.name
|
||||
}
|
||||
|
||||
_imageChannel?.clipToOutline = true;
|
||||
|
||||
_textVideoName.text = content.name;
|
||||
_textChannelName.text = content.author.name
|
||||
_layoutDownloaded.visibility = if (StateDownloads.instance.isDownloaded(content.id)) VISIBLE else GONE;
|
||||
|
||||
_platformIndicator.setPlatformFromClientID(content.id.pluginId);
|
||||
@@ -296,22 +299,50 @@ open class PreviewVideoView : LinearLayout {
|
||||
_playerVideoThumbnail?.setMuteChangedListener(callback);
|
||||
}
|
||||
|
||||
private fun updateClaimsLayout(claims: PolycentricCache.CachedOwnedClaims?, animate: Boolean) {
|
||||
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
_neopassAnimator?.cancel();
|
||||
_neopassAnimator = null;
|
||||
|
||||
val harborAvailable = claims != null && !claims.ownedClaims.isNullOrEmpty();
|
||||
if (harborAvailable) {
|
||||
_imageNeopassChannel?.visibility = View.VISIBLE
|
||||
if (animate) {
|
||||
_neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500)
|
||||
_neopassAnimator?.start()
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
if (_creatorThumbnail != null) {
|
||||
val dp_32 = 32.dp(context.resources);
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_32 * dp_32)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(content?.author?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
}
|
||||
} else if (_imageChannel != null) {
|
||||
val dp_28 = 28.dp(context.resources);
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_28 * dp_28)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_imageChannel.let {
|
||||
Glide.with(_imageChannel)
|
||||
.load(avatar)
|
||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||
.into(_imageChannel);
|
||||
}
|
||||
|
||||
_imageNeopassChannel?.visibility = View.VISIBLE
|
||||
if (animate) {
|
||||
_neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500)
|
||||
_neopassAnimator?.start()
|
||||
} else {
|
||||
_imageNeopassChannel?.alpha = 1.0f;
|
||||
}
|
||||
} else {
|
||||
_imageNeopassChannel?.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
_imageNeopassChannel?.visibility = View.GONE
|
||||
}
|
||||
|
||||
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate)
|
||||
if (profile != null) {
|
||||
_textChannelName.text = profile.systemState.username
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -72,22 +72,27 @@ class SubscriptionViewHolder : ViewHolder {
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(sub.channel.thumbnail, false);
|
||||
_taskLoadProfile.run(sub.channel.id);
|
||||
_textName.text = sub.channel.name;
|
||||
}
|
||||
|
||||
_textName.text = sub.channel.name;
|
||||
_platformIndicator.setPlatformFromClientID(sub.channel.id.pluginId);
|
||||
}
|
||||
|
||||
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
val dp_46 = 46.dp(itemView.context.resources);
|
||||
val avatar = cachedPolycentricProfile?.profile?.systemState?.avatar?.selectBestImage(dp_46 * dp_46)
|
||||
?.let { it.toURLInfoSystemLinkUrl(cachedPolycentricProfile.profile.system.toProto(), it.process, cachedPolycentricProfile.profile.systemState.servers.toList()) };
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_46 * dp_46)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(this.subscription?.channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(cachedPolycentricProfile?.profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
_textName.text = profile.systemState.username;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+50
-4
@@ -6,13 +6,23 @@ import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.toHumanNumber
|
||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||
import com.futo.platformplayer.views.adapters.SubscriptionViewHolder
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
|
||||
class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Boolean) : AnyAdapter.AnyViewHolder<PlatformAuthorLink>(
|
||||
LayoutInflater.from(_viewGroup.context).inflate(R.layout.list_creator, _viewGroup, false)) {
|
||||
@@ -25,7 +35,15 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
|
||||
private var _authorLink: PlatformAuthorLink? = null;
|
||||
|
||||
val onClick = Event1<PlatformAuthorLink>();
|
||||
|
||||
|
||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
||||
StateApp.instance.scopeGetter,
|
||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
||||
.success { it -> onProfileLoaded(it, true) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load profile.", it);
|
||||
};
|
||||
|
||||
init {
|
||||
_textName = _view.findViewById(R.id.text_channel_name);
|
||||
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
|
||||
@@ -45,12 +63,21 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
|
||||
}
|
||||
|
||||
override fun bind(authorLink: PlatformAuthorLink) {
|
||||
_textName.text = authorLink.name;
|
||||
_creatorThumbnail.setThumbnail(authorLink.thumbnail, false);
|
||||
_taskLoadProfile.cancel();
|
||||
|
||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(authorLink.url, true);
|
||||
if (cachedProfile != null) {
|
||||
onProfileLoaded(cachedProfile, false);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(authorLink.thumbnail, false);
|
||||
_taskLoadProfile.run(authorLink.id);
|
||||
_textName.text = authorLink.name;
|
||||
}
|
||||
|
||||
if(authorLink.subscribers == null || (authorLink.subscribers ?: 0) <= 0L)
|
||||
_textMetadata.visibility = View.GONE;
|
||||
else {
|
||||
_textMetadata.text = if(authorLink?.subscribers ?: 0 > 0) authorLink.subscribers!!.toHumanNumber() + " subscribers" else "";
|
||||
_textMetadata.text = if((authorLink.subscribers ?: 0) > 0) authorLink.subscribers!!.toHumanNumber() + " subscribers" else "";
|
||||
_textMetadata.visibility = View.VISIBLE;
|
||||
}
|
||||
_buttonSubscribe.setSubscribeChannel(authorLink.url);
|
||||
@@ -58,6 +85,25 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
|
||||
_authorLink = authorLink;
|
||||
}
|
||||
|
||||
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
val dp_61 = 61.dp(itemView.context.resources);
|
||||
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_61 * dp_61)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(_authorLink?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
_textName.text = profile.systemState.username;
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CreatorViewHolder";
|
||||
}
|
||||
|
||||
+9
-4
@@ -57,22 +57,27 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(subscription.channel.thumbnail, false);
|
||||
_taskLoadProfile.run(subscription.channel.id);
|
||||
_name.text = subscription.channel.name;
|
||||
}
|
||||
|
||||
_name.text = subscription.channel.name;
|
||||
_subscription = subscription;
|
||||
}
|
||||
|
||||
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
val dp_55 = 55.dp(itemView.context.resources)
|
||||
val avatar = cachedPolycentricProfile?.profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
|
||||
?.let { it.toURLInfoSystemLinkUrl(cachedPolycentricProfile.profile.system.toProto(), it.process, cachedPolycentricProfile.profile.systemState.servers.toList()) };
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(cachedPolycentricProfile?.profile != null, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate);
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
_name.text = profile.systemState.username;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import com.futo.platformplayer.constructs.Event0
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
|
||||
class BigButton : LinearLayout {
|
||||
open class BigButton : LinearLayout {
|
||||
private val _root: LinearLayout;
|
||||
private val _icon: ShapeableImageView;
|
||||
private val _textPrimary: TextView;
|
||||
@@ -78,6 +78,10 @@ class BigButton : LinearLayout {
|
||||
_textSecondary.text = text;
|
||||
return this;
|
||||
}
|
||||
fun withSecondaryTextMaxLines(lines: Int): BigButton {
|
||||
_textSecondary.maxLines = lines;
|
||||
return this;
|
||||
}
|
||||
|
||||
fun withIcon(resourceId: Int, rounded: Boolean = false): BigButton {
|
||||
if (resourceId != -1) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,21 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
|
||||
class ButtonField : LinearLayout, IField {
|
||||
|
||||
@Target(AnnotationTarget.FIELD, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class FormFieldButton(val drawable: Int = 0)
|
||||
|
||||
class ButtonField : BigButton, IField {
|
||||
override var descriptor: FormField? = null;
|
||||
private var _obj : Any? = null;
|
||||
private var _method : Method? = null;
|
||||
@@ -26,17 +34,22 @@ class ButtonField : LinearLayout, IField {
|
||||
return null;
|
||||
};
|
||||
|
||||
private val _title : TextView;
|
||||
private val _subtitle : TextView;
|
||||
//private val _title : TextView;
|
||||
//private val _subtitle : TextView;
|
||||
|
||||
override val onChanged = Event2<IField, Any>();
|
||||
|
||||
constructor(context : Context, attrs : AttributeSet? = null) : super(context, attrs){
|
||||
inflate(context, R.layout.field_button, this);
|
||||
_title = findViewById(R.id.field_title);
|
||||
_subtitle = findViewById(R.id.field_subtitle);
|
||||
//inflate(context, R.layout.field_button, this);
|
||||
//_title = findViewById(R.id.field_title);
|
||||
//_subtitle = findViewById(R.id.field_subtitle);
|
||||
|
||||
setOnClickListener {
|
||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
|
||||
val dp5 = 5.dp(context.resources);
|
||||
setMargins(0, dp5, 0, dp5)
|
||||
};
|
||||
|
||||
super.onClick.subscribe {
|
||||
if(_method?.parameterCount == 1)
|
||||
_method?.invoke(_obj, context);
|
||||
else if(_method?.parameterCount == 2)
|
||||
@@ -51,13 +64,17 @@ class ButtonField : LinearLayout, IField {
|
||||
this._obj = obj;
|
||||
|
||||
val attrField = method.getAnnotation(FormField::class.java);
|
||||
val attrButtonField = method.getAnnotation(FormFieldButton::class.java);
|
||||
if(attrField != null) {
|
||||
_title.text = attrField.title;
|
||||
_subtitle.text = attrField.subtitle;
|
||||
super.withPrimaryText(attrField.title)
|
||||
.withSecondaryText(attrField.subtitle)
|
||||
.withSecondaryTextMaxLines(2);
|
||||
descriptor = attrField;
|
||||
}
|
||||
else
|
||||
_title.text = method.name;
|
||||
super.withPrimaryText(method.name);
|
||||
if(attrButtonField != null)
|
||||
super.withIcon(attrButtonField.drawable, false);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,760Q546,760 593,713Q640,666 640,600L640,440Q640,374 593,327Q546,280 480,280Q414,280 367,327Q320,374 320,440L320,600Q320,666 367,713Q414,760 480,760ZM400,640L560,640L560,560L400,560L400,640ZM400,480L560,480L560,400L400,400L400,480ZM480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520ZM480,840Q415,840 359.5,808Q304,776 272,720L160,720L160,640L244,640Q241,620 240.5,600Q240,580 240,560L160,560L160,480L240,480Q240,460 240.5,440Q241,420 244,400L160,400L160,320L272,320Q286,297 303.5,277Q321,257 344,242L280,176L336,120L422,206Q450,197 479,197Q508,197 536,206L624,120L680,176L614,242Q637,257 655.5,276.5Q674,296 688,320L800,320L800,400L716,400Q719,420 719.5,440Q720,460 720,480L800,480L800,560L720,560Q720,580 719.5,600Q719,620 716,640L800,640L800,720L688,720Q656,776 600.5,808Q545,840 480,840Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M120,800L120,720L600,720L600,800L120,800ZM640,520Q557,520 498.5,461.5Q440,403 440,320Q440,237 498.5,178.5Q557,120 640,120Q723,120 781.5,178.5Q840,237 840,320Q840,403 781.5,461.5Q723,520 640,520ZM120,480L120,400L372,400Q379,422 388,442Q397,462 410,480L120,480ZM120,640L120,560L496,560Q519,574 545,583.5Q571,593 600,597L600,640L120,640ZM620,360L660,360L660,200L620,200L620,360ZM640,440Q648,440 654,434Q660,428 660,420Q660,412 654,406Q648,400 640,400Q632,400 626,406Q620,412 620,420Q620,428 626,434Q632,440 640,440Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M560,600Q577,600 589.5,587.5Q602,575 602,558Q602,541 589.5,528.5Q577,516 560,516Q543,516 530.5,528.5Q518,541 518,558Q518,575 530.5,587.5Q543,600 560,600ZM530,472L590,472Q590,443 596,429.5Q602,416 624,394Q654,364 664,345.5Q674,327 674,302Q674,257 642.5,228.5Q611,200 560,200Q519,200 488.5,223Q458,246 446,284L500,306Q509,281 524.5,268.5Q540,256 560,256Q584,256 599,269.5Q614,283 614,306Q614,320 606,332.5Q598,345 578,364Q545,393 537.5,409.5Q530,426 530,472ZM320,720Q287,720 263.5,696.5Q240,673 240,640L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L320,720ZM320,640L800,640Q800,640 800,640Q800,640 800,640L800,160Q800,160 800,160Q800,160 800,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640ZM160,880Q127,880 103.5,856.5Q80,833 80,800L80,240L160,240L160,800Q160,800 160,800Q160,800 160,800L720,800L720,880L160,880ZM320,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640L320,640Q320,640 320,640Q320,640 320,640L320,160Q320,160 320,160Q320,160 320,160Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M160,720L800,720Q800,720 800,720Q800,720 800,720L800,400L520,400L520,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720ZM160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720L160,720Z"/>
|
||||
</vector>
|
||||
@@ -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>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<string name="history">History</string>
|
||||
<string name="sources">Sources</string>
|
||||
<string name="buy">Buy</string>
|
||||
<string name="faq">FAQ</string>
|
||||
<string name="the_top_source_will_be_considered_primary">The top source will be considered primary</string>
|
||||
<string name="defaults">Defaults</string>
|
||||
<string name="home_screen">Home Screen</string>
|
||||
|
||||
Submodule app/src/stable/assets/sources/nebula updated: aa2a4f2970...8ea9393634
Submodule app/src/stable/assets/sources/odysee updated: 474672bcc0...cbde0c9e9c
Submodule app/src/stable/assets/sources/soundcloud updated: 3e3f95365a...eff8285222
Submodule app/src/stable/assets/sources/twitch updated: 7645b88a76...eb198a3d20
Submodule app/src/stable/assets/sources/youtube updated: eff873edf3...a1d432865e
Submodule app/src/unstable/assets/sources/odysee updated: 474672bcc0...cbde0c9e9c
Submodule app/src/unstable/assets/sources/soundcloud updated: 3e3f95365a...eff8285222
Submodule app/src/unstable/assets/sources/twitch updated: 7645b88a76...eb198a3d20
Submodule app/src/unstable/assets/sources/youtube updated: d05a959174...a1d432865e
Reference in New Issue
Block a user