mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 918b2bbe96 | |||
| e529a3d34d | |||
| 5475778d67 | |||
| c6a3ff0a53 | |||
| cf3587f504 | |||
| d42f104884 | |||
| 6a43568369 | |||
| 85c9cd0a6e | |||
| be5920cfae | |||
| 3d25d94a77 | |||
| fe97850835 | |||
| dab9decd89 | |||
| 854651aa71 | |||
| fdd1af3287 | |||
| 0bf92b6aff | |||
| d9403bf4da | |||
| 716d8caf4d | |||
| 0f0f368a75 | |||
| ff8d7558d4 | |||
| 66f9824b68 | |||
| 44a6e5da38 | |||
| de5a4aa5f3 | |||
| e8007082a7 | |||
| 3c70c5a366 | |||
| eb6e79b055 | |||
| ea59f8dccb | |||
| aef1c584e5 | |||
| c4ce671a87 | |||
| e8a79c87ab | |||
| 249e77a5d3 |
@@ -201,7 +201,7 @@ class PlatformContent {
|
||||
obj = obj ?? {};
|
||||
this.id = obj.id ?? PlatformID(); //PlatformID
|
||||
this.name = obj.name ?? ""; //string
|
||||
this.thumbnails = obj.thumbnails; //Thumbnail[]
|
||||
this.thumbnails = obj.thumbnails ?? new Thumbnails([]); //Thumbnail[]
|
||||
this.author = obj.author; //PlatformAuthorLink
|
||||
this.datetime = obj.datetime ?? obj.uploadDate ?? 0; //OffsetDateTime (Long)
|
||||
this.url = obj.url ?? ""; //String
|
||||
@@ -278,12 +278,49 @@ class PlatformPostDetails extends PlatformPost {
|
||||
super(obj);
|
||||
obj = obj ?? {};
|
||||
this.plugin_type = "PlatformPostDetails";
|
||||
this.rating = obj.rating ?? RatingLikes(-1);
|
||||
this.rating = obj.rating ?? new RatingLikes(-1);
|
||||
this.textType = obj.textType ?? 0;
|
||||
this.content = obj.content ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
class PlatformArticleDetails extends PlatformContent {
|
||||
constructor(obj) {
|
||||
super(obj, 3);
|
||||
obj = obj ?? {};
|
||||
this.plugin_type = "PlatformArticleDetails";
|
||||
this.rating = obj.rating ?? new RatingLikes(-1);
|
||||
this.summary = obj.summary ?? "";
|
||||
this.segments = obj.segments ?? [];
|
||||
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
|
||||
}
|
||||
}
|
||||
class ArticleSegment {
|
||||
constructor(type) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
class ArticleTextSegment extends ArticleSegment {
|
||||
constructor(content, textType) {
|
||||
super(1);
|
||||
this.textType = textType;
|
||||
this.content = content;
|
||||
}
|
||||
}
|
||||
class ArticleImagesSegment extends ArticleSegment {
|
||||
constructor(images) {
|
||||
super(2);
|
||||
this.images = images;
|
||||
}
|
||||
}
|
||||
class ArticleNestedSegment extends ArticleSegment {
|
||||
constructor(nested) {
|
||||
super(9);
|
||||
this.nested = nested;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Sources
|
||||
class VideoSourceDescriptor {
|
||||
constructor(obj) {
|
||||
|
||||
@@ -77,10 +77,14 @@ class AdvancedOrientationListener(private val activity: Activity, private val li
|
||||
lastStableOrientation = newOrientation
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
delay(stabilityThresholdTime)
|
||||
if (newOrientation == lastStableOrientation) {
|
||||
lastOrientation = newOrientation
|
||||
onOrientationChanged.emit(newOrientation)
|
||||
try {
|
||||
delay(stabilityThresholdTime)
|
||||
if (newOrientation == lastStableOrientation) {
|
||||
lastOrientation = newOrientation
|
||||
onOrientationChanged.emit(newOrientation)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to trigger onOrientationChanged", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,4 +115,8 @@ class AdvancedOrientationListener(private val activity: Activity, private val li
|
||||
fun stopListening() {
|
||||
sensorManager.unregisterListener(sensorListener)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "AdvancedOrientationListener"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,11 +360,11 @@ fun String.matchesDomain(queryDomain: String): Boolean {
|
||||
|
||||
val parts = queryDomain.lowercase().split(".");
|
||||
if(parts.size < 3)
|
||||
throw IllegalStateException("Illegal use of wildcards on First-Level-Domain");
|
||||
throw IllegalStateException("Illegal use of wildcards on First-Level-Domain (" + queryDomain + ")");
|
||||
if(parts.size >= 3){
|
||||
val isSLD = slds.contains("." + parts[parts.size - 2] + "." + parts[parts.size - 1]);
|
||||
if(isSLD && parts.size <= 3)
|
||||
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain");
|
||||
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
|
||||
}
|
||||
|
||||
//TODO: Should be safe, but double verify if can't be exploited
|
||||
@@ -372,4 +372,13 @@ fun String.matchesDomain(queryDomain: String): Boolean {
|
||||
}
|
||||
else
|
||||
return this == queryDomain;
|
||||
}
|
||||
|
||||
fun String.getSubdomainWildcardQuery(): String {
|
||||
val domainParts = this.split(".");
|
||||
val sldParts = "." + domainParts[domainParts.size - 2].lowercase() + "." + domainParts[domainParts.size - 1].lowercase();
|
||||
if(slds.contains(sldParts))
|
||||
return "." + domainParts.drop(domainParts.size - 3).joinToString(".");
|
||||
else
|
||||
return "." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
||||
}
|
||||
@@ -485,12 +485,18 @@ class Settings : FragmentedStorageFileJson() {
|
||||
var comments = CommentSettings();
|
||||
@Serializable
|
||||
class CommentSettings {
|
||||
var didAskPolycentricDefault: Boolean = false;
|
||||
|
||||
@FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
|
||||
@DropdownFieldOptionsId(R.array.comment_sections)
|
||||
var defaultCommentSection: Int = 0;
|
||||
var defaultCommentSection: Int = 2;
|
||||
|
||||
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
|
||||
var recommendationsDefault: Boolean = false;
|
||||
|
||||
@FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0)
|
||||
var badReputationCommentsFading: Boolean = true;
|
||||
|
||||
}
|
||||
|
||||
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 7)
|
||||
@@ -539,7 +545,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
class Browsing {
|
||||
@FormField(R.string.enable_video_cache, FieldForm.TOGGLE, R.string.cache_to_quickly_load_previously_fetched_videos, 0)
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var videoCache: Boolean = true;
|
||||
var videoCache: Boolean = false; //Temporary default disabled to prevent ui freeze?
|
||||
}
|
||||
|
||||
@FormField(R.string.casting, "group", R.string.configure_casting, 9)
|
||||
|
||||
@@ -235,13 +235,17 @@ class SettingsDev : FragmentedStorageFileJson() {
|
||||
R.string.test_background_worker_description, 4)
|
||||
fun triggerBackgroundUpdate() {
|
||||
val act = SettingsActivity.getActivity()!!;
|
||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
|
||||
try {
|
||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Starting test background worker");
|
||||
|
||||
val wm = WorkManager.getInstance(act);
|
||||
val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
|
||||
.setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build())
|
||||
.build();
|
||||
wm.enqueue(req);
|
||||
val wm = WorkManager.getInstance(act);
|
||||
val req = OneTimeWorkRequestBuilder<BackgroundWorker>()
|
||||
.setInputData(Data.Builder().putBoolean("bypassMainCheck", true).build())
|
||||
.build();
|
||||
wm.enqueue(req);
|
||||
} catch (e: Throwable) {
|
||||
UIDialogs.showGeneralErrorDialog(act, "Failed to trigger background update", e)
|
||||
}
|
||||
}
|
||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON,
|
||||
R.string.test_background_worker_description, 4)
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.pm.ActivityInfo
|
||||
import android.hardware.SensorManager
|
||||
import android.view.OrientationEventListener
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -34,10 +35,14 @@ class SimpleOrientationListener(
|
||||
lastStableOrientation = newOrientation
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
delay(stabilityThresholdTime)
|
||||
if (newOrientation == lastStableOrientation) {
|
||||
lastOrientation = newOrientation
|
||||
onOrientationChanged.emit(newOrientation)
|
||||
try {
|
||||
delay(stabilityThresholdTime)
|
||||
if (newOrientation == lastStableOrientation) {
|
||||
lastOrientation = newOrientation
|
||||
onOrientationChanged.emit(newOrientation)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to trigger onOrientationChanged", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,4 +57,8 @@ class SimpleOrientationListener(
|
||||
fun stopListening() {
|
||||
orientationListener.disable()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "SimpleOrientationListener"
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
@@ -223,18 +224,20 @@ class UIDialogs {
|
||||
this.visibility = View.GONE;
|
||||
else {
|
||||
this.text = code;
|
||||
this.movementMethod = ScrollingMovementMethod.getInstance();
|
||||
this.visibility = View.VISIBLE;
|
||||
}
|
||||
};
|
||||
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
|
||||
val center = actions.any { it?.center == true };
|
||||
val buttons = actions.map<Action, TextView> { act ->
|
||||
val buttonView = TextView(context);
|
||||
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt();
|
||||
val dp28 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28f, resources.displayMetrics).toInt();
|
||||
val dp14 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14.0f, resources.displayMetrics).toInt();
|
||||
buttonView.layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||
if(actions.size > 1)
|
||||
this.marginEnd = if(actions.size > 2) dp14 else dp28;
|
||||
this.marginStart = if(actions.size >= 2) dp14 / 2 else dp28 / 2;
|
||||
this.marginEnd = if(actions.size >= 2) dp14 / 2 else dp28 / 2;
|
||||
};
|
||||
buttonView.setTextColor(Color.WHITE);
|
||||
buttonView.textSize = 14f;
|
||||
@@ -256,7 +259,7 @@ class UIDialogs {
|
||||
|
||||
return@map buttonView;
|
||||
};
|
||||
if(actions.size <= 1)
|
||||
if(actions.size <= 1 || center)
|
||||
this.gravity = Gravity.CENTER;
|
||||
else
|
||||
this.gravity = Gravity.END;
|
||||
@@ -507,11 +510,13 @@ class UIDialogs {
|
||||
val text: String;
|
||||
val action: ()->Unit;
|
||||
val style: ActionStyle;
|
||||
var center: Boolean;
|
||||
|
||||
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE) {
|
||||
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
|
||||
this.text = text;
|
||||
this.action = action;
|
||||
this.style = style;
|
||||
this.center = center;
|
||||
}
|
||||
}
|
||||
enum class ActionStyle {
|
||||
|
||||
@@ -147,8 +147,6 @@ fun InputStream.copyToOutputStream(inputStreamLength: Long, outputStream: Output
|
||||
@Suppress("DEPRECATION")
|
||||
fun Activity.setNavigationBarColorAndIcons() {
|
||||
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.black);
|
||||
if (Settings.instance.playback.allowVideoToGoUnderCutout)
|
||||
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.insetsController?.setSystemBarsAppearance(0, WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS);
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.os.StrictMode.VmPolicy
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.result.ActivityResult
|
||||
@@ -253,6 +254,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
setNavigationBarColorAndIcons();
|
||||
if (Settings.instance.playback.allowVideoToGoUnderCutout)
|
||||
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
|
||||
runBlocking {
|
||||
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
||||
|
||||
@@ -27,6 +27,8 @@ open class PlatformAuthorLink {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val UNKNOWN = PlatformAuthorLink(PlatformID.NONE, "Unknown", "", null, null);
|
||||
|
||||
fun fromV8(config: SourcePluginConfig, value: V8ValueObject): PlatformAuthorLink {
|
||||
if(value.has("membershipUrl"))
|
||||
return PlatformAuthorMembershipLink.fromV8(config, value);
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ interface IJSContentDetails: IPlatformContent {
|
||||
return when(ContentType.fromInt(type)) {
|
||||
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
|
||||
ContentType.POST -> JSPostDetails(plugin.config, obj);
|
||||
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
|
||||
else -> throw NotImplementedError("Unknown content type ${type}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js.models
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.IPluginSourced
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.post.TextType
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullableList
|
||||
import kotlin.streams.toList
|
||||
|
||||
open class JSArticle : JSContent, IPluginSourced {
|
||||
final override val contentType: ContentType get() = ContentType.POST;
|
||||
|
||||
val summary: String;
|
||||
val thumbnails: Thumbnails?;
|
||||
val segments: List<IJSArticleSegment>;
|
||||
|
||||
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
||||
val contextName = "PlatformPost";
|
||||
|
||||
summary = _content.getOrThrow(config, "summary", contextName);
|
||||
if(_content.has("thumbnails"))
|
||||
thumbnails = Thumbnails.fromV8(config, _content.getOrThrow(config, "thumbnails", contextName));
|
||||
else
|
||||
thumbnails = null;
|
||||
|
||||
|
||||
segments = (obj.getOrThrowNullableList<V8ValueObject>(config, "segments", contextName)
|
||||
?.map { fromV8Segment(config, it) }
|
||||
?.filterNotNull() ?: listOf());
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
fun fromV8Segment(config: SourcePluginConfig, obj: V8ValueObject): IJSArticleSegment? {
|
||||
if(!obj.has("type"))
|
||||
throw IllegalArgumentException("Object missing type field");
|
||||
return when(obj.getOrThrow<SegmentType>(config, "type", "JSArticle.Segment")) {
|
||||
SegmentType.TEXT -> JSTextSegment(config, obj);
|
||||
SegmentType.IMAGES -> JSImagesSegment(config, obj);
|
||||
else -> null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SegmentType(i: Int) {
|
||||
UNKNOWN(0),
|
||||
TEXT(1),
|
||||
IMAGES(2)
|
||||
}
|
||||
|
||||
interface IJSArticleSegment {
|
||||
val type: SegmentType;
|
||||
}
|
||||
class JSTextSegment: IJSArticleSegment {
|
||||
override val type = SegmentType.TEXT;
|
||||
val textType: TextType;
|
||||
val content: String;
|
||||
|
||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) {
|
||||
val contextName = "JSTextSegment";
|
||||
textType = TextType.fromInt((obj.getOrDefault<Int>(config, "textType", contextName, null) ?: 0));
|
||||
content = obj.getOrDefault(config, "content", contextName, "") ?: "";
|
||||
}
|
||||
}
|
||||
class JSImagesSegment: IJSArticleSegment {
|
||||
override val type = SegmentType.IMAGES;
|
||||
val images: List<String>;
|
||||
|
||||
constructor(config: SourcePluginConfig, obj: V8ValueObject) {
|
||||
val contextName = "JSTextSegment";
|
||||
images = obj.getOrThrowNullableList<String>(config, "images", contextName) ?: listOf();
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js.models
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.IPluginSourced
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
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.contents.IPlatformContentDetails
|
||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.post.TextType
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
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.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullableList
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
open class JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails {
|
||||
final override val contentType: ContentType get() = ContentType.ARTICLE;
|
||||
|
||||
private val _hasGetComments: Boolean;
|
||||
private val _hasGetContentRecommendations: Boolean;
|
||||
|
||||
val rating: IRating;
|
||||
|
||||
val summary: String;
|
||||
val thumbnails: Thumbnails?;
|
||||
val segments: List<IJSArticleSegment>;
|
||||
|
||||
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
|
||||
val contextName = "PlatformPost";
|
||||
|
||||
rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
|
||||
summary = _content.getOrThrow(client.config, "summary", contextName);
|
||||
if(_content.has("thumbnails"))
|
||||
thumbnails = Thumbnails.fromV8(client.config, _content.getOrThrow(client.config, "thumbnails", contextName));
|
||||
else
|
||||
thumbnails = null;
|
||||
|
||||
|
||||
segments = (obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", contextName)
|
||||
?.map { fromV8Segment(client, it) }
|
||||
?.filterNotNull() ?: listOf());
|
||||
|
||||
_hasGetComments = _content.has("getComments");
|
||||
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
||||
}
|
||||
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||
if(!_hasGetComments || _content.isClosed)
|
||||
return null;
|
||||
|
||||
if(client is DevJSClient)
|
||||
return StateDeveloper.instance.handleDevCall(client.devID, "videoDetail.getComments()") {
|
||||
return@handleDevCall getCommentsJS(client);
|
||||
}
|
||||
else if(client is JSClient)
|
||||
return getCommentsJS(client);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||
|
||||
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
|
||||
if(!_hasGetContentRecommendations || _content.isClosed)
|
||||
return null;
|
||||
|
||||
if(client is DevJSClient)
|
||||
return StateDeveloper.instance.handleDevCall(client.devID, "postDetail.getContentRecommendations()") {
|
||||
return@handleDevCall getContentRecommendationsJS(client);
|
||||
}
|
||||
else if(client is JSClient)
|
||||
return getContentRecommendationsJS(client);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
|
||||
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
return JSContentPager(_pluginConfig, client, contentPager);
|
||||
}
|
||||
|
||||
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
||||
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
||||
return JSCommentPager(_pluginConfig, client, commentPager);
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromV8Segment(client: JSClient, obj: V8ValueObject): IJSArticleSegment? {
|
||||
if(!obj.has("type"))
|
||||
throw IllegalArgumentException("Object missing type field");
|
||||
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
|
||||
SegmentType.TEXT -> JSTextSegment(client, obj);
|
||||
SegmentType.IMAGES -> JSImagesSegment(client, obj);
|
||||
SegmentType.NESTED -> JSNestedSegment(client, obj);
|
||||
else -> null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SegmentType(val value: Int) {
|
||||
UNKNOWN(0),
|
||||
TEXT(1),
|
||||
IMAGES(2),
|
||||
|
||||
NESTED(9);
|
||||
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int): SegmentType
|
||||
{
|
||||
val result = SegmentType.entries.firstOrNull { it.value == value };
|
||||
if(result == null)
|
||||
throw IllegalArgumentException("Unknown Texttype: $value");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IJSArticleSegment {
|
||||
val type: SegmentType;
|
||||
}
|
||||
class JSTextSegment: IJSArticleSegment {
|
||||
override val type = SegmentType.TEXT;
|
||||
val textType: TextType;
|
||||
val content: String;
|
||||
|
||||
constructor(client: JSClient, obj: V8ValueObject) {
|
||||
val contextName = "JSTextSegment";
|
||||
textType = TextType.fromInt((obj.getOrDefault<Int>(client.config, "textType", contextName, null) ?: 0));
|
||||
content = obj.getOrDefault(client.config, "content", contextName, "") ?: "";
|
||||
}
|
||||
}
|
||||
class JSImagesSegment: IJSArticleSegment {
|
||||
override val type = SegmentType.IMAGES;
|
||||
val images: List<String>;
|
||||
val caption: String;
|
||||
|
||||
constructor(client: JSClient, obj: V8ValueObject) {
|
||||
val contextName = "JSTextSegment";
|
||||
images = obj.getOrThrowNullableList<String>(client.config, "images", contextName) ?: listOf();
|
||||
caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: "";
|
||||
}
|
||||
}
|
||||
class JSNestedSegment: IJSArticleSegment {
|
||||
override val type = SegmentType.NESTED;
|
||||
val nested: IPlatformContent;
|
||||
|
||||
constructor(client: JSClient, obj: V8ValueObject) {
|
||||
val contextName = "JSNestedSegment";
|
||||
val nestedObj = obj.getOrThrow<V8ValueObject>(client.config, "nested", contextName, false);
|
||||
nested = IJSContent.fromV8(client, nestedObj);
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -42,7 +42,12 @@ open class JSContent : IPlatformContent, IPluginSourced {
|
||||
|
||||
id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
|
||||
name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
|
||||
author = PlatformAuthorLink.fromV8(_pluginConfig, _content.getOrThrow(config, "author", contextName));
|
||||
|
||||
val authorObj = _content.getOrDefault<V8ValueObject>(config, "author", contextName, null);
|
||||
if(authorObj != null)
|
||||
author = PlatformAuthorLink.fromV8(_pluginConfig, authorObj);
|
||||
else
|
||||
author = PlatformAuthorLink.UNKNOWN;
|
||||
|
||||
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
|
||||
if(datetimeInt == 0.toLong())
|
||||
|
||||
+9
-1
@@ -368,7 +368,15 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
|
||||
//Add configurable buttons here
|
||||
var buttonDefinitions = listOf(
|
||||
ButtonDefinition(0, R.drawable.ic_home, R.drawable.ic_home_filled, R.string.home, canToggle = true, { it.currentMain is HomeFragment }, { it.navigate<HomeFragment>() }),
|
||||
ButtonDefinition(0, R.drawable.ic_home, R.drawable.ic_home_filled, R.string.home, canToggle = true, { it.currentMain is HomeFragment }, {
|
||||
val currentMain = it.currentMain
|
||||
if (currentMain is HomeFragment) {
|
||||
currentMain.scrollToTop(false)
|
||||
currentMain.reloadFeed()
|
||||
} else {
|
||||
it.navigate<HomeFragment>()
|
||||
}
|
||||
}),
|
||||
ButtonDefinition(1, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate<SubscriptionsFeedFragment>() }),
|
||||
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>() }),
|
||||
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>() }),
|
||||
|
||||
+26
-8
@@ -46,6 +46,14 @@ class HomeFragment : MainFragment() {
|
||||
private var _view: HomeView? = null;
|
||||
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
||||
|
||||
fun reloadFeed() {
|
||||
_view?.reloadFeed()
|
||||
}
|
||||
|
||||
fun scrollToTop(smooth: Boolean) {
|
||||
_view?.scrollToTop(smooth)
|
||||
}
|
||||
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
super.onShownWithView(parameter, isBack);
|
||||
_view?.onShown();
|
||||
@@ -138,17 +146,12 @@ class HomeFragment : MainFragment() {
|
||||
fun onShown() {
|
||||
val lastClients = recyclerData.lastClients;
|
||||
val clients = StatePlatform.instance.getSortedEnabledClient().filter { if (it is JSClient) it.enableInHome else true };
|
||||
|
||||
val feedstyleChanged = recyclerData.loadedFeedStyle != feedStyle;
|
||||
val clientsChanged = lastClients == null || lastClients.size != clients.size || !lastClients.containsAll(clients);
|
||||
val outdated = recyclerData.lastLoad.getNowDiffSeconds() > 60;
|
||||
Logger.i(TAG, "onShown (recyclerData.loadedFeedStyle=${recyclerData.loadedFeedStyle}, recyclerData.lastLoad=${recyclerData.lastLoad}, feedstyleChanged=$feedstyleChanged, clientsChanged=$clientsChanged, outdated=$outdated)")
|
||||
Logger.i(TAG, "onShown (recyclerData.loadedFeedStyle=${recyclerData.loadedFeedStyle}, recyclerData.lastLoad=${recyclerData.lastLoad}, feedstyleChanged=$feedstyleChanged, clientsChanged=$clientsChanged)")
|
||||
|
||||
if(feedstyleChanged || outdated || clientsChanged) {
|
||||
recyclerData.lastLoad = OffsetDateTime.now();
|
||||
recyclerData.loadedFeedStyle = feedStyle;
|
||||
recyclerData.lastClients = clients;
|
||||
loadResults();
|
||||
if(feedstyleChanged || clientsChanged) {
|
||||
reloadFeed()
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -156,6 +159,21 @@ class HomeFragment : MainFragment() {
|
||||
finishRefreshLayoutLoader();
|
||||
}
|
||||
|
||||
fun scrollToTop(smooth: Boolean) {
|
||||
if (smooth) {
|
||||
_recyclerResults.smoothScrollToPosition(0)
|
||||
} else {
|
||||
_recyclerResults.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
||||
fun reloadFeed() {
|
||||
recyclerData.lastLoad = OffsetDateTime.now();
|
||||
recyclerData.loadedFeedStyle = feedStyle;
|
||||
recyclerData.lastClients = StatePlatform.instance.getSortedEnabledClient().filter { if (it is JSClient) it.enableInHome else true };
|
||||
loadResults();
|
||||
}
|
||||
|
||||
override fun getEmptyPagerView(): View? {
|
||||
val dp10 = 10.dp(resources);
|
||||
val dp30 = 30.dp(resources);
|
||||
|
||||
+28
-6
@@ -156,6 +156,14 @@ class PlaylistFragment : MainFragment() {
|
||||
};
|
||||
}
|
||||
|
||||
private fun copyPlaylist(playlist: Playlist) {
|
||||
StatePlaylists.instance.playlistStore.save(playlist)
|
||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(
|
||||
arrayListOf()
|
||||
)
|
||||
UIDialogs.toast("Playlist saved")
|
||||
}
|
||||
|
||||
fun onShown(parameter: Any?) {
|
||||
_taskLoadPlaylist.cancel()
|
||||
|
||||
@@ -170,14 +178,10 @@ class PlaylistFragment : MainFragment() {
|
||||
setButtonDownloadVisible(true)
|
||||
setButtonEditVisible(true)
|
||||
|
||||
if (!StatePlaylists.instance.playlistStore.getItems().contains(parameter)) {
|
||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
|
||||
_fragment.topBar?.assume<NavigationTopBarFragment>()
|
||||
?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
|
||||
StatePlaylists.instance.playlistStore.save(parameter)
|
||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(
|
||||
arrayListOf()
|
||||
)
|
||||
UIDialogs.toast("Playlist saved")
|
||||
copyPlaylist(parameter)
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
@@ -242,6 +246,15 @@ class PlaylistFragment : MainFragment() {
|
||||
}
|
||||
|
||||
private fun download() {
|
||||
val playlist = _playlist ?: return
|
||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
|
||||
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to download", {
|
||||
copyPlaylist(playlist)
|
||||
download()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
_playlist?.let {
|
||||
UISlideOverlays.showDownloadPlaylistOverlay(it, overlayContainer);
|
||||
}
|
||||
@@ -266,6 +279,15 @@ class PlaylistFragment : MainFragment() {
|
||||
override fun canEdit(): Boolean { return _playlist != null; }
|
||||
|
||||
override fun onEditClick() {
|
||||
val playlist = _playlist ?: return
|
||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
|
||||
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to edit the name", {
|
||||
copyPlaylist(playlist)
|
||||
onEditClick()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
_editPlaylistNameInput?.activate();
|
||||
_editPlaylistOverlay?.show();
|
||||
}
|
||||
|
||||
+46
-31
@@ -9,6 +9,7 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
@@ -19,6 +20,7 @@ import androidx.core.view.children
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
@@ -135,10 +137,7 @@ class PostDetailFragment : MainFragment {
|
||||
private val _imageDislikeIcon: ImageView;
|
||||
private val _textDislikes: TextView;
|
||||
|
||||
private val _textComments: TextView;
|
||||
private val _textCommentType: TextView;
|
||||
private val _addCommentView: AddCommentView;
|
||||
private val _toggleCommentType: Toggle;
|
||||
|
||||
private val _rating: PillRatingLikesDislikes;
|
||||
|
||||
@@ -152,6 +151,10 @@ class PostDetailFragment : MainFragment {
|
||||
|
||||
private val _commentsList: CommentsList;
|
||||
|
||||
private var _commentType: Boolean? = null;
|
||||
private val _buttonPolycentric: Button
|
||||
private val _buttonPlatform: Button
|
||||
|
||||
private val _taskLoadPost = if(!isInEditMode) TaskHandler<String, IPlatformPostDetails>(
|
||||
StateApp.instance.scopeGetter,
|
||||
{
|
||||
@@ -198,9 +201,6 @@ class PostDetailFragment : MainFragment {
|
||||
_textDislikes = findViewById(R.id.text_dislikes);
|
||||
|
||||
_commentsList = findViewById(R.id.comments_list);
|
||||
_textCommentType = findViewById(R.id.text_comment_type);
|
||||
_toggleCommentType = findViewById(R.id.toggle_comment_type);
|
||||
_textComments = findViewById(R.id.text_comments);
|
||||
_addCommentView = findViewById(R.id.add_comment_view);
|
||||
|
||||
_rating = findViewById(R.id.rating);
|
||||
@@ -213,6 +213,9 @@ class PostDetailFragment : MainFragment {
|
||||
|
||||
_repliesOverlay = findViewById(R.id.replies_overlay);
|
||||
|
||||
_buttonPolycentric = findViewById(R.id.button_polycentric)
|
||||
_buttonPlatform = findViewById(R.id.button_platform)
|
||||
|
||||
_textContent.setPlatformPlayerLinkMovementMethod(context);
|
||||
|
||||
_buttonSubscribe.onSubscribed.subscribe {
|
||||
@@ -224,9 +227,10 @@ class PostDetailFragment : MainFragment {
|
||||
root.removeView(layoutTop);
|
||||
_commentsList.setPrependedView(layoutTop);
|
||||
|
||||
/*TODO: Why is this here?
|
||||
_commentsList.onCommentsLoaded.subscribe {
|
||||
updateCommentType(false);
|
||||
};
|
||||
};*/
|
||||
|
||||
_commentsList.onRepliesClick.subscribe { c ->
|
||||
val replyCount = c.replyCount ?: 0;
|
||||
@@ -237,7 +241,7 @@ class PostDetailFragment : MainFragment {
|
||||
|
||||
if (c is PolycentricPlatformComment) {
|
||||
var parentComment: PolycentricPlatformComment = c;
|
||||
_repliesOverlay.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference, c,
|
||||
_repliesOverlay.load(_commentType!!, metadata, c.contextUrl, c.reference, c,
|
||||
{ StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) },
|
||||
{
|
||||
val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1);
|
||||
@@ -245,22 +249,23 @@ class PostDetailFragment : MainFragment {
|
||||
parentComment = newComment;
|
||||
});
|
||||
} else {
|
||||
_repliesOverlay.load(_toggleCommentType.value, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
|
||||
_repliesOverlay.load(_commentType!!, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
|
||||
}
|
||||
|
||||
setRepliesOverlayVisible(isVisible = true, animate = true);
|
||||
};
|
||||
|
||||
if (StatePolycentric.instance.enabled) {
|
||||
_buttonPolycentric.setOnClickListener {
|
||||
updateCommentType(false)
|
||||
}
|
||||
} else {
|
||||
_buttonPolycentric.visibility = View.GONE
|
||||
}
|
||||
|
||||
_toggleCommentType.onValueChanged.subscribe {
|
||||
updateCommentType(true);
|
||||
};
|
||||
|
||||
_textCommentType.setOnClickListener {
|
||||
_toggleCommentType.setValue(!_toggleCommentType.value, true);
|
||||
updateCommentType(true);
|
||||
};
|
||||
|
||||
_buttonPlatform.setOnClickListener {
|
||||
updateCommentType(true)
|
||||
}
|
||||
_layoutMonetization.visibility = View.GONE;
|
||||
|
||||
_buttonSupport.setOnClickListener {
|
||||
@@ -432,7 +437,7 @@ class PostDetailFragment : MainFragment {
|
||||
_taskLoadPolycentricProfile.cancel();
|
||||
_version++;
|
||||
|
||||
_toggleCommentType.setValue(false, false);
|
||||
updateCommentType(null)
|
||||
_url = null;
|
||||
_post = null;
|
||||
_postOverview = null;
|
||||
@@ -476,7 +481,8 @@ class PostDetailFragment : MainFragment {
|
||||
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
|
||||
}
|
||||
|
||||
updateCommentType(true);
|
||||
val commentType = !Settings.instance.other.polycentricEnabled || Settings.instance.comments.defaultCommentSection == 1
|
||||
updateCommentType(commentType, true);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -679,20 +685,29 @@ class PostDetailFragment : MainFragment {
|
||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post!!.url, ref, listOfNotNull(extraBytesRef)); };
|
||||
}
|
||||
|
||||
private fun updateCommentType(reloadComments: Boolean) {
|
||||
if (_toggleCommentType.value) {
|
||||
_textCommentType.text = "Platform";
|
||||
_addCommentView.visibility = View.GONE;
|
||||
private fun updateCommentType(commentType: Boolean?, forceReload: Boolean = false) {
|
||||
val changed = commentType != _commentType
|
||||
_commentType = commentType
|
||||
|
||||
if (reloadComments) {
|
||||
fetchComments();
|
||||
}
|
||||
if (commentType == null) {
|
||||
_buttonPlatform.setTextColor(resources.getColor(R.color.gray_ac))
|
||||
_buttonPolycentric.setTextColor(resources.getColor(R.color.gray_ac))
|
||||
} else {
|
||||
_textCommentType.text = "Polycentric";
|
||||
_addCommentView.visibility = View.VISIBLE;
|
||||
_buttonPlatform.setTextColor(resources.getColor(if (commentType) R.color.white else R.color.gray_ac))
|
||||
_buttonPolycentric.setTextColor(resources.getColor(if (!commentType) R.color.white else R.color.gray_ac))
|
||||
|
||||
if (reloadComments) {
|
||||
fetchPolycentricComments()
|
||||
if (commentType) {
|
||||
_addCommentView.visibility = View.GONE;
|
||||
|
||||
if (forceReload || changed) {
|
||||
fetchComments();
|
||||
}
|
||||
} else {
|
||||
_addCommentView.visibility = View.VISIBLE;
|
||||
|
||||
if (forceReload || changed) {
|
||||
fetchPolycentricComments()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -117,8 +117,14 @@ class SuggestionsFragment : MainFragment {
|
||||
} else if (_searchType == SearchType.PLAYLIST) {
|
||||
navigate<PlaylistSearchResultsFragment>(it);
|
||||
} else {
|
||||
if(it.isHttpUrl())
|
||||
navigate<VideoDetailFragment>(it);
|
||||
if(it.isHttpUrl()) {
|
||||
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
|
||||
navigate<RemotePlaylistFragment>(it);
|
||||
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
||||
navigate<ChannelFragment>(it);
|
||||
else
|
||||
navigate<VideoDetailFragment>(it);
|
||||
}
|
||||
else
|
||||
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl));
|
||||
}
|
||||
|
||||
+79
-27
@@ -2,6 +2,7 @@ package com.futo.platformplayer.fragment.mainactivity.main
|
||||
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
@@ -10,6 +11,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import android.view.WindowManager
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -91,22 +93,40 @@ class VideoDetailFragment : MainFragment {
|
||||
}
|
||||
|
||||
private fun updateOrientation() {
|
||||
val a = activity ?: return
|
||||
val isMaximized = state == State.MAXIMIZED
|
||||
val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait;
|
||||
val currentOrientation = _currentOrientation
|
||||
val bypassRotationPrevention = Settings.instance.other.bypassRotationPrevention;
|
||||
val currentRequestedOrientation = a.requestedOrientation
|
||||
val currentOrientation = if (_currentOrientation == -1) currentRequestedOrientation else _currentOrientation
|
||||
val isAutoRotate = Settings.instance.playback.isAutoRotate()
|
||||
val isFs = isFullscreen
|
||||
|
||||
if (isFs && isMaximized) {
|
||||
if (isFullScreenPortraitAllowed) {
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
|
||||
if (isAutoRotate) {
|
||||
a.requestedOrientation = currentOrientation
|
||||
}
|
||||
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
|
||||
if (isAutoRotate) {
|
||||
a.requestedOrientation = currentOrientation
|
||||
}
|
||||
} else {
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
} else if (bypassRotationPrevention) {
|
||||
if (isAutoRotate) {
|
||||
a.requestedOrientation = currentOrientation
|
||||
}
|
||||
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT) {
|
||||
if (isAutoRotate) {
|
||||
a.requestedOrientation = currentOrientation
|
||||
}
|
||||
} else {
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
|
||||
Log.i(TAG, "updateOrientation (isFs = ${isFs}, currentOrientation = ${currentOrientation}, isMaximized = ${isMaximized}, isFullScreenPortraitAllowed = ${isFullScreenPortraitAllowed}) resulted in requested orientation ${activity?.requestedOrientation}");
|
||||
Log.i(TAG, "updateOrientation (isFs = ${isFs}, currentOrientation = ${currentOrientation}, currentRequestedOrientation = ${currentRequestedOrientation}, isMaximized = ${isMaximized}, isAutoRotate = ${isAutoRotate}, isFullScreenPortraitAllowed = ${isFullScreenPortraitAllowed}) resulted in requested orientation ${activity?.requestedOrientation}");
|
||||
}
|
||||
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
@@ -267,6 +287,9 @@ class VideoDetailFragment : MainFragment {
|
||||
}
|
||||
|
||||
_autoRotateChangeListener = AutoRotateChangeListener(requireContext(), Handler()) { _ ->
|
||||
if (updateAutoFullscreen()) {
|
||||
return@AutoRotateChangeListener
|
||||
}
|
||||
updateOrientation()
|
||||
}
|
||||
|
||||
@@ -278,6 +301,9 @@ class VideoDetailFragment : MainFragment {
|
||||
}
|
||||
|
||||
StatePlayer.instance.onRotationLockChanged.subscribe(this) {
|
||||
if (updateAutoFullscreen()) {
|
||||
return@subscribe
|
||||
}
|
||||
updateOrientation()
|
||||
}
|
||||
|
||||
@@ -286,23 +312,29 @@ class VideoDetailFragment : MainFragment {
|
||||
_currentOrientation = it
|
||||
Logger.i(TAG, "Current orientation changed (_currentOrientation = ${_currentOrientation})")
|
||||
|
||||
if (Settings.instance.playback.isAutoRotate()) {
|
||||
if (state == State.MAXIMIZED && !isFullscreen && (it == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || it == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE)) {
|
||||
_viewDetail?.setFullscreen(true)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (state == State.MAXIMIZED && isFullscreen && !Settings.instance.playback.fullscreenPortrait && (it == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || it == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT)) {
|
||||
_viewDetail?.setFullscreen(false)
|
||||
return@subscribe
|
||||
}
|
||||
if (updateAutoFullscreen()) {
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
updateOrientation()
|
||||
}
|
||||
return _view!!;
|
||||
}
|
||||
|
||||
private fun updateAutoFullscreen(): Boolean {
|
||||
if (Settings.instance.playback.isAutoRotate()) {
|
||||
if (state == State.MAXIMIZED && !isFullscreen && (_currentOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || _currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE)) {
|
||||
_viewDetail?.setFullscreen(true)
|
||||
return true
|
||||
}
|
||||
|
||||
if (state == State.MAXIMIZED && isFullscreen && !Settings.instance.playback.fullscreenPortrait && (_currentOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || _currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT)) {
|
||||
_viewDetail?.setFullscreen(false)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun onUserLeaveHint() {
|
||||
val viewDetail = _viewDetail;
|
||||
Logger.i(TAG, "onUserLeaveHint preventPictureInPicture=${viewDetail?.preventPictureInPicture} isCasting=${StateCasting.instance.isCasting} isBackgroundPictureInPicture=${Settings.instance.playback.isBackgroundPictureInPicture()} allowBackground=${viewDetail?.allowBackground}");
|
||||
@@ -426,22 +458,42 @@ class VideoDetailFragment : MainFragment {
|
||||
onMaximized.clear();
|
||||
}
|
||||
|
||||
|
||||
private fun hideSystemUI() {
|
||||
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
|
||||
activity?.window?.insetsController?.let { controller ->
|
||||
controller.hide(WindowInsets.Type.statusBars())
|
||||
controller.hide(WindowInsets.Type.systemBars())
|
||||
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, false)
|
||||
activity?.window?.insetsController?.let { controller ->
|
||||
controller.hide(WindowInsets.Type.statusBars())
|
||||
controller.hide(WindowInsets.Type.systemBars())
|
||||
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
activity?.window?.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
)
|
||||
@Suppress("DEPRECATION")
|
||||
activity?.window?.decorView?.systemUiVisibility = (
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSystemUI() {
|
||||
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, true)
|
||||
activity?.window?.insetsController?.let { controller ->
|
||||
controller.show(WindowInsets.Type.statusBars())
|
||||
controller.show(WindowInsets.Type.systemBars())
|
||||
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_DEFAULT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
WindowCompat.setDecorFitsSystemWindows(requireActivity().window, true)
|
||||
activity?.window?.insetsController?.let { controller ->
|
||||
controller.show(WindowInsets.Type.statusBars())
|
||||
controller.show(WindowInsets.Type.systemBars())
|
||||
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_DEFAULT
|
||||
}
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
@Suppress("DEPRECATION")
|
||||
activity?.window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+144
-43
@@ -23,6 +23,7 @@ import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.view.WindowManager
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
@@ -52,6 +53,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
|
||||
import com.futo.platformplayer.api.media.models.chapters.ChapterType
|
||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
@@ -72,7 +74,6 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.casting.CastConnectionState
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
@@ -103,6 +104,7 @@ import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StateMeta
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
@@ -117,12 +119,14 @@ import com.futo.platformplayer.toHumanBytesSize
|
||||
import com.futo.platformplayer.toHumanNowDiffString
|
||||
import com.futo.platformplayer.toHumanNumber
|
||||
import com.futo.platformplayer.toHumanTime
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
import com.futo.platformplayer.views.MonetizationView
|
||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoView
|
||||
import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
|
||||
import com.futo.platformplayer.views.casting.CastView
|
||||
import com.futo.platformplayer.views.comments.AddCommentView
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.others.Toggle
|
||||
import com.futo.platformplayer.views.overlays.DescriptionOverlay
|
||||
import com.futo.platformplayer.views.overlays.LiveChatOverlay
|
||||
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
||||
@@ -156,6 +160,8 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Dispatcher
|
||||
import org.w3c.dom.Text
|
||||
import userpackage.Protocol
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.math.abs
|
||||
@@ -226,10 +232,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
var preventPictureInPicture: Boolean = false;
|
||||
|
||||
private val _textComments: TextView;
|
||||
private val _textCommentType: TextView;
|
||||
private val _addCommentView: AddCommentView;
|
||||
private val _toggleCommentType: Toggle;
|
||||
private var _tabIndex: Int? = null;
|
||||
|
||||
private val _layoutSkip: LinearLayout;
|
||||
private val _textSkip: TextView;
|
||||
@@ -237,6 +241,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
private val _layoutResume: LinearLayout;
|
||||
private var _jobHideResume: Job? = null;
|
||||
private val _layoutPlayerContainer: TouchInterceptFrameLayout;
|
||||
private val _layoutChangeBottomSection: LinearLayout;
|
||||
|
||||
//Overlays
|
||||
private val _overlayContainer: FrameLayout;
|
||||
@@ -260,12 +265,16 @@ class VideoDetailView : ConstraintLayout {
|
||||
private val _layoutRating: LinearLayout;
|
||||
private val _imageDislikeIcon: ImageView;
|
||||
private val _imageLikeIcon: ImageView;
|
||||
private val _layoutToggleCommentSection: LinearLayout;
|
||||
|
||||
private val _monetization: MonetizationView;
|
||||
|
||||
private val _buttonMore: RoundButton;
|
||||
|
||||
private val _buttonPolycentric: Button
|
||||
private val _buttonPlatform: Button
|
||||
private val _buttonRecommended: Button
|
||||
private val _layoutRecommended: LinearLayout
|
||||
|
||||
private var _didStop: Boolean = false;
|
||||
private var _onPauseCalled = false;
|
||||
private var _lastVideoSource: IVideoSource? = null;
|
||||
@@ -335,9 +344,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
_overlay_loading_spinner = findViewById(R.id.videodetail_loader);
|
||||
_rating = findViewById(R.id.videodetail_rating);
|
||||
_upNext = findViewById(R.id.up_next);
|
||||
_textCommentType = findViewById(R.id.text_comment_type);
|
||||
_toggleCommentType = findViewById(R.id.toggle_comment_type);
|
||||
_layoutToggleCommentSection = findViewById(R.id.layout_toggle_comment_section);
|
||||
_layoutChangeBottomSection = findViewById(R.id.layout_change_bottom_section);
|
||||
_layoutRecommended = findViewById(R.id.layout_recommended)
|
||||
|
||||
_overlayContainer = findViewById(R.id.overlay_container);
|
||||
_overlay_quality_container = findViewById(R.id.videodetail_quality_overview);
|
||||
@@ -359,7 +367,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
_container_content_support = findViewById(R.id.videodetail_container_support);
|
||||
_container_content_browser = findViewById(R.id.videodetail_container_webview)
|
||||
|
||||
_textComments = findViewById(R.id.text_comments);
|
||||
_addCommentView = findViewById(R.id.add_comment_view);
|
||||
_commentsList = findViewById(R.id.comments_list);
|
||||
|
||||
@@ -376,6 +383,10 @@ class VideoDetailView : ConstraintLayout {
|
||||
_imageLikeIcon = findViewById(R.id.image_like_icon);
|
||||
_imageDislikeIcon = findViewById(R.id.image_dislike_icon);
|
||||
|
||||
_buttonPolycentric = findViewById(R.id.button_polycentric)
|
||||
_buttonPlatform = findViewById(R.id.button_platform)
|
||||
_buttonRecommended = findViewById(R.id.button_recommended)
|
||||
|
||||
_monetization = findViewById(R.id.monetization);
|
||||
_player.attachPlayer();
|
||||
|
||||
@@ -429,17 +440,26 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
_commentsList.onCommentsLoaded.subscribe { count ->
|
||||
_commentsCount = count;
|
||||
updateCommentType(false);
|
||||
//TODO: Why is this here ? updateTabs(false);
|
||||
};
|
||||
|
||||
_toggleCommentType.onValueChanged.subscribe {
|
||||
updateCommentType(true);
|
||||
};
|
||||
if (StatePolycentric.instance.enabled) {
|
||||
_buttonPolycentric.setOnClickListener {
|
||||
setTabIndex(0);
|
||||
StateMeta.instance.setLastCommentSection(0);
|
||||
}
|
||||
} else {
|
||||
_buttonPolycentric.visibility = View.GONE
|
||||
}
|
||||
|
||||
_textCommentType.setOnClickListener {
|
||||
_toggleCommentType.setValue(!_toggleCommentType.value, true);
|
||||
updateCommentType(true);
|
||||
};
|
||||
_buttonRecommended.setOnClickListener {
|
||||
setTabIndex(2)
|
||||
}
|
||||
|
||||
_buttonPlatform.setOnClickListener {
|
||||
setTabIndex(1)
|
||||
StateMeta.instance.setLastCommentSection(1);
|
||||
}
|
||||
|
||||
val layoutTop: LinearLayout = findViewById(R.id.layout_top);
|
||||
_container_content_main.removeView(layoutTop);
|
||||
@@ -676,7 +696,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
if (c is PolycentricPlatformComment) {
|
||||
var parentComment: PolycentricPlatformComment = c;
|
||||
_container_content_replies.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference, c,
|
||||
_container_content_replies.load(if (_tabIndex!! == 0) false else true, metadata, c.contextUrl, c.reference, c,
|
||||
{ StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) },
|
||||
{
|
||||
val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1);
|
||||
@@ -684,7 +704,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
parentComment = newComment;
|
||||
});
|
||||
} else {
|
||||
_container_content_replies.load(_toggleCommentType.value, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
|
||||
_container_content_replies.load(if (_tabIndex!! == 0) false else true, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
|
||||
}
|
||||
switchContentView(_container_content_replies);
|
||||
};
|
||||
@@ -1023,7 +1043,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
setDescription("".fixHtmlWhitespace());
|
||||
_descriptionContainer.visibility = View.GONE;
|
||||
_player.clear();
|
||||
_textComments.visibility = View.INVISIBLE;
|
||||
_commentsList.clear();
|
||||
|
||||
_lastVideoSource = null;
|
||||
@@ -1047,7 +1066,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
setLastPositionMilliseconds(_videoResumePositionMilliseconds, false);
|
||||
_addCommentView.setContext(null, null);
|
||||
|
||||
_toggleCommentType.setValue(false, false);
|
||||
setTabIndex(0)
|
||||
_commentsList.clear();
|
||||
|
||||
setEmpty();
|
||||
@@ -1087,12 +1106,11 @@ class VideoDetailView : ConstraintLayout {
|
||||
setLastPositionMilliseconds(_videoResumePositionMilliseconds, false);
|
||||
_addCommentView.setContext(null, null);
|
||||
|
||||
_toggleCommentType.setValue(false, false);
|
||||
setTabIndex(null)
|
||||
|
||||
_title.text = video.name;
|
||||
_rating.visibility = View.GONE;
|
||||
_layoutRating.visibility = View.GONE;
|
||||
_textComments.visibility = View.VISIBLE;
|
||||
|
||||
_minimize_title.text = video.name;
|
||||
_minimize_meta.text = video.author.name;
|
||||
@@ -1277,13 +1295,19 @@ class VideoDetailView : ConstraintLayout {
|
||||
_player.setMetadata(video.name, video.author.name);
|
||||
|
||||
if (video is TutorialFragment.TutorialVideo) {
|
||||
_toggleCommentType.setValue(false, false);
|
||||
setTabIndex(0, true)
|
||||
} else {
|
||||
_toggleCommentType.setValue(!Settings.instance.other.polycentricEnabled || Settings.instance.comments.defaultCommentSection == 1, false);
|
||||
if (Settings.instance.comments.recommendationsDefault) {
|
||||
setTabIndex(2, true)
|
||||
} else {
|
||||
when(Settings.instance.comments.defaultCommentSection) {
|
||||
0 -> if(Settings.instance.other.polycentricEnabled) setTabIndex(0, true) else setTabIndex(1, true);
|
||||
1 -> setTabIndex(1, true);
|
||||
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateCommentType(true);
|
||||
|
||||
//UI
|
||||
_title.text = video.name;
|
||||
_channelName.text = video.author.name;
|
||||
@@ -1310,6 +1334,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
setDescription(video.description.fixHtmlLinks());
|
||||
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
||||
|
||||
|
||||
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url, true);
|
||||
if (cachedPolycentricProfile != null) {
|
||||
setPolycentricProfile(cachedPolycentricProfile, animate = false);
|
||||
@@ -1469,6 +1494,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
if(video.isLive && video.live == null && !video.video.videoSources.any())
|
||||
startLiveTry(video);
|
||||
|
||||
|
||||
_player.updateNextPrevious();
|
||||
updateMoreButtons();
|
||||
|
||||
@@ -1477,13 +1503,13 @@ class VideoDetailView : ConstraintLayout {
|
||||
_buttonMore.visibility = View.GONE
|
||||
_buttonPins.visibility = View.GONE
|
||||
_layoutRating.visibility = View.GONE
|
||||
_layoutToggleCommentSection.visibility = View.GONE
|
||||
_layoutChangeBottomSection.visibility = View.GONE
|
||||
} else {
|
||||
_buttonSubscribe.visibility = View.VISIBLE
|
||||
_buttonMore.visibility = View.VISIBLE
|
||||
_buttonPins.visibility = View.VISIBLE
|
||||
_layoutRating.visibility = View.VISIBLE
|
||||
_layoutToggleCommentSection.visibility = View.VISIBLE
|
||||
_layoutChangeBottomSection.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
fun loadLiveChat(video: IPlatformVideoDetails) {
|
||||
@@ -1594,7 +1620,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
});
|
||||
else
|
||||
_player.setArtwork(null);
|
||||
|
||||
_player.setSource(videoSource, audioSource, _playWhenReady, false);
|
||||
if(subtitleSource != null)
|
||||
_player.swapSubtitles(fragment.lifecycleScope, subtitleSource);
|
||||
@@ -2271,24 +2296,93 @@ class VideoDetailView : ConstraintLayout {
|
||||
};
|
||||
}
|
||||
|
||||
private fun updateCommentType(reloadComments: Boolean) {
|
||||
if (_toggleCommentType.value) {
|
||||
_textCommentType.text = "Platform";
|
||||
_addCommentView.visibility = View.GONE;
|
||||
private fun setTabIndex(index: Int?, forceReload: Boolean = false) {
|
||||
Logger.i(TAG, "setTabIndex (index: ${index}, forceReload: ${forceReload})")
|
||||
val changed = _tabIndex != index || forceReload
|
||||
if (!changed) {
|
||||
return
|
||||
}
|
||||
|
||||
if (reloadComments) {
|
||||
fetchComments();
|
||||
}
|
||||
} else {
|
||||
_textCommentType.text = "Polycentric";
|
||||
_addCommentView.visibility = View.VISIBLE;
|
||||
_taskLoadRecommendations.cancel()
|
||||
_tabIndex = index
|
||||
_buttonRecommended.setTextColor(resources.getColor(if (index == 2) R.color.white else R.color.gray_ac))
|
||||
_buttonPlatform.setTextColor(resources.getColor(if (index == 1) R.color.white else R.color.gray_ac))
|
||||
_buttonPolycentric.setTextColor(resources.getColor(if (index == 0) R.color.white else R.color.gray_ac))
|
||||
_layoutRecommended.removeAllViews()
|
||||
|
||||
if (reloadComments) {
|
||||
fetchPolycentricComments()
|
||||
if (index == null) {
|
||||
_addCommentView.visibility = View.GONE
|
||||
_commentsList.clear()
|
||||
_layoutRecommended.visibility = View.GONE
|
||||
} else if (index == 0) {
|
||||
_addCommentView.visibility = View.VISIBLE
|
||||
_layoutRecommended.visibility = View.GONE
|
||||
fetchPolycentricComments()
|
||||
} else if (index == 1) {
|
||||
_addCommentView.visibility = View.VISIBLE
|
||||
_layoutRecommended.visibility = View.GONE
|
||||
fetchComments()
|
||||
} else if (index == 2) {
|
||||
_addCommentView.visibility = View.GONE
|
||||
_layoutRecommended.visibility = View.VISIBLE
|
||||
_commentsList.clear()
|
||||
|
||||
val url = _url
|
||||
if (url != null) {
|
||||
_layoutRecommended.addView(LoaderView(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(60.dp(resources), 60.dp(resources))
|
||||
start()
|
||||
})
|
||||
_taskLoadRecommendations.run(url)
|
||||
} else {
|
||||
_layoutRecommended.addView(TextView(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(60.dp(resources), 60.dp(resources))
|
||||
textSize = 12.0f
|
||||
text = "No recommendations found"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setRecommendations(pager: IPager<IPlatformContent>?, message: String? = null) {
|
||||
_layoutRecommended.removeAllViews()
|
||||
if (pager == null) {
|
||||
_layoutRecommended.addView(TextView(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply {
|
||||
setMargins(20.dp(resources), 20.dp(resources), 20.dp(resources), 20.dp(resources))
|
||||
}
|
||||
textAlignment = TEXT_ALIGNMENT_CENTER
|
||||
textSize = 14.0f
|
||||
text = message
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
val results = pager.getResults().filter { it is IPlatformVideo }
|
||||
for (result in results) {
|
||||
_layoutRecommended.addView(PreviewVideoView(context, FeedStyle.THUMBNAIL, null, false).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
bind(result)
|
||||
|
||||
hideAddTo()
|
||||
|
||||
onVideoClicked.subscribe { video, _ ->
|
||||
fragment.navigate<VideoDetailFragment>(video).maximizeVideoDetail()
|
||||
}
|
||||
|
||||
onChannelClicked.subscribe {
|
||||
fragment.navigate<ChannelFragment>(it)
|
||||
}
|
||||
|
||||
onAddToWatchLaterClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it));
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//Picture2Picture
|
||||
fun startPictureInPicture() {
|
||||
@@ -2634,6 +2728,13 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
|
||||
|
||||
private val _taskLoadRecommendations = TaskHandler<String, IPager<IPlatformContent>?>(StateApp.instance.scopeGetter, { video?.getContentRecommendations(StatePlatform.instance.getContentClient(it)) })
|
||||
.success { setRecommendations(it, "No recommendations found") }
|
||||
.exception<Throwable> {
|
||||
setRecommendations(null, it.message)
|
||||
Logger.w(TAG, "Failed to load recommendations.", it);
|
||||
};
|
||||
|
||||
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
||||
.success { it -> setPolycentricProfile(it, animate = true) }
|
||||
.exception<Throwable> {
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.getSubdomainWildcardQuery
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.matchesDomain
|
||||
import kotlinx.serialization.encodeToString
|
||||
@@ -109,8 +110,9 @@ class LoginWebViewClient : WebViewClient {
|
||||
//TODO: For now we assume cookies are legit for all subdomains of a top-level domain, this is the most common scenario anyway
|
||||
val cookieString = CookieManager.getInstance().getCookie(request.url.toString());
|
||||
if(cookieString != null) {
|
||||
val domainParts = domain!!.split(".");
|
||||
val cookieDomain = "." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
||||
//val domainParts = domain!!.split(".");
|
||||
//val cookieDomain = "." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
||||
val cookieDomain = domain!!.getSubdomainWildcardQuery();
|
||||
if(_pluginConfig == null || _pluginConfig.allowUrls.any { it == "everywhere" || it.lowercase().matchesDomain(cookieDomain) })
|
||||
_authConfig.cookiesToFind?.let { cookiesToFind ->
|
||||
val cookies = cookieString.split(";");
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.futo.platformplayer.others
|
||||
import android.net.Uri
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebResourceRequest
|
||||
import com.futo.platformplayer.getSubdomainWildcardQuery
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.matchesDomain
|
||||
|
||||
@@ -64,8 +65,8 @@ class WebViewRequirementExtractor {
|
||||
//TODO: For now we assume cookies are legit for all subdomains of a top-level domain, this is the most common scenario anyway
|
||||
val cookieString = CookieManager.getInstance().getCookie(request.url.toString());
|
||||
if(cookieString != null) {
|
||||
val domainParts = domain!!.split(".");
|
||||
val cookieDomain = "." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
||||
//val domainParts = domain!!.split(".");
|
||||
val cookieDomain = domain!!.getSubdomainWildcardQuery()//"." + domainParts.drop(domainParts.size - 2).joinToString(".");
|
||||
if(allowedUrls.any { it == "everywhere" || it.lowercase().matchesDomain(cookieDomain) })
|
||||
cookiesToFind?.let { cookiesToFind ->
|
||||
val cookies = cookieString.split(";");
|
||||
|
||||
@@ -55,11 +55,15 @@ class MediaPlaybackService : Service() {
|
||||
private var _notificationChannel: NotificationChannel? = null;
|
||||
private var _mediaSession: MediaSessionCompat? = null;
|
||||
private var _hasFocus: Boolean = false;
|
||||
private var _isTransientLoss: Boolean = false;
|
||||
private var _focusRequest: AudioFocusRequest? = null;
|
||||
private var _audioFocusLossTime_ms: Long? = null
|
||||
private var _playbackState = PlaybackStateCompat.STATE_NONE;
|
||||
private val _handler = Handler(Looper.getMainLooper())
|
||||
private val _audioFocusRunnable = Runnable { setAudioFocus(false) }
|
||||
private var _lastAudioFocusAttempt_ms: Long? = null
|
||||
private val isPlaying get() = _playbackState != PlaybackStateCompat.STATE_PAUSED &&
|
||||
_playbackState != PlaybackStateCompat.STATE_STOPPED &&
|
||||
_playbackState != PlaybackStateCompat.STATE_NONE &&
|
||||
_playbackState != PlaybackStateCompat.STATE_ERROR
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Logger.v(TAG, "onStartCommand");
|
||||
@@ -161,14 +165,7 @@ class MediaPlaybackService : Service() {
|
||||
Logger.v(TAG, "closeMediaSession");
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
|
||||
val focusRequest = _focusRequest;
|
||||
if (focusRequest != null) {
|
||||
Logger.i(TAG, "Audio focus abandoned")
|
||||
_handler.removeCallbacks(_audioFocusRunnable)
|
||||
_audioManager?.abandonAudioFocusRequest(focusRequest);
|
||||
_focusRequest = null;
|
||||
}
|
||||
_hasFocus = false;
|
||||
abandonAudioFocus()
|
||||
|
||||
val notifManager = _notificationManager;
|
||||
Logger.i(TAG, "Cancelling playback notification (notifManager: ${notifManager != null})");
|
||||
@@ -186,10 +183,12 @@ class MediaPlaybackService : Service() {
|
||||
Logger.v(TAG, "updateMediaSession");
|
||||
var isUpdating = false;
|
||||
val video: IPlatformVideo;
|
||||
var lastBitmap: Bitmap? = null
|
||||
if(videoUpdated == null) {
|
||||
val notifLastVideo = _notif_last_video ?: return;
|
||||
video = notifLastVideo;
|
||||
isUpdating = true;
|
||||
lastBitmap = _notif_last_bitmap;
|
||||
}
|
||||
else
|
||||
video = videoUpdated;
|
||||
@@ -202,6 +201,7 @@ class MediaPlaybackService : Service() {
|
||||
.putString(MediaMetadata.METADATA_KEY_ARTIST, video.author.name)
|
||||
.putString(MediaMetadata.METADATA_KEY_TITLE, video.name)
|
||||
.putLong(MediaMetadata.METADATA_KEY_DURATION, video.duration * 1000)
|
||||
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, lastBitmap)
|
||||
.build());
|
||||
|
||||
val thumbnail = video.thumbnails.getHQThumbnail();
|
||||
@@ -217,8 +217,16 @@ class MediaPlaybackService : Service() {
|
||||
.load(thumbnail)
|
||||
.into(object: CustomTarget<Bitmap>() {
|
||||
override fun onResourceReady(resource: Bitmap,transition: Transition<in Bitmap>?) {
|
||||
if(tag == _notif_last_video)
|
||||
if(tag == _notif_last_video) {
|
||||
notifyMediaSession(video, resource)
|
||||
_mediaSession?.setMetadata(
|
||||
MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadata.METADATA_KEY_ARTIST, video.author.name)
|
||||
.putString(MediaMetadata.METADATA_KEY_TITLE, video.name)
|
||||
.putLong(MediaMetadata.METADATA_KEY_DURATION, video.duration * 1000)
|
||||
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, resource)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
if(tag == _notif_last_video)
|
||||
@@ -339,27 +347,34 @@ class MediaPlaybackService : Service() {
|
||||
.setState(state, pos, 1f, SystemClock.elapsedRealtime())
|
||||
.build());
|
||||
|
||||
if(_focusRequest == null)
|
||||
setAudioFocus();
|
||||
|
||||
_playbackState = state;
|
||||
try {
|
||||
setAudioFocus()
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to set audio focus", e)
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: (TBD) This code probably more fitting inside FutoVideoPlayer, as this service is generally only used for global events
|
||||
private fun setAudioFocus(createFocusRequest: Boolean = true) {
|
||||
_handler.removeCallbacks(_audioFocusRunnable)
|
||||
private fun setAudioFocus() {
|
||||
if (!isPlaying) {
|
||||
return
|
||||
}
|
||||
|
||||
if (_hasFocus) {
|
||||
Log.i(TAG, "Skipped trying to get audio focus because audio focus is already obtained.");
|
||||
if (_hasFocus || _isTransientLoss) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_focusRequest == null) {
|
||||
if (!createFocusRequest) {
|
||||
Log.i(TAG, "Skipped trying to get audio focus because createFocusRequest = false and no focus request exists.");
|
||||
return;
|
||||
}
|
||||
val now = System.currentTimeMillis()
|
||||
val lastAudioFocusAttempt_ms = _lastAudioFocusAttempt_ms
|
||||
if (lastAudioFocusAttempt_ms == null || now - lastAudioFocusAttempt_ms > 1000) {
|
||||
_lastAudioFocusAttempt_ms = now
|
||||
} else {
|
||||
Log.v(TAG, "Skipped trying to get audio focus because gaining audio focus was recently attempted.");
|
||||
return
|
||||
}
|
||||
|
||||
if (_focusRequest == null) {
|
||||
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
||||
.setAcceptsDelayedFocusGain(true)
|
||||
.setOnAudioFocusChangeListener(_audioFocusChangeListener)
|
||||
@@ -374,37 +389,51 @@ class MediaPlaybackService : Service() {
|
||||
val result = _audioManager?.requestAudioFocus(_focusRequest!!)
|
||||
Log.i(TAG, "Audio focus request result $result");
|
||||
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
_hasFocus = true;
|
||||
_hasFocus = true
|
||||
_isTransientLoss = false
|
||||
Log.i(TAG, "Audio focus received");
|
||||
} else if (result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
|
||||
_hasFocus = false
|
||||
_isTransientLoss = false
|
||||
Log.i(TAG, "Audio focus delayed, waiting for focus")
|
||||
} else {
|
||||
_hasFocus = false
|
||||
Log.i(TAG, "Audio focus not granted, retrying in 1 second")
|
||||
_handler.postDelayed(_audioFocusRunnable, 1000)
|
||||
_isTransientLoss = false
|
||||
Log.i(TAG, "Audio focus not granted, retrying later")
|
||||
}
|
||||
|
||||
Log.i(TAG, "Audio focus requested.");
|
||||
}
|
||||
|
||||
private fun abandonAudioFocus() {
|
||||
val focusRequest = _focusRequest;
|
||||
if (focusRequest != null) {
|
||||
Logger.i(TAG, "Audio focus abandoned")
|
||||
_audioManager?.abandonAudioFocusRequest(focusRequest);
|
||||
_focusRequest = null;
|
||||
}
|
||||
_hasFocus = false;
|
||||
_isTransientLoss = false;
|
||||
}
|
||||
|
||||
private val _audioFocusChangeListener =
|
||||
OnAudioFocusChangeListener { focusChange ->
|
||||
try {
|
||||
when (focusChange) {
|
||||
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
_handler.removeCallbacks(_audioFocusRunnable)
|
||||
_hasFocus = true;
|
||||
Log.i(TAG, "Audio focus gained (restartPlaybackAfterLoss = ${Settings.instance.playback.restartPlaybackAfterLoss}, _audioFocusLossTime_ms = $_audioFocusLossTime_ms)");
|
||||
_isTransientLoss = false;
|
||||
|
||||
val audioFocusLossDuration = _audioFocusLossTime_ms?.let { System.currentTimeMillis() - it }
|
||||
_audioFocusLossTime_ms = null
|
||||
Log.i(TAG, "Audio focus gained (restartPlaybackAfterLoss = ${Settings.instance.playback.restartPlaybackAfterLoss}, _audioFocusLossTime_ms = $_audioFocusLossTime_ms, audioFocusLossDuration = ${audioFocusLossDuration})");
|
||||
|
||||
if (Settings.instance.playback.restartPlaybackAfterLoss == 1) {
|
||||
val lossTime_ms = _audioFocusLossTime_ms
|
||||
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 10) {
|
||||
if (audioFocusLossDuration != null && audioFocusLossDuration < 1000 * 10) {
|
||||
MediaControlReceiver.onPlayReceived.emit()
|
||||
}
|
||||
} else if (Settings.instance.playback.restartPlaybackAfterLoss == 2) {
|
||||
val lossTime_ms = _audioFocusLossTime_ms
|
||||
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 30) {
|
||||
if (audioFocusLossDuration != null && audioFocusLossDuration < 1000 * 30) {
|
||||
MediaControlReceiver.onPlayReceived.emit()
|
||||
}
|
||||
} else if (Settings.instance.playback.restartPlaybackAfterLoss == 3) {
|
||||
@@ -412,31 +441,31 @@ class MediaPlaybackService : Service() {
|
||||
}
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||
if (_playbackState != PlaybackStateCompat.STATE_PAUSED &&
|
||||
_playbackState != PlaybackStateCompat.STATE_STOPPED &&
|
||||
_playbackState != PlaybackStateCompat.STATE_NONE &&
|
||||
_playbackState != PlaybackStateCompat.STATE_ERROR) {
|
||||
_audioFocusLossTime_ms = System.currentTimeMillis()
|
||||
_audioFocusLossTime_ms = if (isPlaying) {
|
||||
System.currentTimeMillis()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
_hasFocus = false;
|
||||
_isTransientLoss = true;
|
||||
MediaControlReceiver.onPauseReceived.emit();
|
||||
Log.i(TAG, "Audio focus transient loss (_audioFocusLossTime_ms = ${_audioFocusLossTime_ms})");
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||
Log.i(TAG, "Audio focus transient loss, can duck");
|
||||
_hasFocus = true;
|
||||
_isTransientLoss = true;
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
if (_playbackState != PlaybackStateCompat.STATE_PAUSED &&
|
||||
_playbackState != PlaybackStateCompat.STATE_STOPPED &&
|
||||
_playbackState != PlaybackStateCompat.STATE_NONE &&
|
||||
_playbackState != PlaybackStateCompat.STATE_ERROR) {
|
||||
_audioFocusLossTime_ms = System.currentTimeMillis()
|
||||
_audioFocusLossTime_ms = if (isPlaying) {
|
||||
System.currentTimeMillis()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
_hasFocus = false;
|
||||
MediaControlReceiver.onPauseReceived.emit();
|
||||
abandonAudioFocus();
|
||||
Log.i(TAG, "Audio focus lost");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,6 +612,20 @@ class StateApp {
|
||||
Settings.instance.didFirstStart = true;
|
||||
Settings.instance.save();
|
||||
}
|
||||
/*
|
||||
if(!Settings.instance.comments.didAskPolycentricDefault) {
|
||||
UIDialogs.showDialog(context, R.drawable.neopass, "Default Comment Section", "Grayjay supports 2 comment sections, the Platform comments and Polycentric comments. You can easily toggle between them, but which would you like to be selected by default? This choice can be changed in settings.\n\nPolycentric is still under active development.", null, 1,
|
||||
UIDialogs.Action("Polycentric", {
|
||||
Settings.instance.comments.didAskPolycentricDefault = true;
|
||||
Settings.instance.comments.defaultCommentSection = 0;
|
||||
Settings.instance.save();
|
||||
}, UIDialogs.ActionStyle.PRIMARY, true),
|
||||
UIDialogs.Action("Platform", {
|
||||
Settings.instance.comments.didAskPolycentricDefault = true;
|
||||
Settings.instance.comments.defaultCommentSection = 1;
|
||||
Settings.instance.save();
|
||||
}, UIDialogs.ActionStyle.PRIMARY, true))
|
||||
}*/
|
||||
if(Settings.instance.backup.shouldAutomaticBackup()) {
|
||||
try {
|
||||
StateBackup.startAutomaticBackup();
|
||||
@@ -627,21 +641,26 @@ class StateApp {
|
||||
|
||||
|
||||
fun scheduleBackgroundWork(context: Context, active: Boolean = true, intervalMinutes: Int = 60 * 12) {
|
||||
val wm = WorkManager.getInstance(context);
|
||||
try {
|
||||
val wm = WorkManager.getInstance(context);
|
||||
|
||||
if(active) {
|
||||
if(BuildConfig.DEBUG)
|
||||
UIDialogs.toast(context, "Scheduling background every ${intervalMinutes} minutes");
|
||||
if(active) {
|
||||
if(BuildConfig.DEBUG)
|
||||
UIDialogs.toast(context, "Scheduling background every ${intervalMinutes} minutes");
|
||||
|
||||
val req = PeriodicWorkRequest.Builder(BackgroundWorker::class.java, intervalMinutes.toLong(), TimeUnit.MINUTES, 5, TimeUnit.MINUTES)
|
||||
.setConstraints(Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
.build())
|
||||
.build();
|
||||
wm.enqueueUniquePeriodicWork("backgroundSubscriptions", ExistingPeriodicWorkPolicy.UPDATE, req);
|
||||
val req = PeriodicWorkRequest.Builder(BackgroundWorker::class.java, intervalMinutes.toLong(), TimeUnit.MINUTES, 5, TimeUnit.MINUTES)
|
||||
.setConstraints(Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
.build())
|
||||
.build();
|
||||
wm.enqueueUniquePeriodicWork("backgroundSubscriptions", ExistingPeriodicWorkPolicy.UPDATE, req);
|
||||
}
|
||||
else
|
||||
wm.cancelAllWork();
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to schedule background subscription updates.", e)
|
||||
UIDialogs.toast(context, "Background subscription update failed: " + e.message)
|
||||
}
|
||||
else
|
||||
wm.cancelAllWork();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -64,8 +64,20 @@ class StateCache {
|
||||
Logger.i(TAG, "Subscriptions CachePager get pagers");
|
||||
val pagers: List<IPager<IPlatformContent>>;
|
||||
|
||||
val splitAmount = 900;
|
||||
val timeCacheRetrieving = measureTimeMillis {
|
||||
pagers = listOf(getAllChannelCachePager(allUrls));
|
||||
if(allUrls.size > splitAmount) {
|
||||
var done = 0;
|
||||
var subsetPagers = mutableListOf<IPager<IPlatformContent>>();
|
||||
while(done < allUrls.size) {
|
||||
val subsetUrls = allUrls.subList(done, Math.min(allUrls.size - 1, done + splitAmount));
|
||||
subsetPagers.add(getAllChannelCachePager(subsetUrls));
|
||||
done += splitAmount;
|
||||
}
|
||||
pagers = subsetPagers;
|
||||
}
|
||||
else
|
||||
pagers = listOf(getAllChannelCachePager(allUrls));
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Subscriptions CachePager compiling (retrieved in ${timeCacheRetrieving}ms)");
|
||||
|
||||
@@ -2,11 +2,29 @@ package com.futo.platformplayer.states
|
||||
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringHashSetStorage
|
||||
import com.futo.platformplayer.stores.StringStorage
|
||||
|
||||
class StateMeta {
|
||||
val hiddenVideos = FragmentedStorage.get<StringHashSetStorage>("hiddenVideos");
|
||||
val hiddenCreators = FragmentedStorage.get<StringHashSetStorage>("hiddenCreators");
|
||||
|
||||
val lastCommentSection = FragmentedStorage.get<StringStorage>("defaultCommentSection");
|
||||
|
||||
fun getLastCommentSection(): Int{
|
||||
return when(lastCommentSection.value){
|
||||
"Polycentric" -> 0;
|
||||
"Platform" -> 1;
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
fun setLastCommentSection(value: Int) {
|
||||
when(value) {
|
||||
0 -> lastCommentSection.setAndSave("Polycentric");
|
||||
1 -> lastCommentSection.setAndSave("Platform");
|
||||
else -> lastCommentSection.setAndSave("");
|
||||
}
|
||||
}
|
||||
|
||||
fun isVideoHidden(videoUrl: String) : Boolean {
|
||||
return hiddenVideos.contains(videoUrl);
|
||||
}
|
||||
|
||||
@@ -209,7 +209,17 @@ class StatePlatform {
|
||||
}
|
||||
|
||||
if(_availableClients.distinctBy { it.id }.count() < _availableClients.size) {
|
||||
throw IllegalStateException("Attempted to add 2 clients with the same ID");
|
||||
val dups = _availableClients.filter { x-> _availableClients.count { it.id == x.id } > 1 };
|
||||
val overrideClients = _availableClients.distinctBy { it.id }
|
||||
_availableClients.clear();
|
||||
_availableClients.addAll(overrideClients);
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
UIDialogs.showDialog(context, R.drawable.ic_error_pred, "Duplicate plugin ids detected", "This can cause unexpected behavior, ideally uninstall duplicate plugins (ids)",
|
||||
dups.map { it.name }.joinToString("\n"), 0, UIDialogs.Action("Ok", { }));
|
||||
}
|
||||
|
||||
//throw IllegalStateException("Attempted to add 2 clients with the same ID");
|
||||
}
|
||||
|
||||
enabled = _enabledClientsPersistent.getAllValues()
|
||||
|
||||
@@ -12,10 +12,10 @@ import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
|
||||
import com.futo.platformplayer.stores.v2.StoreSerializer
|
||||
import kotlinx.serialization.KSerializer
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.lang.reflect.Field
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentMap
|
||||
import kotlin.IllegalArgumentException
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.reflect.KType
|
||||
@@ -318,8 +318,12 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
||||
});
|
||||
}
|
||||
|
||||
private val inLimit = 990;
|
||||
fun <X> queryInPager(field: KProperty<*>, obj: List<String>, pageSize: Int, convert: (I)->X): IPager<X> = queryInPager(validateFieldName(field), obj, pageSize, convert);
|
||||
fun <X> queryInPager(field: String, obj: List<String>, pageSize: Int, convert: (I)->X): IPager<X> {
|
||||
if(obj.size > inLimit) {
|
||||
throw IllegalArgumentException("Too many objects requested (IN query), create subqueries of ${inLimit}");
|
||||
}
|
||||
return AdhocPager({
|
||||
queryInPage(field, obj, it - 1, pageSize).map(convert);
|
||||
});
|
||||
|
||||
+17
-5
@@ -130,6 +130,11 @@ open class PreviewVideoView : LinearLayout {
|
||||
_button_add_to_watch_later.setOnClickListener { currentVideo?.let { onAddToWatchLaterClicked.emit(it); } }
|
||||
}
|
||||
|
||||
fun hideAddTo() {
|
||||
_button_add_to.visibility = View.GONE
|
||||
_button_add_to_queue.visibility = View.GONE
|
||||
}
|
||||
|
||||
protected open fun inflate(feedStyle: FeedStyle) {
|
||||
inflate(context, when(feedStyle) {
|
||||
FeedStyle.PREVIEW -> R.layout.list_video_preview
|
||||
@@ -165,11 +170,18 @@ open class PreviewVideoView : LinearLayout {
|
||||
|
||||
_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);
|
||||
|
||||
val thumbnail = content.author.thumbnail
|
||||
if (thumbnail != null) {
|
||||
_imageChannel?.visibility = View.VISIBLE
|
||||
_imageChannel?.let {
|
||||
Glide.with(_imageChannel)
|
||||
.load(content.author.thumbnail)
|
||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||
.into(_imageChannel);
|
||||
}
|
||||
} else {
|
||||
_imageChannel?.visibility = View.GONE
|
||||
}
|
||||
|
||||
_textChannelName.text = content.author.name
|
||||
|
||||
@@ -254,6 +254,7 @@ class CommentsList : ConstraintLayout {
|
||||
|
||||
fun clear() {
|
||||
cancel();
|
||||
setLoading(false);
|
||||
_comments.clear();
|
||||
_commentsPager = null;
|
||||
_adapterComments.notifyDataSetChanged();
|
||||
|
||||
@@ -299,43 +299,41 @@
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_change_bottom_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="8dp">
|
||||
android:background="@drawable/background_videodetail_description"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginLeft="14dp"
|
||||
android:layout_marginRight="14dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_comments"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="14dp"
|
||||
android:fontFamily="@font/inter_medium"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="17dp"
|
||||
android:text="@string/comments" />
|
||||
|
||||
<Space
|
||||
<Button
|
||||
android:id="@+id/button_polycentric"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="match_parent" />
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:text="Polycentric"
|
||||
android:textColor="#fff"
|
||||
android:textSize="10dp"
|
||||
android:lines="1"
|
||||
android:ellipsize="marquee"
|
||||
android:padding="10dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_comment_type"
|
||||
android:layout_width="wrap_content"
|
||||
<Button
|
||||
android:id="@+id/button_platform"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/inter_extra_light"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:text="@string/polycentric"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<com.futo.platformplayer.views.others.Toggle
|
||||
android:id="@+id/toggle_comment_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:toggleEnabled="false"
|
||||
android:layout_marginEnd="14dp" />
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:text="Platform"
|
||||
android:textColor="#fff"
|
||||
android:textSize="10dp"
|
||||
android:lines="1"
|
||||
android:ellipsize="marquee"
|
||||
android:padding="10dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.futo.platformplayer.views.comments.AddCommentView
|
||||
|
||||
@@ -467,49 +467,64 @@
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_change_bottom_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="10dp">
|
||||
android:background="@drawable/background_videodetail_description"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginLeft="14dp"
|
||||
android:layout_marginRight="14dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_comments"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="14dp"
|
||||
android:fontFamily="@font/inter_medium"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="17dp"
|
||||
android:text="@string/comments" />
|
||||
|
||||
<Space
|
||||
<Button
|
||||
android:id="@+id/button_polycentric"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="match_parent" />
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:text="Polycentric"
|
||||
android:textColor="#fff"
|
||||
android:textSize="10dp"
|
||||
android:lines="1"
|
||||
android:ellipsize="marquee"
|
||||
android:padding="10dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_toggle_comment_section"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<Button
|
||||
android:id="@+id/button_platform"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:text="Platform"
|
||||
android:textColor="#fff"
|
||||
android:textSize="10dp"
|
||||
android:lines="1"
|
||||
android:ellipsize="marquee"
|
||||
android:padding="10dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_comment_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/inter_extra_light"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:text="@string/polycentric"
|
||||
android:layout_marginEnd="8dp" />
|
||||
<Button
|
||||
android:id="@+id/button_recommended"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:text="Recommended"
|
||||
android:textColor="#fff"
|
||||
android:textSize="10dp"
|
||||
android:lines="1"
|
||||
android:ellipsize="marquee"
|
||||
android:padding="10dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.futo.platformplayer.views.others.Toggle
|
||||
android:id="@+id/toggle_comment_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:toggleEnabled="false"
|
||||
android:layout_marginEnd="14dp" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_recommended"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:layout_marginLeft="12dp"
|
||||
android:layout_marginRight="12dp">
|
||||
</LinearLayout>
|
||||
|
||||
<com.futo.platformplayer.views.comments.AddCommentView
|
||||
|
||||
@@ -412,6 +412,8 @@
|
||||
<string name="preferred_preview_quality_description">Default quality while previewing a video in a feed</string>
|
||||
<string name="primary_language">Primary Language</string>
|
||||
<string name="default_comment_section">Default Comment Section</string>
|
||||
<string name="default_recommendations">Recommendations as Default</string>
|
||||
<string name="default_recommendations_description">Show recommendations as default, instead of comments.</string>
|
||||
<string name="bad_reputation_comments_fading">Bad Reputation Comment Fading</string>
|
||||
<string name="bad_reputation_comments_fading_description">If comments with a very bad reputation should be faded. Disabling may worsen experience.</string>
|
||||
<string name="reinstall_embedded_plugins">Reinstall Embedded Plugins</string>
|
||||
@@ -914,6 +916,7 @@
|
||||
<string-array name="comment_sections">
|
||||
<item>Polycentric</item>
|
||||
<item>Platform</item>
|
||||
<item>Last Selected</item>
|
||||
</string-array>
|
||||
<string-array name="audio_languages">
|
||||
<item>English</item>
|
||||
|
||||
Submodule app/src/stable/assets/sources/youtube updated: a4766ec223...c0a601817d
@@ -68,9 +68,19 @@ class ExtensionsFormattingTests {
|
||||
@Test
|
||||
fun testMatchesDomain() {
|
||||
assertTrue("google.com".matchesDomain("google.com"))
|
||||
assertTrue("google.com".matchesDomain(".google.com"))
|
||||
assertFalse("yahoo.com".matchesDomain("google.com"))
|
||||
assertTrue("mail.google.com".matchesDomain(".google.com"))
|
||||
}
|
||||
@Test
|
||||
fun testPrimaryDomain() {
|
||||
assertEquals(".google.com", "google.com".getSubdomainWildcardQuery());
|
||||
assertEquals(".google.com", "test.google.com".getSubdomainWildcardQuery());
|
||||
assertEquals(".google.com", "test1.test2.google.com".getSubdomainWildcardQuery());
|
||||
assertEquals(".google.co.uk", "google.co.uk".getSubdomainWildcardQuery());
|
||||
assertEquals(".google.co.uk", "test.google.co.uk".getSubdomainWildcardQuery());
|
||||
assertEquals(".google.co.uk", "test1.test2.google.co.uk".getSubdomainWildcardQuery());
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTimeDiff() {
|
||||
|
||||
Submodule app/src/unstable/assets/sources/youtube updated: a4766ec223...c0a601817d
Reference in New Issue
Block a user