Compare commits

...

23 Commits

Author SHA1 Message Date
Kelvin cf3587f504 last selected comment section option and default 2024-09-06 17:21:50 +02:00
Kelvin d42f104884 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-06 16:29:31 +02:00
Kelvin 6a43568369 Dialog improvement, prep dialog 2024-09-06 16:29:24 +02:00
Koen J 85c9cd0a6e Recommendation mostly finished. 2024-09-06 16:00:13 +02:00
Koen J be5920cfae Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-06 15:36:05 +02:00
Koen J 3d25d94a77 Mostly implemented recommendations. 2024-09-06 15:35:59 +02:00
Kelvin fe97850835 Remove target size 2024-09-06 15:33:55 +02:00
Kelvin dab9decd89 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-06 15:32:58 +02:00
Kelvin 854651aa71 Fix notification thumbnail pixelation on newer androids 2024-09-06 15:32:49 +02:00
Koen J fdd1af3287 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-06 10:13:09 +02:00
Koen J 0bf92b6aff Home button refresh added. Possible fix for Grayjay starting playback after hours of being inactive. Scroll to top and reload feed. 2024-09-06 10:12:23 +02:00
Kelvin d9403bf4da Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-05 20:18:37 +02:00
Kelvin 716d8caf4d SLD crash fix 2024-09-05 20:18:29 +02:00
Koen J 0f0f368a75 Do not allow downloading/editing name of temporary playlist. 2024-09-05 13:14:41 +02:00
Koen J ff8d7558d4 Re-added bypass rotation prevention. 2024-09-05 12:42:12 +02:00
Koen J 66f9824b68 Finetuning rotation. 2024-09-05 10:53:42 +02:00
Koen J 44a6e5da38 Added background subscription upadte failed toast and removed home page refresh when older than a minute. 2024-09-05 10:04:53 +02:00
Kelvin de5a4aa5f3 Duplicate client id dialog and filtering, scrollable code block for dialogs 2024-09-04 21:24:26 +02:00
Kelvin e8007082a7 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-04 20:02:55 +02:00
Kelvin 3c70c5a366 Better handling of null author, search url fixes, Handling of more than 1000 subscriptions 2024-09-04 20:02:44 +02:00
Koen J eb6e79b055 Catch WorkManager crash. 2024-09-04 14:24:11 +02:00
Kelvin ea59f8dccb Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-03 22:33:04 +02:00
Kelvin aef1c584e5 Article spec structures 2024-09-03 22:32:50 +02:00
33 changed files with 772 additions and 299 deletions
+39 -2
View File
@@ -201,7 +201,7 @@ class PlatformContent {
obj = obj ?? {};
this.id = obj.id ?? PlatformID(); //PlatformID
this.name = obj.name ?? ""; //string
this.thumbnails = obj.thumbnails; //Thumbnail[]
this.thumbnails = obj.thumbnails ?? new Thumbnails([]); //Thumbnail[]
this.author = obj.author; //PlatformAuthorLink
this.datetime = obj.datetime ?? obj.uploadDate ?? 0; //OffsetDateTime (Long)
this.url = obj.url ?? ""; //String
@@ -278,12 +278,49 @@ class PlatformPostDetails extends PlatformPost {
super(obj);
obj = obj ?? {};
this.plugin_type = "PlatformPostDetails";
this.rating = obj.rating ?? RatingLikes(-1);
this.rating = obj.rating ?? new RatingLikes(-1);
this.textType = obj.textType ?? 0;
this.content = obj.content ?? "";
}
}
class PlatformArticleDetails extends PlatformContent {
constructor(obj) {
super(obj, 3);
obj = obj ?? {};
this.plugin_type = "PlatformArticleDetails";
this.rating = obj.rating ?? new RatingLikes(-1);
this.summary = obj.summary ?? "";
this.segments = obj.segments ?? [];
this.thumbnails = obj.thumbnails ?? new Thumbnails([]);
}
}
class ArticleSegment {
constructor(type) {
this.type = type;
}
}
class ArticleTextSegment extends ArticleSegment {
constructor(content, textType) {
super(1);
this.textType = textType;
this.content = content;
}
}
class ArticleImagesSegment extends ArticleSegment {
constructor(images) {
super(2);
this.images = images;
}
}
class ArticleNestedSegment extends ArticleSegment {
constructor(nested) {
super(9);
this.nested = nested;
}
}
//Sources
class VideoSourceDescriptor {
constructor(obj) {
@@ -360,11 +360,11 @@ fun String.matchesDomain(queryDomain: String): Boolean {
val parts = queryDomain.lowercase().split(".");
if(parts.size < 3)
throw IllegalStateException("Illegal use of wildcards on First-Level-Domain");
throw IllegalStateException("Illegal use of wildcards on First-Level-Domain (" + queryDomain + ")");
if(parts.size >= 3){
val isSLD = slds.contains("." + parts[parts.size - 2] + "." + parts[parts.size - 1]);
if(isSLD && parts.size <= 3)
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain");
throw IllegalStateException("Illegal use of wildcards on Second-Level-Domain (" + queryDomain + ")");
}
//TODO: Should be safe, but double verify if can't be exploited
@@ -372,4 +372,13 @@ fun String.matchesDomain(queryDomain: String): Boolean {
}
else
return this == queryDomain;
}
fun String.getSubdomainWildcardQuery(): String {
val domainParts = this.split(".");
val sldParts = "." + domainParts[domainParts.size - 2].lowercase() + "." + domainParts[domainParts.size - 1].lowercase();
if(slds.contains(sldParts))
return "." + domainParts.drop(domainParts.size - 3).joinToString(".");
else
return "." + domainParts.drop(domainParts.size - 2).joinToString(".");
}
@@ -485,12 +485,18 @@ class Settings : FragmentedStorageFileJson() {
var comments = CommentSettings();
@Serializable
class CommentSettings {
var didAskPolycentricDefault: Boolean = false;
@FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
@DropdownFieldOptionsId(R.array.comment_sections)
var defaultCommentSection: Int = 1;
var defaultCommentSection: Int = 2;
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
var recommendationsDefault: Boolean = false;
@FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0)
var badReputationCommentsFading: Boolean = true;
}
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 7)
@@ -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)
@@ -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 {
@@ -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);
@@ -16,6 +16,7 @@ interface IJSContentDetails: IPlatformContent {
return when(ContentType.fromInt(type)) {
ContentType.MEDIA -> JSVideoDetails(plugin, obj);
ContentType.POST -> JSPostDetails(plugin.config, obj);
ContentType.ARTICLE -> JSArticleDetails(plugin, obj);
else -> throw NotImplementedError("Unknown content type ${type}");
}
}
@@ -1,78 +0,0 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.post.TextType
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import kotlin.streams.toList
open class JSArticle : JSContent, IPluginSourced {
final override val contentType: ContentType get() = ContentType.POST;
val summary: String;
val thumbnails: Thumbnails?;
val segments: List<IJSArticleSegment>;
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
val contextName = "PlatformPost";
summary = _content.getOrThrow(config, "summary", contextName);
if(_content.has("thumbnails"))
thumbnails = Thumbnails.fromV8(config, _content.getOrThrow(config, "thumbnails", contextName));
else
thumbnails = null;
segments = (obj.getOrThrowNullableList<V8ValueObject>(config, "segments", contextName)
?.map { fromV8Segment(config, it) }
?.filterNotNull() ?: listOf());
}
companion object {
fun fromV8Segment(config: SourcePluginConfig, obj: V8ValueObject): IJSArticleSegment? {
if(!obj.has("type"))
throw IllegalArgumentException("Object missing type field");
return when(obj.getOrThrow<SegmentType>(config, "type", "JSArticle.Segment")) {
SegmentType.TEXT -> JSTextSegment(config, obj);
SegmentType.IMAGES -> JSImagesSegment(config, obj);
else -> null;
}
}
}
}
enum class SegmentType(i: Int) {
UNKNOWN(0),
TEXT(1),
IMAGES(2)
}
interface IJSArticleSegment {
val type: SegmentType;
}
class JSTextSegment: IJSArticleSegment {
override val type = SegmentType.TEXT;
val textType: TextType;
val content: String;
constructor(config: SourcePluginConfig, obj: V8ValueObject) {
val contextName = "JSTextSegment";
textType = TextType.fromInt((obj.getOrDefault<Int>(config, "textType", contextName, null) ?: 0));
content = obj.getOrDefault(config, "content", contextName, "") ?: "";
}
}
class JSImagesSegment: IJSArticleSegment {
override val type = SegmentType.IMAGES;
val images: List<String>;
constructor(config: SourcePluginConfig, obj: V8ValueObject) {
val contextName = "JSTextSegment";
images = obj.getOrThrowNullableList<String>(config, "images", contextName) ?: listOf();
}
}
@@ -0,0 +1,162 @@
package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.post.TextType
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullableList
import com.futo.platformplayer.states.StateDeveloper
open class JSArticleDetails : JSContent, IPluginSourced, IPlatformContentDetails {
final override val contentType: ContentType get() = ContentType.ARTICLE;
private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean;
val rating: IRating;
val summary: String;
val thumbnails: Thumbnails?;
val segments: List<IJSArticleSegment>;
constructor(client: JSClient, obj: V8ValueObject): super(client.config, obj) {
val contextName = "PlatformPost";
rating = obj.getOrDefault<V8ValueObject>(client.config, "rating", contextName, null)?.let { IRating.fromV8(client.config, it, contextName) } ?: RatingLikes(0);
summary = _content.getOrThrow(client.config, "summary", contextName);
if(_content.has("thumbnails"))
thumbnails = Thumbnails.fromV8(client.config, _content.getOrThrow(client.config, "thumbnails", contextName));
else
thumbnails = null;
segments = (obj.getOrThrowNullableList<V8ValueObject>(client.config, "segments", contextName)
?.map { fromV8Segment(client, it) }
?.filterNotNull() ?: listOf());
_hasGetComments = _content.has("getComments");
_hasGetContentRecommendations = _content.has("getContentRecommendations");
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
if(!_hasGetComments || _content.isClosed)
return null;
if(client is DevJSClient)
return StateDeveloper.instance.handleDevCall(client.devID, "videoDetail.getComments()") {
return@handleDevCall getCommentsJS(client);
}
else if(client is JSClient)
return getCommentsJS(client);
return null;
}
override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
if(!_hasGetContentRecommendations || _content.isClosed)
return null;
if(client is DevJSClient)
return StateDeveloper.instance.handleDevCall(client.devID, "postDetail.getContentRecommendations()") {
return@handleDevCall getContentRecommendationsJS(client);
}
else if(client is JSClient)
return getContentRecommendationsJS(client);
return null;
}
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager);
}
private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client, commentPager);
}
companion object {
fun fromV8Segment(client: JSClient, obj: V8ValueObject): IJSArticleSegment? {
if(!obj.has("type"))
throw IllegalArgumentException("Object missing type field");
return when(SegmentType.fromInt(obj.getOrThrow(client.config, "type", "JSArticle.Segment"))) {
SegmentType.TEXT -> JSTextSegment(client, obj);
SegmentType.IMAGES -> JSImagesSegment(client, obj);
SegmentType.NESTED -> JSNestedSegment(client, obj);
else -> null;
}
}
}
}
enum class SegmentType(val value: Int) {
UNKNOWN(0),
TEXT(1),
IMAGES(2),
NESTED(9);
companion object {
fun fromInt(value: Int): SegmentType
{
val result = SegmentType.entries.firstOrNull { it.value == value };
if(result == null)
throw IllegalArgumentException("Unknown Texttype: $value");
return result;
}
}
}
interface IJSArticleSegment {
val type: SegmentType;
}
class JSTextSegment: IJSArticleSegment {
override val type = SegmentType.TEXT;
val textType: TextType;
val content: String;
constructor(client: JSClient, obj: V8ValueObject) {
val contextName = "JSTextSegment";
textType = TextType.fromInt((obj.getOrDefault<Int>(client.config, "textType", contextName, null) ?: 0));
content = obj.getOrDefault(client.config, "content", contextName, "") ?: "";
}
}
class JSImagesSegment: IJSArticleSegment {
override val type = SegmentType.IMAGES;
val images: List<String>;
val caption: String;
constructor(client: JSClient, obj: V8ValueObject) {
val contextName = "JSTextSegment";
images = obj.getOrThrowNullableList<String>(client.config, "images", contextName) ?: listOf();
caption = obj.getOrDefault(client.config, "caption", contextName, "") ?: "";
}
}
class JSNestedSegment: IJSArticleSegment {
override val type = SegmentType.NESTED;
val nested: IPlatformContent;
constructor(client: JSClient, obj: V8ValueObject) {
val contextName = "JSNestedSegment";
val nestedObj = obj.getOrThrow<V8ValueObject>(client.config, "nested", contextName, false);
nested = IJSContent.fromV8(client, nestedObj);
}
}
@@ -42,7 +42,12 @@ open class JSContent : IPlatformContent, IPluginSourced {
id = PlatformID.fromV8(_pluginConfig, _content.getOrThrow(config, "id", contextName));
name = HtmlCompat.fromHtml(_content.getOrThrow<String>(config, "name", contextName).decodeUnicode(), HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
author = PlatformAuthorLink.fromV8(_pluginConfig, _content.getOrThrow(config, "author", contextName));
val authorObj = _content.getOrDefault<V8ValueObject>(config, "author", contextName, null);
if(authorObj != null)
author = PlatformAuthorLink.fromV8(_pluginConfig, authorObj);
else
author = PlatformAuthorLink.UNKNOWN;
val datetimeInt = _content.getOrThrow<Int>(config, "datetime", contextName).toLong();
if(datetimeInt == 0.toLong())
@@ -368,7 +368,15 @@ class MenuBottomBarFragment : MainActivityFragment() {
//Add configurable buttons here
var buttonDefinitions = listOf(
ButtonDefinition(0, R.drawable.ic_home, R.drawable.ic_home_filled, R.string.home, canToggle = true, { it.currentMain is HomeFragment }, { it.navigate<HomeFragment>() }),
ButtonDefinition(0, R.drawable.ic_home, R.drawable.ic_home_filled, R.string.home, canToggle = true, { it.currentMain is HomeFragment }, {
val currentMain = it.currentMain
if (currentMain is HomeFragment) {
currentMain.scrollToTop(false)
currentMain.reloadFeed()
} else {
it.navigate<HomeFragment>()
}
}),
ButtonDefinition(1, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate<SubscriptionsFeedFragment>() }),
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>() }),
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>() }),
@@ -46,6 +46,14 @@ class HomeFragment : MainFragment() {
private var _view: HomeView? = null;
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
fun reloadFeed() {
_view?.reloadFeed()
}
fun scrollToTop(smooth: Boolean) {
_view?.scrollToTop(smooth)
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
_view?.onShown();
@@ -138,17 +146,12 @@ class HomeFragment : MainFragment() {
fun onShown() {
val lastClients = recyclerData.lastClients;
val clients = StatePlatform.instance.getSortedEnabledClient().filter { if (it is JSClient) it.enableInHome else true };
val feedstyleChanged = recyclerData.loadedFeedStyle != feedStyle;
val clientsChanged = lastClients == null || lastClients.size != clients.size || !lastClients.containsAll(clients);
val outdated = recyclerData.lastLoad.getNowDiffSeconds() > 60;
Logger.i(TAG, "onShown (recyclerData.loadedFeedStyle=${recyclerData.loadedFeedStyle}, recyclerData.lastLoad=${recyclerData.lastLoad}, feedstyleChanged=$feedstyleChanged, clientsChanged=$clientsChanged, outdated=$outdated)")
Logger.i(TAG, "onShown (recyclerData.loadedFeedStyle=${recyclerData.loadedFeedStyle}, recyclerData.lastLoad=${recyclerData.lastLoad}, feedstyleChanged=$feedstyleChanged, clientsChanged=$clientsChanged)")
if(feedstyleChanged || outdated || clientsChanged) {
recyclerData.lastLoad = OffsetDateTime.now();
recyclerData.loadedFeedStyle = feedStyle;
recyclerData.lastClients = clients;
loadResults();
if(feedstyleChanged || clientsChanged) {
reloadFeed()
} else {
setLoading(false);
}
@@ -156,6 +159,21 @@ class HomeFragment : MainFragment() {
finishRefreshLayoutLoader();
}
fun scrollToTop(smooth: Boolean) {
if (smooth) {
_recyclerResults.smoothScrollToPosition(0)
} else {
_recyclerResults.scrollToPosition(0)
}
}
fun reloadFeed() {
recyclerData.lastLoad = OffsetDateTime.now();
recyclerData.loadedFeedStyle = feedStyle;
recyclerData.lastClients = StatePlatform.instance.getSortedEnabledClient().filter { if (it is JSClient) it.enableInHome else true };
loadResults();
}
override fun getEmptyPagerView(): View? {
val dp10 = 10.dp(resources);
val dp30 = 30.dp(resources);
@@ -156,6 +156,14 @@ class PlaylistFragment : MainFragment() {
};
}
private fun copyPlaylist(playlist: Playlist) {
StatePlaylists.instance.playlistStore.save(playlist)
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(
arrayListOf()
)
UIDialogs.toast("Playlist saved")
}
fun onShown(parameter: Any?) {
_taskLoadPlaylist.cancel()
@@ -170,14 +178,10 @@ class PlaylistFragment : MainFragment() {
setButtonDownloadVisible(true)
setButtonEditVisible(true)
if (!StatePlaylists.instance.playlistStore.getItems().contains(parameter)) {
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
_fragment.topBar?.assume<NavigationTopBarFragment>()
?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
StatePlaylists.instance.playlistStore.save(parameter)
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(
arrayListOf()
)
UIDialogs.toast("Playlist saved")
copyPlaylist(parameter)
}))
}
} else {
@@ -242,6 +246,15 @@ class PlaylistFragment : MainFragment() {
}
private fun download() {
val playlist = _playlist ?: return
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to download", {
copyPlaylist(playlist)
download()
})
return
}
_playlist?.let {
UISlideOverlays.showDownloadPlaylistOverlay(it, overlayContainer);
}
@@ -266,6 +279,15 @@ class PlaylistFragment : MainFragment() {
override fun canEdit(): Boolean { return _playlist != null; }
override fun onEditClick() {
val playlist = _playlist ?: return
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to edit the name", {
copyPlaylist(playlist)
onEditClick()
})
return
}
_editPlaylistNameInput?.activate();
_editPlaylistOverlay?.show();
}
@@ -9,6 +9,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewPropertyAnimator
import android.widget.Button
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
@@ -19,6 +20,7 @@ import androidx.core.view.children
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.Thumbnails
@@ -135,10 +137,7 @@ class PostDetailFragment : MainFragment {
private val _imageDislikeIcon: ImageView;
private val _textDislikes: TextView;
private val _textComments: TextView;
private val _textCommentType: TextView;
private val _addCommentView: AddCommentView;
private val _toggleCommentType: Toggle;
private val _rating: PillRatingLikesDislikes;
@@ -152,6 +151,10 @@ class PostDetailFragment : MainFragment {
private val _commentsList: CommentsList;
private var _commentType: Boolean? = null;
private val _buttonPolycentric: Button
private val _buttonPlatform: Button
private val _taskLoadPost = if(!isInEditMode) TaskHandler<String, IPlatformPostDetails>(
StateApp.instance.scopeGetter,
{
@@ -198,9 +201,6 @@ class PostDetailFragment : MainFragment {
_textDislikes = findViewById(R.id.text_dislikes);
_commentsList = findViewById(R.id.comments_list);
_textCommentType = findViewById(R.id.text_comment_type);
_toggleCommentType = findViewById(R.id.toggle_comment_type);
_textComments = findViewById(R.id.text_comments);
_addCommentView = findViewById(R.id.add_comment_view);
_rating = findViewById(R.id.rating);
@@ -213,6 +213,9 @@ class PostDetailFragment : MainFragment {
_repliesOverlay = findViewById(R.id.replies_overlay);
_buttonPolycentric = findViewById(R.id.button_polycentric)
_buttonPlatform = findViewById(R.id.button_platform)
_textContent.setPlatformPlayerLinkMovementMethod(context);
_buttonSubscribe.onSubscribed.subscribe {
@@ -224,9 +227,10 @@ class PostDetailFragment : MainFragment {
root.removeView(layoutTop);
_commentsList.setPrependedView(layoutTop);
/*TODO: Why is this here?
_commentsList.onCommentsLoaded.subscribe {
updateCommentType(false);
};
};*/
_commentsList.onRepliesClick.subscribe { c ->
val replyCount = c.replyCount ?: 0;
@@ -237,7 +241,7 @@ class PostDetailFragment : MainFragment {
if (c is PolycentricPlatformComment) {
var parentComment: PolycentricPlatformComment = c;
_repliesOverlay.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference, c,
_repliesOverlay.load(_commentType!!, metadata, c.contextUrl, c.reference, c,
{ StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) },
{
val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1);
@@ -245,22 +249,23 @@ class PostDetailFragment : MainFragment {
parentComment = newComment;
});
} else {
_repliesOverlay.load(_toggleCommentType.value, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
_repliesOverlay.load(_commentType!!, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
}
setRepliesOverlayVisible(isVisible = true, animate = true);
};
if (StatePolycentric.instance.enabled) {
_buttonPolycentric.setOnClickListener {
updateCommentType(false)
}
} else {
_buttonPolycentric.visibility = View.GONE
}
_toggleCommentType.onValueChanged.subscribe {
updateCommentType(true);
};
_textCommentType.setOnClickListener {
_toggleCommentType.setValue(!_toggleCommentType.value, true);
updateCommentType(true);
};
_buttonPlatform.setOnClickListener {
updateCommentType(true)
}
_layoutMonetization.visibility = View.GONE;
_buttonSupport.setOnClickListener {
@@ -432,7 +437,7 @@ class PostDetailFragment : MainFragment {
_taskLoadPolycentricProfile.cancel();
_version++;
_toggleCommentType.setValue(false, false);
updateCommentType(null)
_url = null;
_post = null;
_postOverview = null;
@@ -476,7 +481,8 @@ class PostDetailFragment : MainFragment {
_addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray()));
}
updateCommentType(true);
val commentType = !Settings.instance.other.polycentricEnabled || Settings.instance.comments.defaultCommentSection == 1
updateCommentType(commentType, true);
setLoading(false);
}
@@ -679,20 +685,29 @@ class PostDetailFragment : MainFragment {
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(post!!.url, ref, listOfNotNull(extraBytesRef)); };
}
private fun updateCommentType(reloadComments: Boolean) {
if (_toggleCommentType.value) {
_textCommentType.text = "Platform";
_addCommentView.visibility = View.GONE;
private fun updateCommentType(commentType: Boolean?, forceReload: Boolean = false) {
val changed = commentType != _commentType
_commentType = commentType
if (reloadComments) {
fetchComments();
}
if (commentType == null) {
_buttonPlatform.setTextColor(resources.getColor(R.color.gray_ac))
_buttonPolycentric.setTextColor(resources.getColor(R.color.gray_ac))
} else {
_textCommentType.text = "Polycentric";
_addCommentView.visibility = View.VISIBLE;
_buttonPlatform.setTextColor(resources.getColor(if (commentType) R.color.white else R.color.gray_ac))
_buttonPolycentric.setTextColor(resources.getColor(if (!commentType) R.color.white else R.color.gray_ac))
if (reloadComments) {
fetchPolycentricComments()
if (commentType) {
_addCommentView.visibility = View.GONE;
if (forceReload || changed) {
fetchComments();
}
} else {
_addCommentView.visibility = View.VISIBLE;
if (forceReload || changed) {
fetchPolycentricComments()
}
}
}
}
@@ -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));
}
@@ -93,22 +93,40 @@ class VideoDetailFragment : MainFragment {
}
private fun updateOrientation() {
val a = activity ?: return
val isMaximized = state == State.MAXIMIZED
val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait;
val currentOrientation = _currentOrientation
val bypassRotationPrevention = Settings.instance.other.bypassRotationPrevention;
val currentRequestedOrientation = a.requestedOrientation
val currentOrientation = if (_currentOrientation == -1) currentRequestedOrientation else _currentOrientation
val isAutoRotate = Settings.instance.playback.isAutoRotate()
val isFs = isFullscreen
if (isFs && isMaximized) {
if (isFullScreenPortraitAllowed) {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
if (isAutoRotate) {
a.requestedOrientation = currentOrientation
}
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
if (isAutoRotate) {
a.requestedOrientation = currentOrientation
}
} else {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
} else if (bypassRotationPrevention) {
if (isAutoRotate) {
a.requestedOrientation = currentOrientation
}
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT) {
if (isAutoRotate) {
a.requestedOrientation = currentOrientation
}
} else {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
Log.i(TAG, "updateOrientation (isFs = ${isFs}, currentOrientation = ${currentOrientation}, isMaximized = ${isMaximized}, isFullScreenPortraitAllowed = ${isFullScreenPortraitAllowed}) resulted in requested orientation ${activity?.requestedOrientation}");
Log.i(TAG, "updateOrientation (isFs = ${isFs}, currentOrientation = ${currentOrientation}, currentRequestedOrientation = ${currentRequestedOrientation}, isMaximized = ${isMaximized}, isAutoRotate = ${isAutoRotate}, isFullScreenPortraitAllowed = ${isFullScreenPortraitAllowed}) resulted in requested orientation ${activity?.requestedOrientation}");
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
@@ -269,6 +287,9 @@ class VideoDetailFragment : MainFragment {
}
_autoRotateChangeListener = AutoRotateChangeListener(requireContext(), Handler()) { _ ->
if (updateAutoFullscreen()) {
return@AutoRotateChangeListener
}
updateOrientation()
}
@@ -280,6 +301,9 @@ class VideoDetailFragment : MainFragment {
}
StatePlayer.instance.onRotationLockChanged.subscribe(this) {
if (updateAutoFullscreen()) {
return@subscribe
}
updateOrientation()
}
@@ -288,23 +312,29 @@ class VideoDetailFragment : MainFragment {
_currentOrientation = it
Logger.i(TAG, "Current orientation changed (_currentOrientation = ${_currentOrientation})")
if (Settings.instance.playback.isAutoRotate()) {
if (state == State.MAXIMIZED && !isFullscreen && (it == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || it == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE)) {
_viewDetail?.setFullscreen(true)
return@subscribe
}
if (state == State.MAXIMIZED && isFullscreen && !Settings.instance.playback.fullscreenPortrait && (it == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || it == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT)) {
_viewDetail?.setFullscreen(false)
return@subscribe
}
if (updateAutoFullscreen()) {
return@subscribe
}
updateOrientation()
}
return _view!!;
}
private fun updateAutoFullscreen(): Boolean {
if (Settings.instance.playback.isAutoRotate()) {
if (state == State.MAXIMIZED && !isFullscreen && (_currentOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || _currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE)) {
_viewDetail?.setFullscreen(true)
return true
}
if (state == State.MAXIMIZED && isFullscreen && !Settings.instance.playback.fullscreenPortrait && (_currentOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || _currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT)) {
_viewDetail?.setFullscreen(false)
return true
}
}
return false
}
fun onUserLeaveHint() {
val viewDetail = _viewDetail;
Logger.i(TAG, "onUserLeaveHint preventPictureInPicture=${viewDetail?.preventPictureInPicture} isCasting=${StateCasting.instance.isCasting} isBackgroundPictureInPicture=${Settings.instance.playback.isBackgroundPictureInPicture()} allowBackground=${viewDetail?.allowBackground}");
@@ -23,6 +23,7 @@ import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.WindowManager
import android.widget.Button
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
@@ -52,6 +53,7 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
import com.futo.platformplayer.api.media.models.chapters.ChapterType
import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
@@ -72,7 +74,6 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.casting.CastConnectionState
import com.futo.platformplayer.casting.StateCasting
@@ -103,6 +104,7 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
@@ -117,12 +119,14 @@ import com.futo.platformplayer.toHumanBytesSize
import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.toHumanNumber
import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.LoaderView
import com.futo.platformplayer.views.MonetizationView
import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoView
import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
import com.futo.platformplayer.views.casting.CastView
import com.futo.platformplayer.views.comments.AddCommentView
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.others.Toggle
import com.futo.platformplayer.views.overlays.DescriptionOverlay
import com.futo.platformplayer.views.overlays.LiveChatOverlay
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
@@ -156,6 +160,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import okhttp3.Dispatcher
import org.w3c.dom.Text
import userpackage.Protocol
import java.time.OffsetDateTime
import kotlin.math.abs
@@ -226,10 +232,8 @@ class VideoDetailView : ConstraintLayout {
var preventPictureInPicture: Boolean = false;
private val _textComments: TextView;
private val _textCommentType: TextView;
private val _addCommentView: AddCommentView;
private val _toggleCommentType: Toggle;
private var _tabIndex: Int? = null;
private val _layoutSkip: LinearLayout;
private val _textSkip: TextView;
@@ -237,6 +241,7 @@ class VideoDetailView : ConstraintLayout {
private val _layoutResume: LinearLayout;
private var _jobHideResume: Job? = null;
private val _layoutPlayerContainer: TouchInterceptFrameLayout;
private val _layoutChangeBottomSection: LinearLayout;
//Overlays
private val _overlayContainer: FrameLayout;
@@ -260,12 +265,16 @@ class VideoDetailView : ConstraintLayout {
private val _layoutRating: LinearLayout;
private val _imageDislikeIcon: ImageView;
private val _imageLikeIcon: ImageView;
private val _layoutToggleCommentSection: LinearLayout;
private val _monetization: MonetizationView;
private val _buttonMore: RoundButton;
private val _buttonPolycentric: Button
private val _buttonPlatform: Button
private val _buttonRecommended: Button
private val _layoutRecommended: LinearLayout
private var _didStop: Boolean = false;
private var _onPauseCalled = false;
private var _lastVideoSource: IVideoSource? = null;
@@ -335,9 +344,8 @@ class VideoDetailView : ConstraintLayout {
_overlay_loading_spinner = findViewById(R.id.videodetail_loader);
_rating = findViewById(R.id.videodetail_rating);
_upNext = findViewById(R.id.up_next);
_textCommentType = findViewById(R.id.text_comment_type);
_toggleCommentType = findViewById(R.id.toggle_comment_type);
_layoutToggleCommentSection = findViewById(R.id.layout_toggle_comment_section);
_layoutChangeBottomSection = findViewById(R.id.layout_change_bottom_section);
_layoutRecommended = findViewById(R.id.layout_recommended)
_overlayContainer = findViewById(R.id.overlay_container);
_overlay_quality_container = findViewById(R.id.videodetail_quality_overview);
@@ -359,7 +367,6 @@ class VideoDetailView : ConstraintLayout {
_container_content_support = findViewById(R.id.videodetail_container_support);
_container_content_browser = findViewById(R.id.videodetail_container_webview)
_textComments = findViewById(R.id.text_comments);
_addCommentView = findViewById(R.id.add_comment_view);
_commentsList = findViewById(R.id.comments_list);
@@ -376,6 +383,10 @@ class VideoDetailView : ConstraintLayout {
_imageLikeIcon = findViewById(R.id.image_like_icon);
_imageDislikeIcon = findViewById(R.id.image_dislike_icon);
_buttonPolycentric = findViewById(R.id.button_polycentric)
_buttonPlatform = findViewById(R.id.button_platform)
_buttonRecommended = findViewById(R.id.button_recommended)
_monetization = findViewById(R.id.monetization);
_player.attachPlayer();
@@ -429,17 +440,26 @@ class VideoDetailView : ConstraintLayout {
_commentsList.onCommentsLoaded.subscribe { count ->
_commentsCount = count;
updateCommentType(false);
//TODO: Why is this here ? updateTabs(false);
};
_toggleCommentType.onValueChanged.subscribe {
updateCommentType(true);
};
if (StatePolycentric.instance.enabled) {
_buttonPolycentric.setOnClickListener {
setTabIndex(0);
StateMeta.instance.setLastCommentSection(0);
}
} else {
_buttonPolycentric.visibility = View.GONE
}
_textCommentType.setOnClickListener {
_toggleCommentType.setValue(!_toggleCommentType.value, true);
updateCommentType(true);
};
_buttonRecommended.setOnClickListener {
setTabIndex(2)
}
_buttonPlatform.setOnClickListener {
setTabIndex(1)
StateMeta.instance.setLastCommentSection(1);
}
val layoutTop: LinearLayout = findViewById(R.id.layout_top);
_container_content_main.removeView(layoutTop);
@@ -676,7 +696,7 @@ class VideoDetailView : ConstraintLayout {
if (c is PolycentricPlatformComment) {
var parentComment: PolycentricPlatformComment = c;
_container_content_replies.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference, c,
_container_content_replies.load(if (_tabIndex!! == 0) false else true, metadata, c.contextUrl, c.reference, c,
{ StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) },
{
val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1);
@@ -684,7 +704,7 @@ class VideoDetailView : ConstraintLayout {
parentComment = newComment;
});
} else {
_container_content_replies.load(_toggleCommentType.value, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
_container_content_replies.load(if (_tabIndex!! == 0) false else true, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
}
switchContentView(_container_content_replies);
};
@@ -1023,7 +1043,6 @@ class VideoDetailView : ConstraintLayout {
setDescription("".fixHtmlWhitespace());
_descriptionContainer.visibility = View.GONE;
_player.clear();
_textComments.visibility = View.INVISIBLE;
_commentsList.clear();
_lastVideoSource = null;
@@ -1047,7 +1066,7 @@ class VideoDetailView : ConstraintLayout {
setLastPositionMilliseconds(_videoResumePositionMilliseconds, false);
_addCommentView.setContext(null, null);
_toggleCommentType.setValue(false, false);
setTabIndex(0)
_commentsList.clear();
setEmpty();
@@ -1087,12 +1106,11 @@ class VideoDetailView : ConstraintLayout {
setLastPositionMilliseconds(_videoResumePositionMilliseconds, false);
_addCommentView.setContext(null, null);
_toggleCommentType.setValue(false, false);
setTabIndex(null)
_title.text = video.name;
_rating.visibility = View.GONE;
_layoutRating.visibility = View.GONE;
_textComments.visibility = View.VISIBLE;
_minimize_title.text = video.name;
_minimize_meta.text = video.author.name;
@@ -1277,13 +1295,19 @@ class VideoDetailView : ConstraintLayout {
_player.setMetadata(video.name, video.author.name);
if (video is TutorialFragment.TutorialVideo) {
_toggleCommentType.setValue(false, false);
setTabIndex(0, true)
} else {
_toggleCommentType.setValue(!Settings.instance.other.polycentricEnabled || Settings.instance.comments.defaultCommentSection == 1, false);
if (Settings.instance.comments.recommendationsDefault) {
setTabIndex(2)
} else {
when(Settings.instance.comments.defaultCommentSection) {
0 -> if(Settings.instance.other.polycentricEnabled) setTabIndex(0) else setTabIndex(1);
1 -> setTabIndex(1);
2 -> setTabIndex(StateMeta.instance.getLastCommentSection())
}
}
}
updateCommentType(true);
//UI
_title.text = video.name;
_channelName.text = video.author.name;
@@ -1477,13 +1501,13 @@ class VideoDetailView : ConstraintLayout {
_buttonMore.visibility = View.GONE
_buttonPins.visibility = View.GONE
_layoutRating.visibility = View.GONE
_layoutToggleCommentSection.visibility = View.GONE
_layoutChangeBottomSection.visibility = View.GONE
} else {
_buttonSubscribe.visibility = View.VISIBLE
_buttonMore.visibility = View.VISIBLE
_buttonPins.visibility = View.VISIBLE
_layoutRating.visibility = View.VISIBLE
_layoutToggleCommentSection.visibility = View.VISIBLE
_layoutChangeBottomSection.visibility = View.VISIBLE
}
}
fun loadLiveChat(video: IPlatformVideoDetails) {
@@ -2271,24 +2295,93 @@ class VideoDetailView : ConstraintLayout {
};
}
private fun updateCommentType(reloadComments: Boolean) {
if (_toggleCommentType.value) {
_textCommentType.text = "Platform";
_addCommentView.visibility = View.GONE;
private fun setTabIndex(index: Int?, forceReload: Boolean = false) {
Logger.i(TAG, "setTabIndex (index: ${index}, forceReload: ${forceReload})")
val changed = _tabIndex != index || forceReload
if (!changed) {
return
}
if (reloadComments) {
fetchComments();
}
} else {
_textCommentType.text = "Polycentric";
_addCommentView.visibility = View.VISIBLE;
_taskLoadRecommendations.cancel()
_tabIndex = index
_buttonRecommended.setTextColor(resources.getColor(if (index == 2) R.color.white else R.color.gray_ac))
_buttonPlatform.setTextColor(resources.getColor(if (index == 1) R.color.white else R.color.gray_ac))
_buttonPolycentric.setTextColor(resources.getColor(if (index == 0) R.color.white else R.color.gray_ac))
_layoutRecommended.removeAllViews()
if (reloadComments) {
fetchPolycentricComments()
if (index == null) {
_addCommentView.visibility = View.GONE
_commentsList.clear()
_layoutRecommended.visibility = View.GONE
} else if (index == 0) {
_addCommentView.visibility = View.VISIBLE
_layoutRecommended.visibility = View.GONE
fetchPolycentricComments()
} else if (index == 1) {
_addCommentView.visibility = View.VISIBLE
_layoutRecommended.visibility = View.GONE
fetchComments()
} else if (index == 2) {
_addCommentView.visibility = View.GONE
_layoutRecommended.visibility = View.VISIBLE
_commentsList.clear()
val url = _url
if (url != null) {
_layoutRecommended.addView(LoaderView(context).apply {
layoutParams = LinearLayout.LayoutParams(60.dp(resources), 60.dp(resources))
start()
})
_taskLoadRecommendations.run(url)
} else {
_layoutRecommended.addView(TextView(context).apply {
layoutParams = LinearLayout.LayoutParams(60.dp(resources), 60.dp(resources))
textSize = 12.0f
text = "No recommendations found"
})
}
}
}
private fun setRecommendations(pager: IPager<IPlatformContent>?, message: String? = null) {
_layoutRecommended.removeAllViews()
if (pager == null) {
_layoutRecommended.addView(TextView(context).apply {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply {
setMargins(20.dp(resources), 20.dp(resources), 20.dp(resources), 20.dp(resources))
}
textAlignment = TEXT_ALIGNMENT_CENTER
textSize = 14.0f
text = message
})
return
}
val results = pager.getResults().filter { it is IPlatformVideo }
for (result in results) {
_layoutRecommended.addView(PreviewVideoView(context, FeedStyle.THUMBNAIL, null, false).apply {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
bind(result)
hideAddTo()
onVideoClicked.subscribe { video, _ ->
fragment.navigate<VideoDetailFragment>(video).maximizeVideoDetail()
}
onChannelClicked.subscribe {
fragment.navigate<ChannelFragment>(it)
}
onAddToWatchLaterClicked.subscribe(this) {
if(it is IPlatformVideo) {
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it));
UIDialogs.toast("Added to watch later\n[${it.name}]");
}
}
})
}
}
//Picture2Picture
fun startPictureInPicture() {
@@ -2634,6 +2727,13 @@ class VideoDetailView : ConstraintLayout {
}
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
private val _taskLoadRecommendations = TaskHandler<String, IPager<IPlatformContent>?>(StateApp.instance.scopeGetter, { video?.getContentRecommendations(StatePlatform.instance.getContentClient(it)) })
.success { setRecommendations(it, "No recommendations found") }
.exception<Throwable> {
setRecommendations(null, it.message)
Logger.w(TAG, "Failed to load recommendations.", it);
};
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
.success { it -> setPolycentricProfile(it, animate = true) }
.exception<Throwable> {
@@ -14,6 +14,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.getSubdomainWildcardQuery
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.matchesDomain
import kotlinx.serialization.encodeToString
@@ -109,8 +110,9 @@ class LoginWebViewClient : WebViewClient {
//TODO: For now we assume cookies are legit for all subdomains of a top-level domain, this is the most common scenario anyway
val cookieString = CookieManager.getInstance().getCookie(request.url.toString());
if(cookieString != null) {
val domainParts = domain!!.split(".");
val cookieDomain = "." + domainParts.drop(domainParts.size - 2).joinToString(".");
//val domainParts = domain!!.split(".");
//val cookieDomain = "." + domainParts.drop(domainParts.size - 2).joinToString(".");
val cookieDomain = domain!!.getSubdomainWildcardQuery();
if(_pluginConfig == null || _pluginConfig.allowUrls.any { it == "everywhere" || it.lowercase().matchesDomain(cookieDomain) })
_authConfig.cookiesToFind?.let { cookiesToFind ->
val cookies = cookieString.split(";");
@@ -3,6 +3,7 @@ package com.futo.platformplayer.others
import android.net.Uri
import android.webkit.CookieManager
import android.webkit.WebResourceRequest
import com.futo.platformplayer.getSubdomainWildcardQuery
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.matchesDomain
@@ -64,8 +65,8 @@ class WebViewRequirementExtractor {
//TODO: For now we assume cookies are legit for all subdomains of a top-level domain, this is the most common scenario anyway
val cookieString = CookieManager.getInstance().getCookie(request.url.toString());
if(cookieString != null) {
val domainParts = domain!!.split(".");
val cookieDomain = "." + domainParts.drop(domainParts.size - 2).joinToString(".");
//val domainParts = domain!!.split(".");
val cookieDomain = domain!!.getSubdomainWildcardQuery()//"." + domainParts.drop(domainParts.size - 2).joinToString(".");
if(allowedUrls.any { it == "everywhere" || it.lowercase().matchesDomain(cookieDomain) })
cookiesToFind?.let { cookiesToFind ->
val cookies = cookieString.split(";");
@@ -183,10 +183,12 @@ class MediaPlaybackService : Service() {
Logger.v(TAG, "updateMediaSession");
var isUpdating = false;
val video: IPlatformVideo;
var lastBitmap: Bitmap? = null
if(videoUpdated == null) {
val notifLastVideo = _notif_last_video ?: return;
video = notifLastVideo;
isUpdating = true;
lastBitmap = _notif_last_bitmap;
}
else
video = videoUpdated;
@@ -199,6 +201,7 @@ class MediaPlaybackService : Service() {
.putString(MediaMetadata.METADATA_KEY_ARTIST, video.author.name)
.putString(MediaMetadata.METADATA_KEY_TITLE, video.name)
.putLong(MediaMetadata.METADATA_KEY_DURATION, video.duration * 1000)
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, lastBitmap)
.build());
val thumbnail = video.thumbnails.getHQThumbnail();
@@ -214,8 +217,16 @@ class MediaPlaybackService : Service() {
.load(thumbnail)
.into(object: CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap,transition: Transition<in Bitmap>?) {
if(tag == _notif_last_video)
if(tag == _notif_last_video) {
notifyMediaSession(video, resource)
_mediaSession?.setMetadata(
MediaMetadataCompat.Builder()
.putString(MediaMetadata.METADATA_KEY_ARTIST, video.author.name)
.putString(MediaMetadata.METADATA_KEY_TITLE, video.name)
.putLong(MediaMetadata.METADATA_KEY_DURATION, video.duration * 1000)
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, resource)
.build());
}
}
override fun onLoadCleared(placeholder: Drawable?) {
if(tag == _notif_last_video)
@@ -412,16 +423,17 @@ class MediaPlaybackService : Service() {
AudioManager.AUDIOFOCUS_GAIN -> {
_hasFocus = true;
_isTransientLoss = false;
Log.i(TAG, "Audio focus gained (restartPlaybackAfterLoss = ${Settings.instance.playback.restartPlaybackAfterLoss}, _audioFocusLossTime_ms = $_audioFocusLossTime_ms)");
val audioFocusLossDuration = _audioFocusLossTime_ms?.let { System.currentTimeMillis() - it }
_audioFocusLossTime_ms = null
Log.i(TAG, "Audio focus gained (restartPlaybackAfterLoss = ${Settings.instance.playback.restartPlaybackAfterLoss}, _audioFocusLossTime_ms = $_audioFocusLossTime_ms, audioFocusLossDuration = ${audioFocusLossDuration})");
if (Settings.instance.playback.restartPlaybackAfterLoss == 1) {
val lossTime_ms = _audioFocusLossTime_ms
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 10) {
if (audioFocusLossDuration != null && audioFocusLossDuration < 1000 * 10) {
MediaControlReceiver.onPlayReceived.emit()
}
} else if (Settings.instance.playback.restartPlaybackAfterLoss == 2) {
val lossTime_ms = _audioFocusLossTime_ms
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 30) {
if (audioFocusLossDuration != null && audioFocusLossDuration < 1000 * 30) {
MediaControlReceiver.onPlayReceived.emit()
}
} else if (Settings.instance.playback.restartPlaybackAfterLoss == 3) {
@@ -429,8 +441,10 @@ class MediaPlaybackService : Service() {
}
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
if (isPlaying) {
_audioFocusLossTime_ms = System.currentTimeMillis()
_audioFocusLossTime_ms = if (isPlaying) {
System.currentTimeMillis()
} else {
null
}
_hasFocus = false;
@@ -444,8 +458,10 @@ class MediaPlaybackService : Service() {
_isTransientLoss = true;
}
AudioManager.AUDIOFOCUS_LOSS -> {
if (isPlaying) {
_audioFocusLossTime_ms = System.currentTimeMillis()
_audioFocusLossTime_ms = if (isPlaying) {
System.currentTimeMillis()
} else {
null
}
MediaControlReceiver.onPauseReceived.emit();
@@ -612,6 +612,20 @@ class StateApp {
Settings.instance.didFirstStart = true;
Settings.instance.save();
}
/*
if(!Settings.instance.comments.didAskPolycentricDefault) {
UIDialogs.showDialog(context, R.drawable.neopass, "Default Comment Section", "Grayjay supports 2 comment sections, the Platform comments and Polycentric comments. You can easily toggle between them, but which would you like to be selected by default? This choice can be changed in settings.\n\nPolycentric is still under active development.", null, 1,
UIDialogs.Action("Polycentric", {
Settings.instance.comments.didAskPolycentricDefault = true;
Settings.instance.comments.defaultCommentSection = 0;
Settings.instance.save();
}, UIDialogs.ActionStyle.PRIMARY, true),
UIDialogs.Action("Platform", {
Settings.instance.comments.didAskPolycentricDefault = true;
Settings.instance.comments.defaultCommentSection = 1;
Settings.instance.save();
}, UIDialogs.ActionStyle.PRIMARY, true))
}*/
if(Settings.instance.backup.shouldAutomaticBackup()) {
try {
StateBackup.startAutomaticBackup();
@@ -627,21 +641,26 @@ class StateApp {
fun scheduleBackgroundWork(context: Context, active: Boolean = true, intervalMinutes: Int = 60 * 12) {
val wm = WorkManager.getInstance(context);
try {
val wm = WorkManager.getInstance(context);
if(active) {
if(BuildConfig.DEBUG)
UIDialogs.toast(context, "Scheduling background every ${intervalMinutes} minutes");
if(active) {
if(BuildConfig.DEBUG)
UIDialogs.toast(context, "Scheduling background every ${intervalMinutes} minutes");
val req = PeriodicWorkRequest.Builder(BackgroundWorker::class.java, intervalMinutes.toLong(), TimeUnit.MINUTES, 5, TimeUnit.MINUTES)
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.build())
.build();
wm.enqueueUniquePeriodicWork("backgroundSubscriptions", ExistingPeriodicWorkPolicy.UPDATE, req);
val req = PeriodicWorkRequest.Builder(BackgroundWorker::class.java, intervalMinutes.toLong(), TimeUnit.MINUTES, 5, TimeUnit.MINUTES)
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.build())
.build();
wm.enqueueUniquePeriodicWork("backgroundSubscriptions", ExistingPeriodicWorkPolicy.UPDATE, req);
}
else
wm.cancelAllWork();
} catch (e: Throwable) {
Logger.e(TAG, "Failed to schedule background subscription updates.", e)
UIDialogs.toast(context, "Background subscription update failed: " + e.message)
}
else
wm.cancelAllWork();
}
@@ -64,8 +64,20 @@ class StateCache {
Logger.i(TAG, "Subscriptions CachePager get pagers");
val pagers: List<IPager<IPlatformContent>>;
val splitAmount = 900;
val timeCacheRetrieving = measureTimeMillis {
pagers = listOf(getAllChannelCachePager(allUrls));
if(allUrls.size > splitAmount) {
var done = 0;
var subsetPagers = mutableListOf<IPager<IPlatformContent>>();
while(done < allUrls.size) {
val subsetUrls = allUrls.subList(done, Math.min(allUrls.size - 1, done + splitAmount));
subsetPagers.add(getAllChannelCachePager(subsetUrls));
done += splitAmount;
}
pagers = subsetPagers;
}
else
pagers = listOf(getAllChannelCachePager(allUrls));
}
Logger.i(TAG, "Subscriptions CachePager compiling (retrieved in ${timeCacheRetrieving}ms)");
@@ -2,11 +2,29 @@ package com.futo.platformplayer.states
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringHashSetStorage
import com.futo.platformplayer.stores.StringStorage
class StateMeta {
val hiddenVideos = FragmentedStorage.get<StringHashSetStorage>("hiddenVideos");
val hiddenCreators = FragmentedStorage.get<StringHashSetStorage>("hiddenCreators");
val lastCommentSection = FragmentedStorage.get<StringStorage>("defaultCommentSection");
fun getLastCommentSection(): Int{
return when(lastCommentSection.value){
"Polycentric" -> 0;
"Platform" -> 1;
else -> 1
}
}
fun setLastCommentSection(value: Int) {
when(value) {
0 -> lastCommentSection.setAndSave("Polycentric");
1 -> lastCommentSection.setAndSave("Platform");
else -> lastCommentSection.setAndSave("");
}
}
fun isVideoHidden(videoUrl: String) : Boolean {
return hiddenVideos.contains(videoUrl);
}
@@ -209,7 +209,17 @@ class StatePlatform {
}
if(_availableClients.distinctBy { it.id }.count() < _availableClients.size) {
throw IllegalStateException("Attempted to add 2 clients with the same ID");
val dups = _availableClients.filter { x-> _availableClients.count { it.id == x.id } > 1 };
val overrideClients = _availableClients.distinctBy { it.id }
_availableClients.clear();
_availableClients.addAll(overrideClients);
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
UIDialogs.showDialog(context, R.drawable.ic_error_pred, "Duplicate plugin ids detected", "This can cause unexpected behavior, ideally uninstall duplicate plugins (ids)",
dups.map { it.name }.joinToString("\n"), 0, UIDialogs.Action("Ok", { }));
}
//throw IllegalStateException("Attempted to add 2 clients with the same ID");
}
enabled = _enabledClientsPersistent.getAllValues()
@@ -12,10 +12,10 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
import com.futo.platformplayer.stores.v2.StoreSerializer
import kotlinx.serialization.KSerializer
import java.lang.IllegalArgumentException
import java.lang.reflect.Field
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import kotlin.IllegalArgumentException
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.KType
@@ -318,8 +318,12 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
});
}
private val inLimit = 990;
fun <X> queryInPager(field: KProperty<*>, obj: List<String>, pageSize: Int, convert: (I)->X): IPager<X> = queryInPager(validateFieldName(field), obj, pageSize, convert);
fun <X> queryInPager(field: String, obj: List<String>, pageSize: Int, convert: (I)->X): IPager<X> {
if(obj.size > inLimit) {
throw IllegalArgumentException("Too many objects requested (IN query), create subqueries of ${inLimit}");
}
return AdhocPager({
queryInPage(field, obj, it - 1, pageSize).map(convert);
});
@@ -130,6 +130,11 @@ open class PreviewVideoView : LinearLayout {
_button_add_to_watch_later.setOnClickListener { currentVideo?.let { onAddToWatchLaterClicked.emit(it); } }
}
fun hideAddTo() {
_button_add_to.visibility = View.GONE
_button_add_to_queue.visibility = View.GONE
}
protected open fun inflate(feedStyle: FeedStyle) {
inflate(context, when(feedStyle) {
FeedStyle.PREVIEW -> R.layout.list_video_preview
@@ -165,11 +170,18 @@ open class PreviewVideoView : LinearLayout {
_imageNeopassChannel?.visibility = View.GONE;
_creatorThumbnail?.setThumbnail(content.author.thumbnail, false);
_imageChannel?.let {
Glide.with(_imageChannel)
.load(content.author.thumbnail)
.placeholder(R.drawable.placeholder_channel_thumbnail)
.into(_imageChannel);
val thumbnail = content.author.thumbnail
if (thumbnail != null) {
_imageChannel?.visibility = View.VISIBLE
_imageChannel?.let {
Glide.with(_imageChannel)
.load(content.author.thumbnail)
.placeholder(R.drawable.placeholder_channel_thumbnail)
.into(_imageChannel);
}
} else {
_imageChannel?.visibility = View.GONE
}
_textChannelName.text = content.author.name
@@ -254,6 +254,7 @@ class CommentsList : ConstraintLayout {
fun clear() {
cancel();
setLoading(false);
_comments.clear();
_commentsPager = null;
_adapterComments.notifyDataSetChanged();
@@ -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
+3
View File
@@ -412,6 +412,8 @@
<string name="preferred_preview_quality_description">Default quality while previewing a video in a feed</string>
<string name="primary_language">Primary Language</string>
<string name="default_comment_section">Default Comment Section</string>
<string name="default_recommendations">Recommendations as Default</string>
<string name="default_recommendations_description">Show recommendations as default, instead of comments.</string>
<string name="bad_reputation_comments_fading">Bad Reputation Comment Fading</string>
<string name="bad_reputation_comments_fading_description">If comments with a very bad reputation should be faded. Disabling may worsen experience.</string>
<string name="reinstall_embedded_plugins">Reinstall Embedded Plugins</string>
@@ -914,6 +916,7 @@
<string-array name="comment_sections">
<item>Polycentric</item>
<item>Platform</item>
<item>Last Selected</item>
</string-array>
<string-array name="audio_languages">
<item>English</item>
@@ -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() {