mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a41b89e52 | |||
| 0a0c16524a | |||
| 9b843a155e | |||
| cb085acbff | |||
| c3d7df166b | |||
| d312062125 | |||
| e2453192aa | |||
| 0f4e4a7d97 | |||
| f20a708b36 | |||
| 8c4e511883 | |||
| a4a3b8d664 | |||
| bf6530ea81 | |||
| 4a80c2aab1 | |||
| 527bbfe43f | |||
| d8e1edb60b | |||
| 245b5f74c0 | |||
| e9a1f63415 | |||
| ec370dd94b | |||
| e39d862ef3 | |||
| 7b065654aa | |||
| 918b2bbe96 | |||
| e529a3d34d | |||
| 5475778d67 | |||
| c6a3ff0a53 | |||
| cf3587f504 | |||
| d42f104884 | |||
| 6a43568369 | |||
| 85c9cd0a6e | |||
| be5920cfae | |||
| 3d25d94a77 | |||
| fe97850835 | |||
| dab9decd89 | |||
| 854651aa71 | |||
| fdd1af3287 | |||
| 0bf92b6aff | |||
| d9403bf4da | |||
| 716d8caf4d | |||
| 0f0f368a75 | |||
| ff8d7558d4 | |||
| 66f9824b68 | |||
| 44a6e5da38 | |||
| de5a4aa5f3 | |||
| e8007082a7 | |||
| 3c70c5a366 | |||
| eb6e79b055 | |||
| ea59f8dccb | |||
| aef1c584e5 | |||
| c4ce671a87 | |||
| e8a79c87ab | |||
| 249e77a5d3 | |||
| 3cf4a52a69 | |||
| eb8b02756b | |||
| 0510d34ed3 | |||
| 1c8d12e72a | |||
| 0a36a6b674 | |||
| b887c9d50f | |||
| ee4e108e4f | |||
| 5e14a0fed4 | |||
| 6045205ea9 | |||
| f2d763cdec | |||
| e5e348205a |
@@ -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) {
|
||||
@@ -795,3 +832,99 @@ class URLSearchParams {
|
||||
return searchString;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var __REGEX_SPACE_CHARACTERS = /<%= spaceCharacters %>/g;
|
||||
var __btoa_TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
function btoa(input) {
|
||||
input = String(input);
|
||||
if (/[^\0-\xFF]/.test(input)) {
|
||||
// Note: no need to special-case astral symbols here, as surrogates are
|
||||
// matched, and the input is supposed to only contain ASCII anyway.
|
||||
error(
|
||||
'The string to be encoded contains characters outside of the ' +
|
||||
'Latin1 range.'
|
||||
);
|
||||
}
|
||||
var padding = input.length % 3;
|
||||
var output = '';
|
||||
var position = -1;
|
||||
var a;
|
||||
var b;
|
||||
var c;
|
||||
var buffer;
|
||||
// Make sure any padding is handled outside of the loop.
|
||||
var length = input.length - padding;
|
||||
|
||||
while (++position < length) {
|
||||
// Read three bytes, i.e. 24 bits.
|
||||
a = input.charCodeAt(position) << 16;
|
||||
b = input.charCodeAt(++position) << 8;
|
||||
c = input.charCodeAt(++position);
|
||||
buffer = a + b + c;
|
||||
// Turn the 24 bits into four chunks of 6 bits each, and append the
|
||||
// matching character for each of them to the output.
|
||||
output += (
|
||||
__btoa_TABLE.charAt(buffer >> 18 & 0x3F) +
|
||||
__btoa_TABLE.charAt(buffer >> 12 & 0x3F) +
|
||||
__btoa_TABLE.charAt(buffer >> 6 & 0x3F) +
|
||||
__btoa_TABLE.charAt(buffer & 0x3F)
|
||||
);
|
||||
}
|
||||
|
||||
if (padding == 2) {
|
||||
a = input.charCodeAt(position) << 8;
|
||||
b = input.charCodeAt(++position);
|
||||
buffer = a + b;
|
||||
output += (
|
||||
__btoa_TABLE.charAt(buffer >> 10) +
|
||||
__btoa_TABLE.charAt((buffer >> 4) & 0x3F) +
|
||||
__btoa_TABLE.charAt((buffer << 2) & 0x3F) +
|
||||
'='
|
||||
);
|
||||
} else if (padding == 1) {
|
||||
buffer = input.charCodeAt(position);
|
||||
output += (
|
||||
__btoa_TABLE.charAt(buffer >> 2) +
|
||||
__btoa_TABLE.charAt((buffer << 4) & 0x3F) +
|
||||
'=='
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
function atob(input) {
|
||||
input = String(input)
|
||||
.replace(__REGEX_SPACE_CHARACTERS, '');
|
||||
var length = input.length;
|
||||
if (length % 4 == 0) {
|
||||
input = input.replace(/==?$/, '');
|
||||
length = input.length;
|
||||
}
|
||||
if (
|
||||
length % 4 == 1 ||
|
||||
// http://whatwg.org/C#alphanumeric-ascii-characters
|
||||
/[^+a-zA-Z0-9/]/.test(input)
|
||||
) {
|
||||
error(
|
||||
'Invalid character: the string to be decoded is not correctly encoded.'
|
||||
);
|
||||
}
|
||||
var bitCounter = 0;
|
||||
var bitStorage;
|
||||
var buffer;
|
||||
var output = '';
|
||||
var position = -1;
|
||||
while (++position < length) {
|
||||
buffer = __btoa_TABLE.indexOf(input.charAt(position));
|
||||
bitStorage = bitCounter % 4 ? bitStorage * 64 + buffer : buffer;
|
||||
// Unless this is the first of a group of 4 characters…
|
||||
if (bitCounter++ % 4) {
|
||||
// …convert the first 8 bits to a single ASCII character.
|
||||
output += String.fromCharCode(
|
||||
0xFF & bitStorage >> (-2 * bitCounter & 6)
|
||||
);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
@@ -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,27 +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)
|
||||
@FormFieldWarning(R.string.changing_this_field_requires_restart)
|
||||
@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)
|
||||
@@ -540,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 {
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
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.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||
import com.futo.platformplayer.downloads.VideoLocal
|
||||
@@ -879,6 +880,12 @@ class UISlideOverlays {
|
||||
val items = arrayListOf<View>();
|
||||
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
|
||||
|
||||
val isLimited = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
|
||||
if (it is JSClient)
|
||||
return@let it.config.reduceFunctionsInLimitedVersion && BuildConfig.IS_PLAYSTORE_BUILD
|
||||
else false;
|
||||
} ?: false;
|
||||
|
||||
if (lastUpdated != null) {
|
||||
items.add(
|
||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
|
||||
@@ -899,17 +906,18 @@ class UISlideOverlays {
|
||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||
(listOf(
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_download,
|
||||
container.context.getString(R.string.download),
|
||||
container.context.getString(R.string.download_the_video),
|
||||
tag = "download",
|
||||
call = {
|
||||
showDownloadVideoOverlay(video, container, true);
|
||||
},
|
||||
invokeParent = false
|
||||
),
|
||||
if(!isLimited)
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_download,
|
||||
container.context.getString(R.string.download),
|
||||
container.context.getString(R.string.download_the_video),
|
||||
tag = "download",
|
||||
call = {
|
||||
showDownloadVideoOverlay(video, container, true);
|
||||
},
|
||||
invokeParent = false
|
||||
) else null,
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_share,
|
||||
@@ -936,7 +944,7 @@ class UISlideOverlays {
|
||||
StateMeta.instance.addHiddenCreator(video.author.url);
|
||||
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
||||
}))
|
||||
+ actions)
|
||||
+ actions).filterNotNull()
|
||||
));
|
||||
items.add(
|
||||
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
|
||||
@@ -1033,15 +1041,7 @@ class UISlideOverlays {
|
||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||
tag = "watch later",
|
||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_download,
|
||||
container.context.getString(R.string.download),
|
||||
container.context.getString(R.string.download_the_video),
|
||||
tag = container.context.getString(R.string.download),
|
||||
call = { showDownloadVideoOverlay(video, container, true); },
|
||||
invokeParent = false
|
||||
))
|
||||
)
|
||||
);
|
||||
|
||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -7,12 +7,14 @@ import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Bundle
|
||||
import android.os.StrictMode
|
||||
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
|
||||
@@ -252,7 +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);
|
||||
@@ -514,7 +517,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+2
-1
@@ -50,7 +50,8 @@ class SourcePluginConfig(
|
||||
var primaryClaimFieldType: Int? = null,
|
||||
var developerSubmitUrl: String? = null,
|
||||
var allowAllHttpHeaderAccess: Boolean = false,
|
||||
var maxDownloadParallelism: Int = 0
|
||||
var maxDownloadParallelism: Int = 0,
|
||||
var reduceFunctionsInLimitedVersion: Boolean = false,
|
||||
) : IV8PluginConfig {
|
||||
|
||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ interface IJSContentDetails: IPlatformContent {
|
||||
return when(ContentType.fromInt(type)) {
|
||||
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
|
||||
ContentType.POST -> JSPostDetails(plugin.config, obj);
|
||||
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
|
||||
else -> throw NotImplementedError("Unknown content type ${type}");
|
||||
}
|
||||
}
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js.models
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.IPluginSourced
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.post.TextType
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullableList
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
open class JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails {
|
||||
final override val contentType: ContentType get() = ContentType.ARTICLE;
|
||||
|
||||
private val _hasGetComments: Boolean;
|
||||
private val _hasGetContentRecommendations: Boolean;
|
||||
|
||||
val rating: IRating;
|
||||
|
||||
val summary: String;
|
||||
val thumbnails: Thumbnails?;
|
||||
val segments: List<IJSArticleSegment>;
|
||||
|
||||
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
|
||||
val contextName = "PlatformPost";
|
||||
|
||||
rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
|
||||
summary = _content.getOrThrow(client.config, "summary", contextName);
|
||||
if(_content.has("thumbnails"))
|
||||
thumbnails = Thumbnails.fromV8(client.config, _content.getOrThrow(client.config, "thumbnails", contextName));
|
||||
else
|
||||
thumbnails = null;
|
||||
|
||||
|
||||
segments = (obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", contextName)
|
||||
?.map { fromV8Segment(client, it) }
|
||||
?.filterNotNull() ?: listOf());
|
||||
|
||||
_hasGetComments = _content.has("getComments");
|
||||
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
||||
}
|
||||
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||
if(!_hasGetComments || _content.isClosed)
|
||||
return null;
|
||||
|
||||
if(client is DevJSClient)
|
||||
return StateDeveloper.instance.handleDevCall(client.devID, "videoDetail.getComments()") {
|
||||
return@handleDevCall getCommentsJS(client);
|
||||
}
|
||||
else if(client is JSClient)
|
||||
return getCommentsJS(client);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||
|
||||
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
|
||||
if(!_hasGetContentRecommendations || _content.isClosed)
|
||||
return null;
|
||||
|
||||
if(client is DevJSClient)
|
||||
return StateDeveloper.instance.handleDevCall(client.devID, "postDetail.getContentRecommendations()") {
|
||||
return@handleDevCall getContentRecommendationsJS(client);
|
||||
}
|
||||
else if(client is JSClient)
|
||||
return getContentRecommendationsJS(client);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
|
||||
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
|
||||
return JSContentPager(_pluginConfig, client, contentPager);
|
||||
}
|
||||
|
||||
private fun getCommentsJS(client: JSClient): JSCommentPager {
|
||||
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
|
||||
return JSCommentPager(_pluginConfig, client, commentPager);
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromV8Segment(client: JSClient, obj: V8ValueObject): IJSArticleSegment? {
|
||||
if(!obj.has("type"))
|
||||
throw IllegalArgumentException("Object missing type field");
|
||||
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
|
||||
SegmentType.TEXT -> JSTextSegment(client, obj);
|
||||
SegmentType.IMAGES -> JSImagesSegment(client, obj);
|
||||
SegmentType.NESTED -> JSNestedSegment(client, obj);
|
||||
else -> null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SegmentType(val value: Int) {
|
||||
UNKNOWN(0),
|
||||
TEXT(1),
|
||||
IMAGES(2),
|
||||
|
||||
NESTED(9);
|
||||
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int): SegmentType
|
||||
{
|
||||
val result = SegmentType.entries.firstOrNull { it.value == value };
|
||||
if(result == null)
|
||||
throw IllegalArgumentException("Unknown Texttype: $value");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IJSArticleSegment {
|
||||
val type: SegmentType;
|
||||
}
|
||||
class JSTextSegment: IJSArticleSegment {
|
||||
override val type = SegmentType.TEXT;
|
||||
val textType: TextType;
|
||||
val content: String;
|
||||
|
||||
constructor(client: JSClient, obj: V8ValueObject) {
|
||||
val contextName = "JSTextSegment";
|
||||
textType = TextType.fromInt((obj.getOrDefault<Int>(client.config, "textType", contextName, null) ?: 0));
|
||||
content = obj.getOrDefault(client.config, "content", contextName, "") ?: "";
|
||||
}
|
||||
}
|
||||
class JSImagesSegment: IJSArticleSegment {
|
||||
override val type = SegmentType.IMAGES;
|
||||
val images: List<String>;
|
||||
val caption: String;
|
||||
|
||||
constructor(client: JSClient, obj: V8ValueObject) {
|
||||
val contextName = "JSTextSegment";
|
||||
images = obj.getOrThrowNullableList<String>(client.config, "images", contextName) ?: listOf();
|
||||
caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: "";
|
||||
}
|
||||
}
|
||||
class JSNestedSegment: IJSArticleSegment {
|
||||
override val type = SegmentType.NESTED;
|
||||
val nested: IPlatformContent;
|
||||
|
||||
constructor(client: JSClient, obj: V8ValueObject) {
|
||||
val contextName = "JSNestedSegment";
|
||||
val nestedObj = obj.getOrThrow<V8ValueObject>(client.config, "nested", contextName, false);
|
||||
nested = IJSContent.fromV8(client, nestedObj);
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -42,7 +42,12 @@ open class JSContent : IPlatformContent, IPluginSourced {
|
||||
|
||||
id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
|
||||
name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
|
||||
author = PlatformAuthorLink.fromV8(_pluginConfig, _content.getOrThrow(config, "author", contextName));
|
||||
|
||||
val authorObj = _content.getOrDefault<V8ValueObject>(config, "author", contextName, null);
|
||||
if(authorObj != null)
|
||||
author = PlatformAuthorLink.fromV8(_pluginConfig, authorObj);
|
||||
else
|
||||
author = PlatformAuthorLink.UNKNOWN;
|
||||
|
||||
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
|
||||
if(datetimeInt == 0.toLong())
|
||||
|
||||
@@ -68,6 +68,10 @@ class PackageDOMParser : V8Package {
|
||||
return result;
|
||||
}
|
||||
@V8Property
|
||||
fun parentElement(): DOMNode? {
|
||||
return parentNode();
|
||||
}
|
||||
@V8Property
|
||||
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
|
||||
@V8Property
|
||||
fun innerHTML(): String = _element.html();
|
||||
@@ -76,6 +80,8 @@ class PackageDOMParser : V8Package {
|
||||
@V8Property
|
||||
fun textContent(): String = _element.text();
|
||||
@V8Property
|
||||
fun tagName(): String = _element.tagName().uppercase();
|
||||
@V8Property
|
||||
fun text(): String = _element.text().ifEmpty { data() };
|
||||
@V8Property
|
||||
fun data(): String = _element.data();
|
||||
|
||||
@@ -99,6 +99,8 @@ class PackageHttp: V8Package {
|
||||
|
||||
if(body is V8ValueString)
|
||||
return client.POST(url, body.value, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||
else if(body is String)
|
||||
return client.POST(url, body, headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||
else if(body is V8ValueTypedArray)
|
||||
return client.POST(url, body.toBytes(), headers, if(useByteResponse) ReturnType.BYTES else ReturnType.STRING);
|
||||
else if(body is ByteArray)
|
||||
|
||||
+28
-16
@@ -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
|
||||
|
||||
+26
-8
@@ -46,6 +46,14 @@ class HomeFragment : MainFragment() {
|
||||
private var _view: HomeView? = null;
|
||||
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
||||
|
||||
fun reloadFeed() {
|
||||
_view?.reloadFeed()
|
||||
}
|
||||
|
||||
fun scrollToTop(smooth: Boolean) {
|
||||
_view?.scrollToTop(smooth)
|
||||
}
|
||||
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
super.onShownWithView(parameter, isBack);
|
||||
_view?.onShown();
|
||||
@@ -138,17 +146,12 @@ class HomeFragment : MainFragment() {
|
||||
fun onShown() {
|
||||
val lastClients = recyclerData.lastClients;
|
||||
val clients = StatePlatform.instance.getSortedEnabledClient().filter { if (it is JSClient) it.enableInHome else true };
|
||||
|
||||
val feedstyleChanged = recyclerData.loadedFeedStyle != feedStyle;
|
||||
val clientsChanged = lastClients == null || lastClients.size != clients.size || !lastClients.containsAll(clients);
|
||||
val outdated = recyclerData.lastLoad.getNowDiffSeconds() > 60;
|
||||
Logger.i(TAG, "onShown (recyclerData.loadedFeedStyle=${recyclerData.loadedFeedStyle}, recyclerData.lastLoad=${recyclerData.lastLoad}, feedstyleChanged=$feedstyleChanged, clientsChanged=$clientsChanged, outdated=$outdated)")
|
||||
Logger.i(TAG, "onShown (recyclerData.loadedFeedStyle=${recyclerData.loadedFeedStyle}, recyclerData.lastLoad=${recyclerData.lastLoad}, feedstyleChanged=$feedstyleChanged, clientsChanged=$clientsChanged)")
|
||||
|
||||
if(feedstyleChanged || outdated || clientsChanged) {
|
||||
recyclerData.lastLoad = OffsetDateTime.now();
|
||||
recyclerData.loadedFeedStyle = feedStyle;
|
||||
recyclerData.lastClients = clients;
|
||||
loadResults();
|
||||
if(feedstyleChanged || clientsChanged) {
|
||||
reloadFeed()
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -156,6 +159,21 @@ class HomeFragment : MainFragment() {
|
||||
finishRefreshLayoutLoader();
|
||||
}
|
||||
|
||||
fun scrollToTop(smooth: Boolean) {
|
||||
if (smooth) {
|
||||
_recyclerResults.smoothScrollToPosition(0)
|
||||
} else {
|
||||
_recyclerResults.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
||||
fun reloadFeed() {
|
||||
recyclerData.lastLoad = OffsetDateTime.now();
|
||||
recyclerData.loadedFeedStyle = feedStyle;
|
||||
recyclerData.lastClients = StatePlatform.instance.getSortedEnabledClient().filter { if (it is JSClient) it.enableInHome else true };
|
||||
loadResults();
|
||||
}
|
||||
|
||||
override fun getEmptyPagerView(): View? {
|
||||
val dp10 = 10.dp(resources);
|
||||
val dp30 = 30.dp(resources);
|
||||
|
||||
+28
-6
@@ -156,6 +156,14 @@ class PlaylistFragment : MainFragment() {
|
||||
};
|
||||
}
|
||||
|
||||
private fun copyPlaylist(playlist: Playlist) {
|
||||
StatePlaylists.instance.playlistStore.save(playlist)
|
||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(
|
||||
arrayListOf()
|
||||
)
|
||||
UIDialogs.toast("Playlist saved")
|
||||
}
|
||||
|
||||
fun onShown(parameter: Any?) {
|
||||
_taskLoadPlaylist.cancel()
|
||||
|
||||
@@ -170,14 +178,10 @@ class PlaylistFragment : MainFragment() {
|
||||
setButtonDownloadVisible(true)
|
||||
setButtonEditVisible(true)
|
||||
|
||||
if (!StatePlaylists.instance.playlistStore.getItems().contains(parameter)) {
|
||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
|
||||
_fragment.topBar?.assume<NavigationTopBarFragment>()
|
||||
?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
|
||||
StatePlaylists.instance.playlistStore.save(parameter)
|
||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(
|
||||
arrayListOf()
|
||||
)
|
||||
UIDialogs.toast("Playlist saved")
|
||||
copyPlaylist(parameter)
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
@@ -242,6 +246,15 @@ class PlaylistFragment : MainFragment() {
|
||||
}
|
||||
|
||||
private fun download() {
|
||||
val playlist = _playlist ?: return
|
||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
|
||||
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to download", {
|
||||
copyPlaylist(playlist)
|
||||
download()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
_playlist?.let {
|
||||
UISlideOverlays.showDownloadPlaylistOverlay(it, overlayContainer);
|
||||
}
|
||||
@@ -266,6 +279,15 @@ class PlaylistFragment : MainFragment() {
|
||||
override fun canEdit(): Boolean { return _playlist != null; }
|
||||
|
||||
override fun onEditClick() {
|
||||
val playlist = _playlist ?: return
|
||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
|
||||
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to edit the name", {
|
||||
copyPlaylist(playlist)
|
||||
onEditClick()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
_editPlaylistNameInput?.activate();
|
||||
_editPlaylistOverlay?.show();
|
||||
}
|
||||
|
||||
+46
-31
@@ -9,6 +9,7 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
@@ -19,6 +20,7 @@ import androidx.core.view.children
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
@@ -135,10 +137,7 @@ class PostDetailFragment : MainFragment {
|
||||
private val _imageDislikeIcon: ImageView;
|
||||
private val _textDislikes: TextView;
|
||||
|
||||
private val _textComments: TextView;
|
||||
private val _textCommentType: TextView;
|
||||
private val _addCommentView: AddCommentView;
|
||||
private val _toggleCommentType: Toggle;
|
||||
|
||||
private val _rating: PillRatingLikesDislikes;
|
||||
|
||||
@@ -152,6 +151,10 @@ class PostDetailFragment : MainFragment {
|
||||
|
||||
private val _commentsList: CommentsList;
|
||||
|
||||
private var _commentType: Boolean? = null;
|
||||
private val _buttonPolycentric: Button
|
||||
private val _buttonPlatform: Button
|
||||
|
||||
private val _taskLoadPost = if(!isInEditMode) TaskHandler<String, IPlatformPostDetails>(
|
||||
StateApp.instance.scopeGetter,
|
||||
{
|
||||
@@ -198,9 +201,6 @@ class PostDetailFragment : MainFragment {
|
||||
_textDislikes = findViewById(R.id.text_dislikes);
|
||||
|
||||
_commentsList = findViewById(R.id.comments_list);
|
||||
_textCommentType = findViewById(R.id.text_comment_type);
|
||||
_toggleCommentType = findViewById(R.id.toggle_comment_type);
|
||||
_textComments = findViewById(R.id.text_comments);
|
||||
_addCommentView = findViewById(R.id.add_comment_view);
|
||||
|
||||
_rating = findViewById(R.id.rating);
|
||||
@@ -213,6 +213,9 @@ class PostDetailFragment : MainFragment {
|
||||
|
||||
_repliesOverlay = findViewById(R.id.replies_overlay);
|
||||
|
||||
_buttonPolycentric = findViewById(R.id.button_polycentric)
|
||||
_buttonPlatform = findViewById(R.id.button_platform)
|
||||
|
||||
_textContent.setPlatformPlayerLinkMovementMethod(context);
|
||||
|
||||
_buttonSubscribe.onSubscribed.subscribe {
|
||||
@@ -224,9 +227,10 @@ class PostDetailFragment : MainFragment {
|
||||
root.removeView(layoutTop);
|
||||
_commentsList.setPrependedView(layoutTop);
|
||||
|
||||
/*TODO: Why is this here?
|
||||
_commentsList.onCommentsLoaded.subscribe {
|
||||
updateCommentType(false);
|
||||
};
|
||||
};*/
|
||||
|
||||
_commentsList.onRepliesClick.subscribe { c ->
|
||||
val replyCount = c.replyCount ?: 0;
|
||||
@@ -237,7 +241,7 @@ class PostDetailFragment : MainFragment {
|
||||
|
||||
if (c is PolycentricPlatformComment) {
|
||||
var parentComment: PolycentricPlatformComment = c;
|
||||
_repliesOverlay.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference, c,
|
||||
_repliesOverlay.load(_commentType!!, metadata, c.contextUrl, c.reference, c,
|
||||
{ StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) },
|
||||
{
|
||||
val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1);
|
||||
@@ -245,22 +249,23 @@ class PostDetailFragment : MainFragment {
|
||||
parentComment = newComment;
|
||||
});
|
||||
} else {
|
||||
_repliesOverlay.load(_toggleCommentType.value, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
|
||||
_repliesOverlay.load(_commentType!!, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
|
||||
}
|
||||
|
||||
setRepliesOverlayVisible(isVisible = true, animate = true);
|
||||
};
|
||||
|
||||
if (StatePolycentric.instance.enabled) {
|
||||
_buttonPolycentric.setOnClickListener {
|
||||
updateCommentType(false)
|
||||
}
|
||||
} else {
|
||||
_buttonPolycentric.visibility = View.GONE
|
||||
}
|
||||
|
||||
_toggleCommentType.onValueChanged.subscribe {
|
||||
updateCommentType(true);
|
||||
};
|
||||
|
||||
_textCommentType.setOnClickListener {
|
||||
_toggleCommentType.setValue(!_toggleCommentType.value, true);
|
||||
updateCommentType(true);
|
||||
};
|
||||
|
||||
_buttonPlatform.setOnClickListener {
|
||||
updateCommentType(true)
|
||||
}
|
||||
_layoutMonetization.visibility = View.GONE;
|
||||
|
||||
_buttonSupport.setOnClickListener {
|
||||
@@ -432,7 +437,7 @@ class PostDetailFragment : MainFragment {
|
||||
_taskLoadPolycentricProfile.cancel();
|
||||
_version++;
|
||||
|
||||
_toggleCommentType.setValue(false, false);
|
||||
updateCommentType(null)
|
||||
_url = null;
|
||||
_post = null;
|
||||
_postOverview = null;
|
||||
@@ -476,7 +481,8 @@ class PostDetailFragment : MainFragment {
|
||||
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
|
||||
}
|
||||
|
||||
updateCommentType(true);
|
||||
val commentType = !Settings.instance.other.polycentricEnabled || Settings.instance.comments.defaultCommentSection == 1
|
||||
updateCommentType(commentType, true);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -679,20 +685,29 @@ class PostDetailFragment : MainFragment {
|
||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post!!.url, ref, listOfNotNull(extraBytesRef)); };
|
||||
}
|
||||
|
||||
private fun updateCommentType(reloadComments: Boolean) {
|
||||
if (_toggleCommentType.value) {
|
||||
_textCommentType.text = "Platform";
|
||||
_addCommentView.visibility = View.GONE;
|
||||
private fun updateCommentType(commentType: Boolean?, forceReload: Boolean = false) {
|
||||
val changed = commentType != _commentType
|
||||
_commentType = commentType
|
||||
|
||||
if (reloadComments) {
|
||||
fetchComments();
|
||||
}
|
||||
if (commentType == null) {
|
||||
_buttonPlatform.setTextColor(resources.getColor(R.color.gray_ac))
|
||||
_buttonPolycentric.setTextColor(resources.getColor(R.color.gray_ac))
|
||||
} else {
|
||||
_textCommentType.text = "Polycentric";
|
||||
_addCommentView.visibility = View.VISIBLE;
|
||||
_buttonPlatform.setTextColor(resources.getColor(if (commentType) R.color.white else R.color.gray_ac))
|
||||
_buttonPolycentric.setTextColor(resources.getColor(if (!commentType) R.color.white else R.color.gray_ac))
|
||||
|
||||
if (reloadComments) {
|
||||
fetchPolycentricComments()
|
||||
if (commentType) {
|
||||
_addCommentView.visibility = View.GONE;
|
||||
|
||||
if (forceReload || changed) {
|
||||
fetchComments();
|
||||
}
|
||||
} else {
|
||||
_addCommentView.visibility = View.VISIBLE;
|
||||
|
||||
if (forceReload || changed) {
|
||||
fetchPolycentricComments()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
-7
@@ -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) {
|
||||
|
||||
+8
-2
@@ -117,8 +117,14 @@ class SuggestionsFragment : MainFragment {
|
||||
} else if (_searchType == SearchType.PLAYLIST) {
|
||||
navigate<PlaylistSearchResultsFragment>(it);
|
||||
} else {
|
||||
if(it.isHttpUrl())
|
||||
navigate<VideoDetailFragment>(it);
|
||||
if(it.isHttpUrl()) {
|
||||
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
|
||||
navigate<RemotePlaylistFragment>(it);
|
||||
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
||||
navigate<ChannelFragment>(it);
|
||||
else
|
||||
navigate<VideoDetailFragment>(it);
|
||||
}
|
||||
else
|
||||
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl));
|
||||
}
|
||||
|
||||
+101
-29
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+226
-74
@@ -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
|
||||
@@ -39,6 +40,7 @@ import androidx.media3.ui.TimeBar
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
@@ -52,6 +54,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
|
||||
@@ -70,9 +73,9 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
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.JSClient
|
||||
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 +106,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 +121,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 +162,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 +234,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 +243,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 +267,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 +292,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 +347,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 +370,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 +386,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 +443,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 +699,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 +707,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 +723,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);
|
||||
|
||||
@@ -787,6 +821,11 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
|
||||
fun updateMoreButtons() {
|
||||
val isLimitedVersion = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
|
||||
if (it is JSClient)
|
||||
return@let it.config.reduceFunctionsInLimitedVersion && BuildConfig.IS_PLAYSTORE_BUILD
|
||||
else false;
|
||||
} ?: false;
|
||||
val buttons = listOf(RoundButton(context, R.drawable.ic_add, context.getString(R.string.add), TAG_ADD) {
|
||||
(video ?: _searchVideo)?.let {
|
||||
_slideUpOverlay = UISlideOverlays.showAddToOverlay(it, _overlayContainer) {
|
||||
@@ -806,38 +845,44 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
_slideUpOverlay?.hide();
|
||||
} else null,
|
||||
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.background), TAG_BACKGROUND) {
|
||||
if(!allowBackground) {
|
||||
_player.switchToAudioMode();
|
||||
allowBackground = true;
|
||||
it.text.text = resources.getString(R.string.background_revert);
|
||||
if(!isLimitedVersion)
|
||||
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.background), TAG_BACKGROUND) {
|
||||
if(!allowBackground) {
|
||||
_player.switchToAudioMode();
|
||||
allowBackground = true;
|
||||
it.text.text = resources.getString(R.string.background_revert);
|
||||
}
|
||||
else {
|
||||
_player.switchToVideoMode();
|
||||
allowBackground = false;
|
||||
it.text.text = resources.getString(R.string.background);
|
||||
}
|
||||
_slideUpOverlay?.hide();
|
||||
}
|
||||
else {
|
||||
_player.switchToVideoMode();
|
||||
allowBackground = false;
|
||||
it.text.text = resources.getString(R.string.background);
|
||||
else null,
|
||||
if(!isLimitedVersion)
|
||||
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
||||
video?.let {
|
||||
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
||||
};
|
||||
}
|
||||
_slideUpOverlay?.hide();
|
||||
},
|
||||
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
||||
video?.let {
|
||||
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
||||
};
|
||||
},
|
||||
RoundButton(context, R.drawable.ic_share, context.getString(R.string.share), TAG_SHARE) {
|
||||
video?.let {
|
||||
Logger.i(TAG, "Share preventPictureInPicture = true");
|
||||
preventPictureInPicture = true;
|
||||
shareVideo();
|
||||
};
|
||||
_slideUpOverlay?.hide();
|
||||
},
|
||||
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.overlay), TAG_OVERLAY) {
|
||||
this.startPictureInPicture();
|
||||
fragment.forcePictureInPicture();
|
||||
//PiPActivity.startPiP(context);
|
||||
_slideUpOverlay?.hide();
|
||||
},
|
||||
else null,
|
||||
RoundButton(context, R.drawable.ic_share, context.getString(R.string.share), TAG_SHARE) {
|
||||
video?.let {
|
||||
Logger.i(TAG, "Share preventPictureInPicture = true");
|
||||
preventPictureInPicture = true;
|
||||
shareVideo();
|
||||
};
|
||||
_slideUpOverlay?.hide();
|
||||
},
|
||||
if(!isLimitedVersion)
|
||||
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.overlay), TAG_OVERLAY) {
|
||||
this.startPictureInPicture();
|
||||
fragment.forcePictureInPicture();
|
||||
//PiPActivity.startPiP(context);
|
||||
_slideUpOverlay?.hide();
|
||||
}
|
||||
else null,
|
||||
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) {
|
||||
video?.let {
|
||||
val url = video?.shareUrl ?: _searchVideo?.shareUrl ?: _url;
|
||||
@@ -986,6 +1031,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 +1069,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
setDescription("".fixHtmlWhitespace());
|
||||
_descriptionContainer.visibility = View.GONE;
|
||||
_player.clear();
|
||||
_textComments.visibility = View.INVISIBLE;
|
||||
_commentsList.clear();
|
||||
|
||||
_lastVideoSource = null;
|
||||
@@ -1047,7 +1092,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
setLastPositionMilliseconds(_videoResumePositionMilliseconds, false);
|
||||
_addCommentView.setContext(null, null);
|
||||
|
||||
_toggleCommentType.setValue(false, false);
|
||||
setTabIndex(0)
|
||||
_commentsList.clear();
|
||||
|
||||
setEmpty();
|
||||
@@ -1083,16 +1128,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 +1219,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 +1325,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 +1364,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 +1524,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
if(video.isLive && video.live == null && !video.video.videoSources.any())
|
||||
startLiveTry(video);
|
||||
|
||||
|
||||
_player.updateNextPrevious();
|
||||
updateMoreButtons();
|
||||
|
||||
@@ -1477,13 +1533,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 +1655,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
});
|
||||
else
|
||||
_player.setArtwork(null);
|
||||
|
||||
_player.setSource(videoSource, audioSource, _playWhenReady, false);
|
||||
if(subtitleSource != null)
|
||||
_player.swapSubtitles(fragment.lifecycleScope, subtitleSource);
|
||||
@@ -1754,6 +1814,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 +2339,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 +2771,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> {
|
||||
|
||||
@@ -18,9 +18,9 @@ class MDNSListener {
|
||||
}
|
||||
|
||||
private val _lockObject = ReentrantLock()
|
||||
private var _receiver4: DatagramSocket? = null
|
||||
private var _receiver6: DatagramSocket? = null
|
||||
private val _senders = mutableListOf<DatagramSocket>()
|
||||
private var _receiver4: MulticastSocket? = null
|
||||
private var _receiver6: MulticastSocket? = null
|
||||
private val _senders = mutableListOf<MulticastSocket>()
|
||||
private val _nicMonitor = NICMonitor()
|
||||
private val _serviceRecordAggregator = ServiceRecordAggregator()
|
||||
private var _started = false
|
||||
@@ -53,13 +53,13 @@ class MDNSListener {
|
||||
|
||||
Logger.i(TAG, "Starting")
|
||||
_lockObject.withLock {
|
||||
val receiver4 = DatagramSocket(null).apply {
|
||||
val receiver4 = MulticastSocket(null).apply {
|
||||
reuseAddress = true
|
||||
bind(InetSocketAddress(InetAddress.getByName("0.0.0.0"), MulticastPort))
|
||||
}
|
||||
_receiver4 = receiver4
|
||||
|
||||
val receiver6 = DatagramSocket(null).apply {
|
||||
val receiver6 = MulticastSocket(null).apply {
|
||||
reuseAddress = true
|
||||
bind(InetSocketAddress(InetAddress.getByName("::"), MulticastPort))
|
||||
}
|
||||
@@ -166,6 +166,11 @@ class MDNSListener {
|
||||
try {
|
||||
when (address) {
|
||||
is Inet4Address -> {
|
||||
_receiver4?.let { receiver4 ->
|
||||
//receiver4.setOption(StandardSocketOptions.IP_MULTICAST_IF, NetworkInterface.getByInetAddress(address))
|
||||
receiver4.joinGroup(InetSocketAddress(MulticastAddressIPv4, MulticastPort), NetworkInterface.getByInetAddress(address))
|
||||
}
|
||||
|
||||
val sender = MulticastSocket(null).apply {
|
||||
reuseAddress = true
|
||||
bind(InetSocketAddress(address, MulticastPort))
|
||||
@@ -175,6 +180,11 @@ class MDNSListener {
|
||||
}
|
||||
|
||||
is Inet6Address -> {
|
||||
_receiver6?.let { receiver6 ->
|
||||
//receiver6.setOption(StandardSocketOptions.IP_MULTICAST_IF, NetworkInterface.getByInetAddress(address))
|
||||
receiver6.joinGroup(InetSocketAddress(MulticastAddressIPv6, MulticastPort), NetworkInterface.getByInetAddress(address))
|
||||
}
|
||||
|
||||
val sender = MulticastSocket(null).apply {
|
||||
reuseAddress = true
|
||||
bind(InetSocketAddress(address, MulticastPort))
|
||||
@@ -222,7 +232,7 @@ class MDNSListener {
|
||||
private fun receiveLoop(client: DatagramSocket) {
|
||||
Logger.i(TAG, "Started receive loop")
|
||||
|
||||
val buffer = ByteArray(1024)
|
||||
val buffer = ByteArray(8972)
|
||||
val packet = DatagramPacket(buffer, buffer.size)
|
||||
while (_started) {
|
||||
try {
|
||||
|
||||
@@ -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(";");
|
||||
|
||||
@@ -10,7 +10,6 @@ import com.futo.platformplayer.constructs.Event1
|
||||
|
||||
|
||||
class MediaControlReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val act = intent?.getStringExtra(EXTRA_MEDIA_ACTION);
|
||||
Logger.i(TAG, "Received MediaControl Event $act");
|
||||
|
||||
@@ -55,9 +55,15 @@ class MediaPlaybackService : Service() {
|
||||
private var _notificationChannel: NotificationChannel? = null;
|
||||
private var _mediaSession: MediaSessionCompat? = null;
|
||||
private var _hasFocus: Boolean = false;
|
||||
private var _isTransientLoss: Boolean = false;
|
||||
private var _focusRequest: AudioFocusRequest? = null;
|
||||
private var _audioFocusLossTime_ms: Long? = null
|
||||
private var _playbackState = PlaybackStateCompat.STATE_NONE;
|
||||
private var _lastAudioFocusAttempt_ms: Long? = null
|
||||
private val isPlaying get() = _playbackState != PlaybackStateCompat.STATE_PAUSED &&
|
||||
_playbackState != PlaybackStateCompat.STATE_STOPPED &&
|
||||
_playbackState != PlaybackStateCompat.STATE_NONE &&
|
||||
_playbackState != PlaybackStateCompat.STATE_ERROR
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Logger.v(TAG, "onStartCommand");
|
||||
@@ -159,12 +165,7 @@ class MediaPlaybackService : Service() {
|
||||
Logger.v(TAG, "closeMediaSession");
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
|
||||
val focusRequest = _focusRequest;
|
||||
if (focusRequest != null) {
|
||||
_audioManager?.abandonAudioFocusRequest(focusRequest);
|
||||
_focusRequest = null;
|
||||
}
|
||||
_hasFocus = false;
|
||||
abandonAudioFocus()
|
||||
|
||||
val notifManager = _notificationManager;
|
||||
Logger.i(TAG, "Cancelling playback notification (notifManager: ${notifManager != null})");
|
||||
@@ -182,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;
|
||||
@@ -198,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();
|
||||
@@ -213,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)
|
||||
@@ -335,29 +347,73 @@ class MediaPlaybackService : Service() {
|
||||
.setState(state, pos, 1f, SystemClock.elapsedRealtime())
|
||||
.build());
|
||||
|
||||
if(_focusRequest == null)
|
||||
setAudioFocus();
|
||||
|
||||
_playbackState = state;
|
||||
try {
|
||||
setAudioFocus()
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to set audio focus", e)
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: (TBD) This code probably more fitting inside FutoVideoPlayer, as this service is generally only used for global events
|
||||
private fun setAudioFocus() {
|
||||
Log.i(TAG, "Requested audio focus.");
|
||||
if (!isPlaying) {
|
||||
return
|
||||
}
|
||||
|
||||
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
||||
.setAcceptsDelayedFocusGain(true)
|
||||
.setOnAudioFocusChangeListener(_audioFocusChangeListener)
|
||||
.build()
|
||||
if (_hasFocus || _isTransientLoss) {
|
||||
return;
|
||||
}
|
||||
|
||||
_focusRequest = focusRequest;
|
||||
val result = _audioManager?.requestAudioFocus(focusRequest)
|
||||
val now = System.currentTimeMillis()
|
||||
val lastAudioFocusAttempt_ms = _lastAudioFocusAttempt_ms
|
||||
if (lastAudioFocusAttempt_ms == null || now - lastAudioFocusAttempt_ms > 1000) {
|
||||
_lastAudioFocusAttempt_ms = now
|
||||
} else {
|
||||
Log.v(TAG, "Skipped trying to get audio focus because gaining audio focus was recently attempted.");
|
||||
return
|
||||
}
|
||||
|
||||
if (_focusRequest == null) {
|
||||
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
||||
.setAcceptsDelayedFocusGain(true)
|
||||
.setOnAudioFocusChangeListener(_audioFocusChangeListener)
|
||||
.build()
|
||||
|
||||
_focusRequest = focusRequest;
|
||||
Log.i(TAG, "Created audio focus request.");
|
||||
}
|
||||
|
||||
Log.i(TAG, "Requesting audio focus.");
|
||||
|
||||
val result = _audioManager?.requestAudioFocus(_focusRequest!!)
|
||||
Log.i(TAG, "Audio focus request result $result");
|
||||
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
//TODO: Handle when not possible to get audio focus
|
||||
_hasFocus = true;
|
||||
_hasFocus = true
|
||||
_isTransientLoss = false
|
||||
Log.i(TAG, "Audio focus received");
|
||||
} else if (result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
|
||||
_hasFocus = false
|
||||
_isTransientLoss = false
|
||||
Log.i(TAG, "Audio focus delayed, waiting for focus")
|
||||
} else {
|
||||
_hasFocus = false
|
||||
_isTransientLoss = false
|
||||
Log.i(TAG, "Audio focus not granted, retrying later")
|
||||
}
|
||||
|
||||
Log.i(TAG, "Audio focus requested.");
|
||||
}
|
||||
|
||||
private fun abandonAudioFocus() {
|
||||
val focusRequest = _focusRequest;
|
||||
if (focusRequest != null) {
|
||||
Logger.i(TAG, "Audio focus abandoned")
|
||||
_audioManager?.abandonAudioFocusRequest(focusRequest);
|
||||
_focusRequest = null;
|
||||
}
|
||||
_hasFocus = false;
|
||||
_isTransientLoss = false;
|
||||
}
|
||||
|
||||
private val _audioFocusChangeListener =
|
||||
@@ -365,19 +421,19 @@ class MediaPlaybackService : Service() {
|
||||
try {
|
||||
when (focusChange) {
|
||||
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
//Do not start playing on gaining audo focus
|
||||
//MediaControlReceiver.onPlayReceived.emit();
|
||||
_hasFocus = true;
|
||||
Log.i(TAG, "Audio focus gained (restartPlaybackAfterLoss = ${Settings.instance.playback.restartPlaybackAfterLoss}, _audioFocusLossTime_ms = $_audioFocusLossTime_ms)");
|
||||
_isTransientLoss = false;
|
||||
|
||||
val audioFocusLossDuration = _audioFocusLossTime_ms?.let { System.currentTimeMillis() - it }
|
||||
_audioFocusLossTime_ms = null
|
||||
Log.i(TAG, "Audio focus gained (restartPlaybackAfterLoss = ${Settings.instance.playback.restartPlaybackAfterLoss}, _audioFocusLossTime_ms = $_audioFocusLossTime_ms, audioFocusLossDuration = ${audioFocusLossDuration})");
|
||||
|
||||
if (Settings.instance.playback.restartPlaybackAfterLoss == 1) {
|
||||
val lossTime_ms = _audioFocusLossTime_ms
|
||||
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 10) {
|
||||
if (audioFocusLossDuration != null && audioFocusLossDuration < 1000 * 10) {
|
||||
MediaControlReceiver.onPlayReceived.emit()
|
||||
}
|
||||
} else if (Settings.instance.playback.restartPlaybackAfterLoss == 2) {
|
||||
val lossTime_ms = _audioFocusLossTime_ms
|
||||
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 30) {
|
||||
if (audioFocusLossDuration != null && audioFocusLossDuration < 1000 * 30) {
|
||||
MediaControlReceiver.onPlayReceived.emit()
|
||||
}
|
||||
} else if (Settings.instance.playback.restartPlaybackAfterLoss == 3) {
|
||||
@@ -385,40 +441,32 @@ class MediaPlaybackService : Service() {
|
||||
}
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||
MediaControlReceiver.onPauseReceived.emit();
|
||||
if (_playbackState != PlaybackStateCompat.STATE_PAUSED &&
|
||||
_playbackState != PlaybackStateCompat.STATE_STOPPED &&
|
||||
_playbackState != PlaybackStateCompat.STATE_NONE &&
|
||||
_playbackState != PlaybackStateCompat.STATE_ERROR) {
|
||||
_audioFocusLossTime_ms = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
Log.i(TAG, "Audio focus transient loss");
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||
Log.i(TAG, "Audio focus transient loss, can duck");
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
if (_playbackState != PlaybackStateCompat.STATE_PAUSED &&
|
||||
_playbackState != PlaybackStateCompat.STATE_STOPPED &&
|
||||
_playbackState != PlaybackStateCompat.STATE_NONE &&
|
||||
_playbackState != PlaybackStateCompat.STATE_ERROR) {
|
||||
_audioFocusLossTime_ms = System.currentTimeMillis()
|
||||
_audioFocusLossTime_ms = if (isPlaying) {
|
||||
System.currentTimeMillis()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
_hasFocus = false;
|
||||
_isTransientLoss = true;
|
||||
MediaControlReceiver.onPauseReceived.emit();
|
||||
Log.i(TAG, "Audio focus lost");
|
||||
|
||||
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
val runningAppProcesses = activityManager.runningAppProcesses
|
||||
for (processInfo in runningAppProcesses) {
|
||||
// Check the importance of the running app process
|
||||
if (processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
|
||||
// This app is in the foreground, which might have caused the loss of audio focus
|
||||
Log.i("AudioFocus", "App ${processInfo.processName} might have caused the loss of audio focus")
|
||||
}
|
||||
Log.i(TAG, "Audio focus transient loss (_audioFocusLossTime_ms = ${_audioFocusLossTime_ms})");
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||
Log.i(TAG, "Audio focus transient loss, can duck");
|
||||
_hasFocus = true;
|
||||
_isTransientLoss = true;
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
_audioFocusLossTime_ms = if (isPlaying) {
|
||||
System.currentTimeMillis()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
MediaControlReceiver.onPauseReceived.emit();
|
||||
abandonAudioFocus();
|
||||
Log.i(TAG, "Audio focus lost");
|
||||
}
|
||||
}
|
||||
} catch(ex: Throwable) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
+17
-5
@@ -130,6 +130,11 @@ open class PreviewVideoView : LinearLayout {
|
||||
_button_add_to_watch_later.setOnClickListener { currentVideo?.let { onAddToWatchLaterClicked.emit(it); } }
|
||||
}
|
||||
|
||||
fun hideAddTo() {
|
||||
_button_add_to.visibility = View.GONE
|
||||
_button_add_to_queue.visibility = View.GONE
|
||||
}
|
||||
|
||||
protected open fun inflate(feedStyle: FeedStyle) {
|
||||
inflate(context, when(feedStyle) {
|
||||
FeedStyle.PREVIEW -> R.layout.list_video_preview
|
||||
@@ -165,11 +170,18 @@ open class PreviewVideoView : LinearLayout {
|
||||
|
||||
_imageNeopassChannel?.visibility = View.GONE;
|
||||
_creatorThumbnail?.setThumbnail(content.author.thumbnail, false);
|
||||
_imageChannel?.let {
|
||||
Glide.with(_imageChannel)
|
||||
.load(content.author.thumbnail)
|
||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||
.into(_imageChannel);
|
||||
|
||||
val thumbnail = content.author.thumbnail
|
||||
if (thumbnail != null) {
|
||||
_imageChannel?.visibility = View.VISIBLE
|
||||
_imageChannel?.let {
|
||||
Glide.with(_imageChannel)
|
||||
.load(content.author.thumbnail)
|
||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||
.into(_imageChannel);
|
||||
}
|
||||
} else {
|
||||
_imageChannel?.visibility = View.GONE
|
||||
}
|
||||
|
||||
_textChannelName.text = content.author.name
|
||||
|
||||
@@ -13,6 +13,10 @@ annotation class FormField(val title: Int, val type: String, val subtitle: Int =
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class FormFieldWarning(val messageRes: Int)
|
||||
|
||||
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class FormFieldHint(val messageRes: Int)
|
||||
|
||||
interface IField {
|
||||
var descriptor: FormField?;
|
||||
val obj : Any?;
|
||||
|
||||
@@ -293,6 +293,12 @@ class FieldForm : LinearLayout {
|
||||
}, UIDialogs.ActionStyle.PRIMARY));
|
||||
}
|
||||
}
|
||||
val hint = propertyMap[field.field]?.findAnnotation<FormFieldHint>();
|
||||
if(hint != null){
|
||||
field.onChanged.subscribe { f, value, oldValue ->
|
||||
UIDialogs.appToast(context.getString(hint.messageRes), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.</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>
|
||||
Submodule app/src/stable/assets/sources/bilibili updated: 850acb49a8...31490e10f9
Submodule app/src/stable/assets/sources/rumble updated: bedbc4a989...cbfe372bcc
Submodule app/src/stable/assets/sources/youtube updated: 6f3352a276...35b56d380a
@@ -68,9 +68,19 @@ class ExtensionsFormattingTests {
|
||||
@Test
|
||||
fun testMatchesDomain() {
|
||||
assertTrue("google.com".matchesDomain("google.com"))
|
||||
assertTrue("google.com".matchesDomain(".google.com"))
|
||||
assertFalse("yahoo.com".matchesDomain("google.com"))
|
||||
assertTrue("mail.google.com".matchesDomain(".google.com"))
|
||||
}
|
||||
@Test
|
||||
fun testPrimaryDomain() {
|
||||
assertEquals(".google.com", "google.com".getSubdomainWildcardQuery());
|
||||
assertEquals(".google.com", "test.google.com".getSubdomainWildcardQuery());
|
||||
assertEquals(".google.com", "test1.test2.google.com".getSubdomainWildcardQuery());
|
||||
assertEquals(".google.co.uk", "google.co.uk".getSubdomainWildcardQuery());
|
||||
assertEquals(".google.co.uk", "test.google.co.uk".getSubdomainWildcardQuery());
|
||||
assertEquals(".google.co.uk", "test1.test2.google.co.uk".getSubdomainWildcardQuery());
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTimeDiff() {
|
||||
|
||||
Submodule app/src/unstable/assets/sources/bilibili updated: 850acb49a8...31490e10f9
Submodule app/src/unstable/assets/sources/rumble updated: bedbc4a989...cbfe372bcc
Submodule app/src/unstable/assets/sources/youtube updated: 6f3352a276...35b56d380a
Reference in New Issue
Block a user