Compare commits

..

48 Commits

Author SHA1 Message Date
Kelvin 0a0c16524a Allow hiding privacy mode and FAQ without breaking existing orderings 2024-09-10 23:10:36 +02:00
Kelvin 9b843a155e Revert "Allow more tabs to be hidden."
This reverts commit 8c4e511883
2024-09-10 20:51:30 +00:00
Kelvin cb085acbff Submods 2024-09-10 21:06:30 +02:00
Kelvin c3d7df166b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-10 20:40:17 +02:00
Kelvin d312062125 Fix content recommendations on offline videos 2024-09-10 20:40:14 +02:00
Koen J e2453192aa More gracefully handle failing to set plugin auth. 2024-09-10 17:27:00 +02:00
Koen J 0f4e4a7d97 Allow configuring stability threshold time and ensure there is no more than 1 job active at a time for SimpleOrientationListener. 2024-09-10 15:54:27 +02:00
Koen J f20a708b36 Check both length and null for 'No recommendations found' 2024-09-10 12:25:30 +02:00
Koen J 8c4e511883 Allow more tabs to be hidden. 2024-09-10 12:24:42 +02:00
Koen J a4a3b8d664 Implement full autorotate lock (default off). 2024-09-10 11:59:44 +02:00
Koen J bf6530ea81 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-10 10:34:46 +02:00
Koen J 4a80c2aab1 Moved Autoplay button to top and load recommendations now appropriately uses 'StatePlatform.instance.getContentRecommendations(v.url)' for local videos. 2024-09-10 10:34:36 +02:00
Kelvin 527bbfe43f Fix watchlater re-downloading every time videos are reordered 2024-09-09 23:07:15 +02:00
Koen J d8e1edb60b Added autoplay icon. 2024-09-09 15:52:55 +02:00
Koen J 245b5f74c0 Increased scrubber size a bit and made add comment view invisible for platform comments. 2024-09-09 15:50:36 +02:00
Koen J e9a1f63415 Added autoplay setting. 2024-09-09 15:20:31 +02:00
Koen J ec370dd94b Added autoplay feature. 2024-09-09 14:58:08 +02:00
Koen J e39d862ef3 Added rotation zone setting allowing you to specify the rotation to be less sensitive (default 45 degrees). Added reverse portrait setting allowing you to allow reverse portrait (default off). Added setting to hide recommendations. 2024-09-09 12:41:16 +02:00
Koen J 7b065654aa Updated submodules. 2024-09-09 10:52:52 +02:00
Kelvin 918b2bbe96 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-06 18:46:33 +02:00
Kelvin e529a3d34d Temporariyl disable video cache 2024-09-06 18:46:26 +02:00
Koen J 5475778d67 Force reload. 2024-09-06 18:24:52 +02:00
Kelvin c6a3ff0a53 Stable ref 2024-09-06 17:42:28 +02:00
Kelvin cf3587f504 last selected comment section option and default 2024-09-06 17:21:50 +02:00
Kelvin d42f104884 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-06 16:29:31 +02:00
Kelvin 6a43568369 Dialog improvement, prep dialog 2024-09-06 16:29:24 +02:00
Koen J 85c9cd0a6e Recommendation mostly finished. 2024-09-06 16:00:13 +02:00
Koen J be5920cfae Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-06 15:36:05 +02:00
Koen J 3d25d94a77 Mostly implemented recommendations. 2024-09-06 15:35:59 +02:00
Kelvin fe97850835 Remove target size 2024-09-06 15:33:55 +02:00
Kelvin dab9decd89 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-06 15:32:58 +02:00
Kelvin 854651aa71 Fix notification thumbnail pixelation on newer androids 2024-09-06 15:32:49 +02:00
Koen J fdd1af3287 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-06 10:13:09 +02:00
Koen J 0bf92b6aff Home button refresh added. Possible fix for Grayjay starting playback after hours of being inactive. Scroll to top and reload feed. 2024-09-06 10:12:23 +02:00
Kelvin d9403bf4da Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-05 20:18:37 +02:00
Kelvin 716d8caf4d SLD crash fix 2024-09-05 20:18:29 +02:00
Koen J 0f0f368a75 Do not allow downloading/editing name of temporary playlist. 2024-09-05 13:14:41 +02:00
Koen J ff8d7558d4 Re-added bypass rotation prevention. 2024-09-05 12:42:12 +02:00
Koen J 66f9824b68 Finetuning rotation. 2024-09-05 10:53:42 +02:00
Koen J 44a6e5da38 Added background subscription upadte failed toast and removed home page refresh when older than a minute. 2024-09-05 10:04:53 +02:00
Kelvin de5a4aa5f3 Duplicate client id dialog and filtering, scrollable code block for dialogs 2024-09-04 21:24:26 +02:00
Kelvin e8007082a7 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-04 20:02:55 +02:00
Kelvin 3c70c5a366 Better handling of null author, search url fixes, Handling of more than 1000 subscriptions 2024-09-04 20:02:44 +02:00
Koen J eb6e79b055 Catch WorkManager crash. 2024-09-04 14:24:11 +02:00
Kelvin ea59f8dccb Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-03 22:33:04 +02:00
Kelvin aef1c584e5 Article spec structures 2024-09-03 22:32:50 +02:00
Koen J c4ce671a87 Fixed crash on Android 10 related to showing and hiding system UI when entering fullscreen. Made Platform comments the default. 2024-09-03 19:59:27 +02:00
Koen J e8a79c87ab Added additional palce where isTransientLoss is reset. 2024-09-03 13:03:18 +02:00
48 changed files with 1132 additions and 364 deletions
+39 -2
View File
@@ -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(".");
}
@@ -471,26 +471,51 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
var fullscreenPortrait: Boolean = false;
@FormField(R.string.reverse_portrait, FieldForm.TOGGLE, R.string.reverse_portrait_description, 14)
var reversePortrait: Boolean = false;
@FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 14)
@FormField(R.string.rotation_zone, FieldForm.DROPDOWN, R.string.rotation_zone_description, 15)
@DropdownFieldOptionsId(R.array.rotation_zone)
var rotationZone: Int = 2;
@FormField(R.string.stability_threshold_time, FieldForm.DROPDOWN, R.string.stability_threshold_time_description, 16)
@DropdownFieldOptionsId(R.array.rotation_threshold_time)
var stabilityThresholdTime: Int = 1;
@FormField(R.string.full_autorotate_lock, FieldForm.TOGGLE, R.string.full_autorotate_lock_description, 17)
var fullAutorotateLock: Boolean = false;
@FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 18)
var preferWebmVideo: Boolean = false;
@FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 15)
@FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 19)
var preferWebmAudio: Boolean = false;
@FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 16)
@FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 20)
var allowVideoToGoUnderCutout: Boolean = true;
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
var autoplay: Boolean = false;
}
@FormField(R.string.comments, "group", R.string.comments_description, 6)
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.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
var hideRecommendations: 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 +564,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,8 +5,10 @@ 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.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -16,28 +18,51 @@ class SimpleOrientationListener(
) {
private var lastOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
private var lastStableOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
private val stabilityThresholdTime = 500L
private var _currentJob: Job? = null
val onOrientationChanged = Event1<Int>()
private val orientationListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_UI) {
override fun onOrientationChanged(orientation: Int) {
//val rotationZone = 45
val stabilityThresholdTime = when (Settings.instance.playback.stabilityThresholdTime) {
0 -> 100L
1 -> 500L
2 -> 750L
3 -> 1000L
4 -> 1500L
5 -> 2000L
else -> 500L
}
val rotationZone = when (Settings.instance.playback.rotationZone) {
0 -> 15
1 -> 30
2 -> 45
else -> 45
}
val newOrientation = when {
orientation in 45..134 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
orientation in 135..224 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
orientation in 225..314 -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
orientation in 315..360 || orientation in 0..44 -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
orientation in (90 - rotationZone)..(90 + rotationZone - 1) -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
orientation in (180 - rotationZone)..(180 + rotationZone - 1) -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
orientation in (270 - rotationZone)..(270 + rotationZone - 1) -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
orientation in (360 - rotationZone)..(360 + rotationZone - 1) || orientation in 0..(rotationZone - 1) -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
else -> lastOrientation
}
if (newOrientation != lastStableOrientation) {
lastStableOrientation = newOrientation
lifecycleScope.launch(Dispatchers.Main) {
delay(stabilityThresholdTime)
if (newOrientation == lastStableOrientation) {
lastOrientation = newOrientation
onOrientationChanged.emit(newOrientation)
_currentJob?.cancel()
_currentJob = lifecycleScope.launch(Dispatchers.Main) {
try {
delay(stabilityThresholdTime)
if (newOrientation == lastStableOrientation) {
lastOrientation = newOrientation
onOrientationChanged.emit(newOrientation)
}
} catch (e: Throwable) {
Logger.i(TAG, "Failed to trigger onOrientationChanged", e)
}
}
}
@@ -50,6 +75,12 @@ class SimpleOrientationListener(
}
fun stopListening() {
_currentJob?.cancel()
_currentJob = null
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);
@@ -237,7 +237,8 @@ open class JSClient : IPlatformClient {
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false,
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false
);
try {
@@ -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();
}
}
@@ -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);
}
}
@@ -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())
@@ -209,26 +209,30 @@ class MenuBottomBarFragment : MainActivityFragment() {
_moreButtons.clear();
_layoutMoreButtons.removeAllViews();
var insertedButtons = 0;
//Force buy to be on top for more buttons
val buyIndex = buttons.indexOfFirst { b -> b.id == 98 };
if (buyIndex != -1) {
val button = buttons[buyIndex]
buttons.removeAt(buyIndex)
buttons.add(0, button)
insertedButtons++;
}
//Force faq to be second
val faqIndex = buttons.indexOfFirst { b -> b.id == 97 };
if (faqIndex != -1) {
val button = buttons[faqIndex]
buttons.removeAt(faqIndex)
buttons.add(if (buttons.size == 1) 1 else 0, button)
buttons.add(if (insertedButtons == 1) 1 else 0, button)
insertedButtons++;
}
//Force privacy to be third
val privacyIndex = buttons.indexOfFirst { b -> b.id == 96 };
if (privacyIndex != -1) {
val button = buttons[privacyIndex]
buttons.removeAt(privacyIndex)
buttons.add(if (buttons.size == 2) 2 else 1, button)
buttons.add(if (insertedButtons == 2) 2 else (if(insertedButtons == 1) 1 else 0), button)
insertedButtons++;
}
for (data in buttons) {
@@ -310,19 +314,6 @@ class MenuBottomBarFragment : MainActivityFragment() {
if (!StatePayment.instance.hasPaid) {
newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid_filled, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate<BuyFragment>() }))
}
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = false, { false }, {
it.navigate<BrowserFragment>(Settings.URL_FAQ);
}))
newCurrentButtonDefinitions.add(ButtonDefinition(96, R.drawable.ic_disabled_visible, R.drawable.ic_disabled_visible, R.string.privacy_mode, canToggle = false, { false }, {
UIDialogs.showDialog(context, R.drawable.ic_disabled_visible_purple, "Privacy Mode",
"All requests will be processed anonymously (unauthenticated), playback and history tracking will be disabled.\n\nTap the icon to disable.", null, 0,
UIDialogs.Action("Cancel", {
StateApp.instance.setPrivacyMode(false);
}, UIDialogs.ActionStyle.NONE),
UIDialogs.Action("Enable", {
StateApp.instance.setPrivacyMode(true);
}, UIDialogs.ActionStyle.PRIMARY));
}))
//Add conditional buttons here, when you add a conditional button, be sure to add the register and unregister events for when the button needs to be updated
@@ -368,7 +359,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>() }),
@@ -387,6 +386,19 @@ class MenuBottomBarFragment : MainActivityFragment() {
if (c is Activity) {
c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken);
}
}),
ButtonDefinition(96, R.drawable.ic_disabled_visible, R.drawable.ic_disabled_visible, R.string.privacy_mode, canToggle = true, { false }, {
UIDialogs.showDialog(it.context ?: return@ButtonDefinition, R.drawable.ic_disabled_visible_purple, "Privacy Mode",
"All requests will be processed anonymously (unauthenticated), playback and history tracking will be disabled.\n\nTap the icon to disable.", null, 0,
UIDialogs.Action("Cancel", {
StateApp.instance.setPrivacyMode(false);
}, UIDialogs.ActionStyle.NONE),
UIDialogs.Action("Enable", {
StateApp.instance.setPrivacyMode(true);
}, UIDialogs.ActionStyle.PRIMARY));
}),
ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = true, { false }, {
it.navigate<BrowserFragment>(Settings.URL_FAQ);
})
//96 is reserved for privacy button
//98 is reserved for buy button
@@ -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);
@@ -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();
}
@@ -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()
}
}
}
}
@@ -397,23 +397,43 @@ class SourceDetailFragment : MainFragment() {
UIDialogs.Action("Cancel", {}, UIDialogs.ActionStyle.NONE),
UIDialogs.Action("Login", {
LoginActivity.showLogin(StateApp.instance.context, config) {
StatePlugins.instance.setPluginAuth(config.id, it);
reloadSource(config.id);
try {
StatePlugins.instance.setPluginAuth(config.id, it);
reloadSource(config.id);
} catch (e: Throwable) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
context?.let { c -> UIDialogs.showGeneralErrorDialog(c, "Failed to set plugin authentication (loginSource, loginWarning)", e) }
}
Logger.e(TAG, "Failed to set plugin authentication (loginSource, loginWarning)", e)
}
};
}, UIDialogs.ActionStyle.PRIMARY))
}
else
LoginActivity.showLogin(StateApp.instance.context, config) {
StatePlugins.instance.setPluginAuth(config.id, it);
reloadSource(config.id);
try {
StatePlugins.instance.setPluginAuth(config.id, it);
reloadSource(config.id);
} catch (e: Throwable) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
context?.let { c -> UIDialogs.showGeneralErrorDialog(c, "Failed to set plugin authentication (loginSource)", e) }
}
Logger.e(TAG, "Failed to set plugin authentication (loginSource)", e)
}
};
}
private fun logoutSource(clear: Boolean = true) {
val config = _config ?: return;
StatePlugins.instance.setPluginAuth(config.id, null);
reloadSource(config.id);
try {
StatePlugins.instance.setPluginAuth(config.id, null);
reloadSource(config.id);
} catch (e: Throwable) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
context?.let { c -> UIDialogs.showGeneralErrorDialog(c, "Failed to clear plugin authentication", e) }
}
Logger.e(TAG, "Failed to clear plugin authentication", e)
}
//TODO: Maybe add a dialog option..
if(Settings.instance.plugins.clearCookiesOnLogout && clear) {
@@ -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));
}
@@ -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,60 @@ 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 fullAutorotateLock = Settings.instance.playback.fullAutorotateLock
val currentRequestedOrientation = a.requestedOrientation
var currentOrientation = if (_currentOrientation == -1) currentRequestedOrientation else _currentOrientation
if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT && !Settings.instance.playback.reversePortrait)
currentOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
val isAutoRotate = Settings.instance.playback.isAutoRotate()
val isFs = isFullscreen
if (isFs && isMaximized) {
if (isFullScreenPortraitAllowed) {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
if (fullAutorotateLock) {
if (isFs && isMaximized) {
if (isFullScreenPortraitAllowed) {
if (isAutoRotate) {
a.requestedOrientation = currentOrientation
}
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
if (isAutoRotate || currentOrientation != currentRequestedOrientation && (currentRequestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || currentRequestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT)) {
a.requestedOrientation = currentOrientation
}
} else {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
} else if (bypassRotationPrevention) {
a.requestedOrientation = currentOrientation
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT) {
a.requestedOrientation = currentOrientation
} else {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
} else {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
if (isFs && isMaximized) {
if (isFullScreenPortraitAllowed) {
a.requestedOrientation = currentOrientation
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
a.requestedOrientation = currentOrientation
} else if (currentRequestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || currentRequestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
//Don't change anything
} else {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
} else if (bypassRotationPrevention) {
a.requestedOrientation = currentOrientation
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT) {
a.requestedOrientation = currentOrientation
} else {
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}, fullAutorotateLock = ${fullAutorotateLock}, currentRequestedOrientation = ${currentRequestedOrientation}, isMaximized = ${isMaximized}, isAutoRotate = ${isAutoRotate}, isFullScreenPortraitAllowed = ${isFullScreenPortraitAllowed}) resulted in requested orientation ${activity?.requestedOrientation}");
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
@@ -267,6 +307,9 @@ class VideoDetailFragment : MainFragment {
}
_autoRotateChangeListener = AutoRotateChangeListener(requireContext(), Handler()) { _ ->
if (updateAutoFullscreen()) {
return@AutoRotateChangeListener
}
updateOrientation()
}
@@ -278,6 +321,9 @@ class VideoDetailFragment : MainFragment {
}
StatePlayer.instance.onRotationLockChanged.subscribe(this) {
if (updateAutoFullscreen()) {
return@subscribe
}
updateOrientation()
}
@@ -286,23 +332,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 +478,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
}
}
@@ -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;
@@ -281,6 +290,7 @@ class VideoDetailView : ConstraintLayout {
private var _commentsCount = 0;
private var _polycentricProfile: PolycentricCache.CachedPolycentricProfile? = null;
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
private var _autoplayVideo: IPlatformVideo? = null
//Events
val onMinimize = Event0();
@@ -335,9 +345,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 +368,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 +384,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 +441,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 +697,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 +705,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);
};
@@ -700,6 +721,17 @@ class VideoDetailView : ConstraintLayout {
fragment.activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
};
StatePlayer.instance.autoplayChanged.subscribe(this) {
if (it) {
val url = _url
val autoPlayVideo = _autoplayVideo
if (url != null && autoPlayVideo == null) {
_taskLoadRecommendations.cancel()
_taskLoadRecommendations.run(url)
}
}
}
_layoutResume.setOnClickListener {
handleSeek(_historicalPosition * 1000);
@@ -986,6 +1018,7 @@ class VideoDetailView : ConstraintLayout {
_container_content_queue.cleanup();
_container_content_description.cleanup();
_container_content_support.cleanup();
StatePlayer.instance.autoplayChanged.remove(this)
StateCasting.instance.onActiveDevicePlayChanged.remove(this);
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
@@ -1023,7 +1056,6 @@ class VideoDetailView : ConstraintLayout {
setDescription("".fixHtmlWhitespace());
_descriptionContainer.visibility = View.GONE;
_player.clear();
_textComments.visibility = View.INVISIBLE;
_commentsList.clear();
_lastVideoSource = null;
@@ -1047,7 +1079,7 @@ class VideoDetailView : ConstraintLayout {
setLastPositionMilliseconds(_videoResumePositionMilliseconds, false);
_addCommentView.setContext(null, null);
_toggleCommentType.setValue(false, false);
setTabIndex(0)
_commentsList.clear();
setEmpty();
@@ -1083,16 +1115,17 @@ class VideoDetailView : ConstraintLayout {
this.video = null;
cleanupPlaybackTracker();
_searchVideo = video;
_autoplayVideo = null
Logger.i(TAG, "Autoplay video cleared (setVideoOverview)")
_videoResumePositionMilliseconds = resumeSeconds * 1000;
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;
@@ -1173,6 +1206,8 @@ class VideoDetailView : ConstraintLayout {
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
_didTriggerDatasourceErrroCount = 0;
_didTriggerDatasourceError = false;
_autoplayVideo = null
Logger.i(TAG, "Autoplay video cleared (setVideoDetails)")
if(newVideo && this.video?.url == videoDetail.url)
return;
@@ -1277,13 +1312,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 && !Settings.instance.comments.hideRecommendations) {
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 +1351,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 +1511,7 @@ class VideoDetailView : ConstraintLayout {
if(video.isLive && video.live == null && !video.video.videoSources.any())
startLiveTry(video);
_player.updateNextPrevious();
updateMoreButtons();
@@ -1477,13 +1520,18 @@ 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
}
if (StatePlayer.instance.autoplay) {
_taskLoadRecommendations.cancel()
_taskLoadRecommendations.run(videoDetail.url)
}
}
fun loadLiveChat(video: IPlatformVideoDetails) {
@@ -1594,7 +1642,6 @@ class VideoDetailView : ConstraintLayout {
});
else
_player.setArtwork(null);
_player.setSource(videoSource, audioSource, _playWhenReady, false);
if(subtitleSource != null)
_player.swapSubtitles(fragment.lifecycleScope, subtitleSource);
@@ -1754,6 +1801,14 @@ class VideoDetailView : ConstraintLayout {
fun nextVideo(forceLoop: Boolean = false, withoutRemoval: Boolean = false, bypassVideoLoop: Boolean = false): Boolean {
Logger.i(TAG, "nextVideo")
var next = StatePlayer.instance.nextQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9, bypassVideoLoop);
val autoplayVideo = _autoplayVideo
if (next == null && autoplayVideo != null && StatePlayer.instance.autoplay) {
Logger.i(TAG, "Found autoplay video!")
StatePlayer.instance.setAutoplayed(autoplayVideo.url)
next = autoplayVideo
}
_autoplayVideo = null
Logger.i(TAG, "Autoplay video cleared (nextVideo)")
if(next == null && forceLoop)
next = StatePlayer.instance.restartQueue();
if(next != null) {
@@ -2271,24 +2326,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;
val recommendationsHidden = Settings.instance.comments.hideRecommendations
_buttonRecommended.visibility = if (recommendationsHidden) View.GONE else View.VISIBLE
if (reloadComments) {
fetchPolycentricComments()
}
_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 (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.GONE
_layoutRecommended.visibility = View.GONE
fetchComments()
} else if (index == 2) {
_addCommentView.visibility = View.GONE
_layoutRecommended.visibility = View.VISIBLE
_commentsList.clear()
_layoutRecommended.addView(LoaderView(context).apply {
layoutParams = LinearLayout.LayoutParams(60.dp(resources), 60.dp(resources))
start()
})
_taskLoadRecommendations.run(null)
}
}
private fun setRecommendations(results: List<IPlatformVideo>?, message: String? = null) {
if (results != null && StatePlayer.instance.autoplay) {
_autoplayVideo = results.firstOrNull { !StatePlayer.instance.wasAutoplayed(it.url) }
Logger.i(TAG, "Autoplay video set (url = ${_autoplayVideo?.url})")
}
if (_tabIndex == 2) {
_layoutRecommended.removeAllViews()
if (results == null || results.isEmpty()) {
_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
}
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 +2758,21 @@ class VideoDetailView : ConstraintLayout {
}
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
private val _taskLoadRecommendations = TaskHandler<String?, IPager<IPlatformContent>?>(StateApp.instance.scopeGetter, {
video?.let { v ->
if (v is VideoLocal) {
StatePlatform.instance.getContentRecommendations(v.url)
} else {
video?.getContentRecommendations(StatePlatform.instance.getContentClient(v.url))
}
}
})
.success { setRecommendations(it?.getResults()?.filter { it is IPlatformVideo }?.map { it as IPlatformVideo }, "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(";");
@@ -183,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;
@@ -199,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();
@@ -214,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)
@@ -379,6 +390,7 @@ class MediaPlaybackService : Service() {
Log.i(TAG, "Audio focus request result $result");
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
_hasFocus = true
_isTransientLoss = false
Log.i(TAG, "Audio focus received");
} else if (result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
_hasFocus = false
@@ -411,16 +423,17 @@ class MediaPlaybackService : Service() {
AudioManager.AUDIOFOCUS_GAIN -> {
_hasFocus = true;
_isTransientLoss = false;
Log.i(TAG, "Audio focus gained (restartPlaybackAfterLoss = ${Settings.instance.playback.restartPlaybackAfterLoss}, _audioFocusLossTime_ms = $_audioFocusLossTime_ms)");
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) {
@@ -428,8 +441,10 @@ class MediaPlaybackService : Service() {
}
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
if (isPlaying) {
_audioFocusLossTime_ms = System.currentTimeMillis()
_audioFocusLossTime_ms = if (isPlaying) {
System.currentTimeMillis()
} else {
null
}
_hasFocus = false;
@@ -443,8 +458,10 @@ class MediaPlaybackService : Service() {
_isTransientLoss = true;
}
AudioManager.AUDIOFOCUS_LOSS -> {
if (isPlaying) {
_audioFocusLossTime_ms = System.currentTimeMillis()
_audioFocusLossTime_ms = if (isPlaying) {
System.currentTimeMillis()
} else {
null
}
MediaControlReceiver.onPauseReceived.emit();
@@ -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()
@@ -8,6 +8,7 @@ import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.upstream.DefaultAllocator
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
@@ -45,6 +46,33 @@ class StatePlayer {
onRotationLockChanged.emit(value)
}
val onRotationLockChanged = Event1<Boolean>()
var autoplay: Boolean = Settings.instance.playback.autoplay
get() = field
set(value) {
if (field != value)
_autoplayed.clear()
field = value
autoplayChanged.emit(value)
}
private val _autoplayed = hashSetOf<String>()
fun wasAutoplayed(url: String?): Boolean {
if (url == null) {
return false
}
synchronized(_autoplayed) {
return _autoplayed.contains(url)
}
}
fun setAutoplayed(url: String?) {
if (url == null) {
return
}
synchronized(_autoplayed) {
_autoplayed.add(url)
}
}
val autoplayChanged = Event1<Boolean>()
var loopVideo : Boolean = false;
val isPlaying: Boolean get() = _exoplayer?.player?.playWhenReady ?: false;
@@ -138,6 +166,12 @@ class StatePlayer {
}
}
fun isUrlInQueue(url : String) : Boolean {
synchronized(_queue) {
return _queue.any { it.url == url };
}
}
fun getQueueType() : String {
return _queueType;
}
@@ -61,8 +61,17 @@ class StatePlaylists {
}
fun updateWatchLater(updated: List<SerializedPlatformVideo>) {
synchronized(_watchlistStore) {
_watchlistStore.deleteAll();
_watchlistStore.saveAllAsync(updated);
//_watchlistStore.deleteAll();
val existing = _watchlistStore.getItems();
val toAdd = updated.filter { u -> !existing.any { u.url == it.url } };
val toRemove = existing.filter { u -> !updated.any { u.url == it.url } };
Logger.i(TAG, "WatchLater changed:\nTo Add:\n" +
(if(toAdd.size == 0) "None" else toAdd.map { " + " + it.name }.joinToString("\n")) +
"\nTo Remove:\n" +
(if(toRemove.size == 0) "None" else toRemove.map { " - " + it.name }.joinToString("\n")));
for(remove in toRemove)
_watchlistStore.delete(remove);
_watchlistStore.saveAllAsync(toAdd);
_watchlistOrderStore.set(*updated.map { it.url }.toTypedArray());
_watchlistOrderStore.save();
}
@@ -10,6 +10,8 @@ import com.futo.platformplayer.api.media.platforms.js.SourceAuth
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment.Companion
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.stores.FragmentedStorage
@@ -128,7 +130,15 @@ class StatePlugins {
return false;
LoginActivity.showLogin(context, config) {
StatePlugins.instance.setPluginAuth(config.id, it);
try {
StatePlugins.instance.setPluginAuth(config.id, it);
} catch (e: Throwable) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
UIDialogs.showGeneralErrorDialog(context, "Failed to set plugin authentication (loginPlugin)", e)
}
Logger.e(SourceDetailFragment.TAG, "Failed to set plugin authentication (loginPlugin)", e)
return@showLogin
}
StateApp.instance.scope.launch(Dispatchers.IO) {
StatePlatform.instance.reloadClient(context, id);
@@ -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);
});
@@ -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();
@@ -18,6 +18,7 @@ import android.widget.ImageButton
import android.widget.TextView
import androidx.annotation.OptIn
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.setMargins
import androidx.media3.common.C
import androidx.media3.common.PlaybackParameters
@@ -74,6 +75,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
//Custom buttons
private val _control_fullscreen: ImageButton;
private val _control_autoplay: ImageButton;
private val _control_videosettings: ImageButton;
private val _control_minimize: ImageButton;
private val _control_rotate_lock: ImageButton;
@@ -92,6 +94,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
private val _control_videosettings_fullscreen: ImageButton;
private val _control_minimize_fullscreen: ImageButton;
private val _control_rotate_lock_fullscreen: ImageButton;
private val _control_autoplay_fullscreen: ImageButton;
private val _control_loop_fullscreen: ImageButton;
private val _control_cast_fullscreen: ImageButton;
private val _control_play_fullscreen: ImageButton;
@@ -149,6 +152,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
videoControls = findViewById(R.id.video_player_controller);
_control_fullscreen = videoControls.findViewById(R.id.button_fullscreen);
_control_autoplay = videoControls.findViewById(R.id.button_autoplay);
_control_videosettings = videoControls.findViewById(R.id.button_settings);
_control_minimize = videoControls.findViewById(R.id.button_minimize);
_control_rotate_lock = videoControls.findViewById(R.id.button_rotate_lock);
@@ -164,6 +168,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_control_duration = videoControls.findViewById(R.id.text_duration);
_videoControls_fullscreen = findViewById(R.id.video_player_controller_fullscreen);
_control_autoplay_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_autoplay);
_control_fullscreen_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_fullscreen);
_control_minimize_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_minimize);
_control_videosettings_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_settings);
@@ -386,6 +391,18 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
UIDialogs.showCastingDialog(context);
};
_control_autoplay.setOnClickListener {
StatePlayer.instance.autoplay = !StatePlayer.instance.autoplay;
updateAutoplayButton()
}
updateAutoplayButton()
_control_autoplay_fullscreen.setOnClickListener {
StatePlayer.instance.autoplay = !StatePlayer.instance.autoplay;
updateAutoplayButton()
}
updateAutoplayButton()
val progressUpdateListener = { position: Long, bufferedPosition: Long ->
val currentTime = position.formatDuration()
val currentDuration = duration.formatDuration()
@@ -433,6 +450,11 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
}
private fun updateAutoplayButton() {
_control_autoplay.setColorFilter(ContextCompat.getColor(context, if (StatePlayer.instance.autoplay) com.futo.futopay.R.color.primary else R.color.white))
_control_autoplay_fullscreen.setColorFilter(ContextCompat.getColor(context, if (StatePlayer.instance.autoplay) com.futo.futopay.R.color.primary else R.color.white))
}
private fun setSystemBrightness(brightness: Float) {
Log.i(TAG, "setSystemBrightness $brightness")
if (android.provider.Settings.System.canWrite(context)) {
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M400,623.08L400,336.92L623.08,480L400,623.08ZM480,880Q363.54,880 269.42,820.12Q175.31,760.23 120,651.23L120,800L80,800L80,584.62L294.62,584.62L294.62,624.62L152,624.62Q198.38,724.23 285.73,782.12Q373.08,840 480,840Q598.85,840 693.5,769Q788.15,698 823.08,584.38L862.62,592.38Q825.31,721.46 719.54,800.73Q613.77,880 480,880ZM82,440Q89.77,377.62 110.15,328.04Q130.54,278.46 169.92,227.23L199.23,255Q167.23,296.77 149.54,339.04Q131.85,381.31 122.23,440L82,440ZM255.23,199.77L227.46,170.46Q275.85,132.62 329.54,110.58Q383.23,88.54 440,83.54L440,123.54Q391.31,128.54 344.15,148.15Q297,167.77 255.23,199.77ZM703.46,199.77Q667.08,169.31 616.73,148.54Q566.38,127.77 520,123.54L520,83.54Q577,88.77 630.81,111.08Q684.62,133.38 732,171.23L703.46,199.77ZM836.46,440Q829.92,384.38 810.31,338.65Q790.69,292.92 758.69,255.77L787.23,227.23Q825.85,273.08 848.15,326.88Q870.46,380.69 876.46,440L836.46,440Z"/>
</vector>
@@ -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
@@ -112,7 +112,8 @@
app:scrubber_drawable="@drawable/player_thumb"
app:bar_height="2dp"
app:scrubber_disabled_size="0dp"
app:scrubber_enabled_size="12dp"
app:scrubber_enabled_size="16dp"
app:scrubber_dragged_size="20dp"
app:played_color="@color/colorPrimary"
app:buffered_color="#DDEEEEEE"
app:unplayed_color="#55EEEEEE" />
+10 -1
View File
@@ -29,6 +29,14 @@
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent">
<ImageButton
android:id="@+id/button_autoplay"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
app:srcCompat="@drawable/autoplay_24px" />
<ImageButton
android:id="@+id/button_cast"
android:layout_width="50dp"
@@ -205,7 +213,8 @@
app:bar_height="2dp"
app:scrubber_drawable="@drawable/player_thumb"
app:scrubber_disabled_size="0dp"
app:scrubber_enabled_size="12dp"
app:scrubber_enabled_size="16dp"
app:scrubber_dragged_size="20dp"
app:played_color="@color/transparent"
app:buffered_color="@color/transparent"
app:unplayed_color="@color/transparent"
@@ -57,6 +57,14 @@
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent">
<ImageButton
android:id="@+id/button_autoplay"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
app:srcCompat="@drawable/autoplay_24px" />
<ImageButton
android:id="@+id/button_cast"
android:layout_width="50dp"
@@ -237,7 +245,8 @@
app:scrubber_drawable="@drawable/player_thumb"
app:bar_height="2dp"
app:scrubber_disabled_size="0dp"
app:scrubber_enabled_size="12dp"
app:scrubber_enabled_size="16dp"
app:scrubber_dragged_size="20dp"
app:played_color="@color/colorPrimary"
app:buffered_color="#AAEEEEEE"
app:unplayed_color="#88EEEEEE"
+2 -1
View File
@@ -178,7 +178,8 @@
app:bar_height="2dp"
app:scrubber_drawable="@drawable/player_thumb"
app:scrubber_disabled_size="0dp"
app:scrubber_enabled_size="12dp"
app:scrubber_enabled_size="16dp"
app:scrubber_dragged_size="20dp"
app:played_color="@color/colorPrimary"
app:buffered_color="@color/transparent"
app:unplayed_color="#7F7F7F"
+28
View File
@@ -373,12 +373,22 @@
<string name="system_volume_descr">Gesture controls adjust system volume</string>
<string name="live_chat_webview">Live Chat Webview</string>
<string name="full_screen_portrait">Fullscreen portrait</string>
<string name="reverse_portrait">Allow reverse portrait</string>
<string name="reverse_portrait_description">Allow app to flip into reverse portrait</string>
<string name="rotation_zone">Rotation zone</string>
<string name="rotation_zone_description">Specify the sensitivity of rotation zones (decrease to make less sensitive)</string>
<string name="stability_threshold_time">Stability threshold time</string>
<string name="stability_threshold_time_description">Specify the duration the orientation needs to be the same to trigger a rotation</string>
<string name="prefer_webm">Prefer Webm Video Codecs</string>
<string name="prefer_webm_description">If player should prefer Webm codecs (vp9/opus) over mp4 codecs (h264/AAC), may result in worse compatibility.</string>
<string name="full_autorotate_lock">Full auto rotate lock</string>
<string name="full_autorotate_lock_description">Prevent any rotation while rotation lock is engaged (even flipping between landscape and landscape reverse).</string>
<string name="prefer_webm_audio">Prefer Webm Audio Codecs</string>
<string name="prefer_webm_audio_description">If player should prefer Webm codecs (opus) over mp4 codecs (AAC), may result in worse compatibility.</string>
<string name="allow_under_cutout">Allow video under cutout</string>
<string name="allow_under_cutout_description">Allow video to go underneath the screen cutout in full-screen.\nMay require restart</string>
<string name="autoplay">Enable autoplay by default</string>
<string name="autoplay_description">Autoplay will be enabled by default whenever you watch a video</string>
<string name="allow_full_screen_portrait">Allow fullscreen portrait</string>
<string name="background_switch_audio">Switch to Audio in Background</string>
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>
@@ -412,6 +422,10 @@
<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="hide_recommendations">Hide Recommendations</string>
<string name="hide_recommendations_description">Fully hide the recommendations tab.</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 +928,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>
@@ -950,4 +965,17 @@
<item>Within 30 seconds of loss</item>
<item>Always</item>
</string-array>
<string-array name="rotation_zone">
<item>15</item>
<item>30</item>
<item>45</item>
</string-array>
<string-array name="rotation_threshold_time">
<item>100</item>
<item>500</item>
<item>750</item>
<item>1000</item>
<item>1500</item>
<item>2000</item>
</string-array>
</resources>
@@ -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() {