mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Merge branch 'master' into ts/polycentric-moderation
This commit is contained in:
@@ -156,6 +156,7 @@ android {
|
||||
dependencies {
|
||||
implementation 'com.google.dagger:dagger:2.48'
|
||||
implementation 'androidx.test:monitor:1.7.2'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
||||
|
||||
//Core
|
||||
|
||||
@@ -251,6 +251,9 @@ class PlatformVideo extends PlatformContent {
|
||||
this.duration = obj.duration ?? -1; //Long
|
||||
this.viewCount = obj.viewCount ?? -1; //Long
|
||||
|
||||
this.playbackTime = obj.playbackTime ?? -1;
|
||||
this.playbackDate = obj.playbackDate ?? undefined;
|
||||
|
||||
this.isLive = obj.isLive ?? false; //Boolean
|
||||
this.isShort = !!obj.isShort ?? false;
|
||||
}
|
||||
@@ -785,6 +788,7 @@ let plugin = {
|
||||
//To override by plugin
|
||||
const source = {
|
||||
getHome() { return new ContentPager([], false, {}); },
|
||||
getShorts() { return new VideoPager([], false, {}); },
|
||||
|
||||
enable(config){ },
|
||||
disable() {},
|
||||
|
||||
@@ -424,7 +424,7 @@ class UIDialogs {
|
||||
}
|
||||
|
||||
|
||||
fun showCastingDialog(context: Context) {
|
||||
fun showCastingDialog(context: Context, ownerActivity: Activity? = null) {
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
if (d != null) {
|
||||
val dialog = ConnectedCastingDialog(context);
|
||||
@@ -432,6 +432,7 @@ class UIDialogs {
|
||||
dialog.setOwnerActivity(context)
|
||||
}
|
||||
registerDialogOpened(dialog);
|
||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
} else {
|
||||
@@ -444,21 +445,24 @@ class UIDialogs {
|
||||
if (c is Activity) {
|
||||
dialog.setOwnerActivity(c);
|
||||
}
|
||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
fun showCastingTutorialDialog(context: Context) {
|
||||
fun showCastingTutorialDialog(context: Context, ownerActivity: Activity? = null) {
|
||||
val dialog = CastingHelpDialog(context);
|
||||
registerDialogOpened(dialog);
|
||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
fun showCastingAddDialog(context: Context) {
|
||||
fun showCastingAddDialog(context: Context, ownerActivity: Activity? = null) {
|
||||
val dialog = CastingAddDialog(context);
|
||||
registerDialogOpened(dialog);
|
||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@@ -129,14 +129,23 @@ class UISlideOverlays {
|
||||
val originalVideo = subscription.doFetchVideos;
|
||||
val originalPosts = subscription.doFetchPosts;
|
||||
|
||||
val menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, listOf());
|
||||
val menu = SlideUpMenuOverlay(
|
||||
container.context,
|
||||
container,
|
||||
"Subscription Settings",
|
||||
null,
|
||||
true,
|
||||
listOf()
|
||||
);
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
|
||||
val capabilities = plugin.getChannelCapabilities();
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
items.addAll(listOf(
|
||||
items.addAll(
|
||||
listOf(
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_notifications,
|
||||
@@ -144,24 +153,43 @@ class UISlideOverlays {
|
||||
"",
|
||||
tag = "notifications",
|
||||
call = {
|
||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||
subscription.doNotifications =
|
||||
menu?.selectOption(null, "notifications", true, true)
|
||||
?: subscription.doNotifications;
|
||||
},
|
||||
invokeParent = false
|
||||
),
|
||||
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||
SlideUpMenuGroup(container.context, "Subscription Groups",
|
||||
if (StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||
.isNotEmpty()
|
||||
)
|
||||
SlideUpMenuGroup(
|
||||
container.context, "Subscription Groups",
|
||||
"You can select which groups this subscription is part of.",
|
||||
-1, listOf()) else null,
|
||||
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||
-1, listOf()
|
||||
) else null,
|
||||
if (StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||
.isNotEmpty()
|
||||
)
|
||||
SlideUpMenuRecycler(container.context, "as") {
|
||||
val groups = ArrayList<SubscriptionGroup>(StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
|
||||
val groups =
|
||||
ArrayList<SubscriptionGroup>(
|
||||
StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||
.map {
|
||||
SubscriptionGroup.Selectable(
|
||||
it,
|
||||
it.urls.contains(subscription.channel.url)
|
||||
)
|
||||
}
|
||||
.sortedBy { !it.selected });
|
||||
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? = null;
|
||||
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? =
|
||||
null;
|
||||
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
|
||||
it.onClick.subscribe {
|
||||
if (it is SubscriptionGroup.Selectable) {
|
||||
val actualGroup = StateSubscriptionGroups.instance.getSubscriptionGroup(it.id)
|
||||
val actualGroup =
|
||||
StateSubscriptionGroups.instance.getSubscriptionGroup(
|
||||
it.id
|
||||
)
|
||||
?: return@subscribe;
|
||||
groups.clear();
|
||||
if (it.selected)
|
||||
@@ -169,9 +197,17 @@ class UISlideOverlays {
|
||||
else
|
||||
actualGroup.urls.add(subscription.channel.url);
|
||||
|
||||
StateSubscriptionGroups.instance.updateSubscriptionGroup(actualGroup);
|
||||
groups.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
|
||||
StateSubscriptionGroups.instance.updateSubscriptionGroup(
|
||||
actualGroup
|
||||
);
|
||||
groups.addAll(
|
||||
StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||
.map {
|
||||
SubscriptionGroup.Selectable(
|
||||
it,
|
||||
it.urls.contains(subscription.channel.url)
|
||||
)
|
||||
}
|
||||
.sortedBy { !it.selected });
|
||||
adapter?.notifyContentChanged();
|
||||
}
|
||||
@@ -179,9 +215,11 @@ class UISlideOverlays {
|
||||
};
|
||||
return@SlideUpMenuRecycler adapter;
|
||||
} else null,
|
||||
SlideUpMenuGroup(container.context, "Fetch Settings",
|
||||
SlideUpMenuGroup(
|
||||
container.context, "Fetch Settings",
|
||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||
-1, listOf()),
|
||||
-1, listOf()
|
||||
),
|
||||
if (capabilities.hasType(ResultCapabilities.TYPE_LIVE)) SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_live_tv,
|
||||
@@ -189,7 +227,9 @@ class UISlideOverlays {
|
||||
"Check for livestreams",
|
||||
tag = "fetchLive",
|
||||
call = {
|
||||
subscription.doFetchLive = menu?.selectOption(null, "fetchLive", true, true) ?: subscription.doFetchLive;
|
||||
subscription.doFetchLive =
|
||||
menu?.selectOption(null, "fetchLive", true, true)
|
||||
?: subscription.doFetchLive;
|
||||
},
|
||||
invokeParent = false
|
||||
) else null,
|
||||
@@ -200,7 +240,9 @@ class UISlideOverlays {
|
||||
"Check for streams",
|
||||
tag = "fetchStreams",
|
||||
call = {
|
||||
subscription.doFetchStreams = menu?.selectOption(null, "fetchStreams", true, true) ?: subscription.doFetchStreams;
|
||||
subscription.doFetchStreams =
|
||||
menu?.selectOption(null, "fetchStreams", true, true)
|
||||
?: subscription.doFetchStreams;
|
||||
},
|
||||
invokeParent = false
|
||||
) else null,
|
||||
@@ -212,7 +254,9 @@ class UISlideOverlays {
|
||||
"Check for videos",
|
||||
tag = "fetchVideos",
|
||||
call = {
|
||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||
subscription.doFetchVideos =
|
||||
menu?.selectOption(null, "fetchVideos", true, true)
|
||||
?: subscription.doFetchVideos;
|
||||
},
|
||||
invokeParent = false
|
||||
) else if (capabilities.hasType(ResultCapabilities.TYPE_MIXED) || capabilities.types.isEmpty())
|
||||
@@ -223,7 +267,9 @@ class UISlideOverlays {
|
||||
"Check for content",
|
||||
tag = "fetchVideos",
|
||||
call = {
|
||||
subscription.doFetchVideos = menu?.selectOption(null, "fetchVideos", true, true) ?: subscription.doFetchVideos;
|
||||
subscription.doFetchVideos =
|
||||
menu?.selectOption(null, "fetchVideos", true, true)
|
||||
?: subscription.doFetchVideos;
|
||||
},
|
||||
invokeParent = false
|
||||
) else null,
|
||||
@@ -234,7 +280,9 @@ class UISlideOverlays {
|
||||
"Check for posts",
|
||||
tag = "fetchPosts",
|
||||
call = {
|
||||
subscription.doFetchPosts = menu?.selectOption(null, "fetchPosts", true, true) ?: subscription.doFetchPosts;
|
||||
subscription.doFetchPosts =
|
||||
menu?.selectOption(null, "fetchPosts", true, true)
|
||||
?: subscription.doFetchPosts;
|
||||
},
|
||||
invokeParent = false
|
||||
) else null/*,,
|
||||
@@ -245,7 +293,8 @@ class UISlideOverlays {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_list, "Add to Group", "", "btnAddToGroup", {
|
||||
showCreateSubscriptionGroup(container, subscription.channel);
|
||||
}, false)*/
|
||||
).filterNotNull());
|
||||
).filterNotNull()
|
||||
);
|
||||
|
||||
menu.setItems(items);
|
||||
|
||||
@@ -267,22 +316,39 @@ class UISlideOverlays {
|
||||
if (subscription.doNotifications && !originalNotif) {
|
||||
val mainContext = StateApp.instance.contextOrNull;
|
||||
if (Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
|
||||
UIDialogs.toast(container.context, "Enable 'Background Update' in settings for notifications to work");
|
||||
UIDialogs.toast(
|
||||
container.context,
|
||||
"Enable 'Background Update' in settings for notifications to work"
|
||||
);
|
||||
|
||||
if (mainContext is MainActivity) {
|
||||
UIDialogs.showDialog(mainContext, R.drawable.ic_settings, "Background Updating Required",
|
||||
"You need to set a Background Updating interval for notifications", null, 0,
|
||||
UIDialogs.showDialog(
|
||||
mainContext,
|
||||
R.drawable.ic_settings,
|
||||
"Background Updating Required",
|
||||
"You need to set a Background Updating interval for notifications",
|
||||
null,
|
||||
0,
|
||||
UIDialogs.Action("Cancel", {}),
|
||||
UIDialogs.Action("Configure", {
|
||||
val intent = Intent(mainContext, SettingsActivity::class.java);
|
||||
intent.putExtra("query", mainContext.getString(R.string.background_update));
|
||||
val intent = Intent(
|
||||
mainContext,
|
||||
SettingsActivity::class.java
|
||||
);
|
||||
intent.putExtra(
|
||||
"query",
|
||||
mainContext.getString(R.string.background_update)
|
||||
);
|
||||
mainContext.startActivity(intent);
|
||||
}, UIDialogs.ActionStyle.PRIMARY));
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
}
|
||||
return@subscribe;
|
||||
}
|
||||
else if(!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
|
||||
UIDialogs.toast(container.context, "Android notifications are disabled");
|
||||
} else if (!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
|
||||
UIDialogs.toast(
|
||||
container.context,
|
||||
"Android notifications are disabled"
|
||||
);
|
||||
if (mainContext is MainActivity) {
|
||||
mainContext.requestNotificationPermissions("Notifications are required for subscription updating and notifications to work");
|
||||
}
|
||||
@@ -301,6 +367,9 @@ class UISlideOverlays {
|
||||
|
||||
menu.show();
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to show subscription overlay.", e)
|
||||
}
|
||||
}
|
||||
|
||||
return menu;
|
||||
|
||||
@@ -62,6 +62,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsF
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ShortsFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
||||
@@ -169,6 +170,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment;
|
||||
lateinit var _fragWatchlist: WatchLaterFragment;
|
||||
lateinit var _fragHistory: HistoryFragment;
|
||||
lateinit var _fragShorts: ShortsFragment;
|
||||
lateinit var _fragSourceDetail: SourceDetailFragment;
|
||||
lateinit var _fragDownloads: DownloadsFragment;
|
||||
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
|
||||
@@ -338,6 +340,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
_fragWebDetail = WebDetailFragment.newInstance();
|
||||
_fragWatchlist = WatchLaterFragment.newInstance();
|
||||
_fragHistory = HistoryFragment.newInstance();
|
||||
_fragShorts = ShortsFragment.newInstance();
|
||||
_fragSourceDetail = SourceDetailFragment.newInstance();
|
||||
_fragDownloads = DownloadsFragment();
|
||||
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
|
||||
@@ -1253,6 +1256,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
WebDetailFragment::class -> _fragWebDetail as T;
|
||||
WatchLaterFragment::class -> _fragWatchlist as T;
|
||||
HistoryFragment::class -> _fragHistory as T;
|
||||
ShortsFragment::class -> _fragShorts as T;
|
||||
SourceDetailFragment::class -> _fragSourceDetail as T;
|
||||
DownloadsFragment::class -> _fragDownloads as T;
|
||||
ImportSubscriptionsFragment::class -> _fragImportSubscriptions as T;
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.models.ImageVariable
|
||||
|
||||
@@ -36,6 +37,11 @@ interface IPlatformClient {
|
||||
*/
|
||||
fun getHome(): IPager<IPlatformContent>
|
||||
|
||||
/**
|
||||
* Gets the shorts feed
|
||||
*/
|
||||
fun getShorts(): IPager<IPlatformVideo>
|
||||
|
||||
//Search
|
||||
/**
|
||||
* Gets search suggestion for the provided query string
|
||||
@@ -176,6 +182,10 @@ interface IPlatformClient {
|
||||
* Retrieves the subscriptions of the currently logged in user
|
||||
*/
|
||||
fun getUserSubscriptions(): Array<String>;
|
||||
/**
|
||||
* Retrieves the history of the currently logged in user
|
||||
*/
|
||||
fun getUserHistory(): IPager<IPlatformContent>;
|
||||
|
||||
|
||||
fun isClaimTypeSupported(claimType: Int): Boolean;
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||
import com.futo.platformplayer.api.media.models.live.LiveEventComment
|
||||
import com.futo.platformplayer.api.media.models.live.LiveEventEmojis
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVODEventPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.BatchedTaskHandler
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -26,12 +27,17 @@ class LiveChatManager {
|
||||
private val _emojiCache: EmojiCache = EmojiCache();
|
||||
private val _pager: IPager<IPlatformLiveEvent>?;
|
||||
|
||||
private var _position: Long = 0;
|
||||
private var _eventsPosition: Long = 0;
|
||||
|
||||
private val _history: ArrayList<IPlatformLiveEvent> = arrayListOf();
|
||||
|
||||
private var _startCounter = 0;
|
||||
|
||||
private val _followers: HashMap<Any, (List<IPlatformLiveEvent>) -> Unit> = hashMapOf();
|
||||
|
||||
val isVOD get() = _pager is JSVODEventPager;
|
||||
|
||||
var viewCount: Long = 0
|
||||
private set;
|
||||
|
||||
@@ -39,7 +45,23 @@ class LiveChatManager {
|
||||
_scope = scope;
|
||||
_pager = pager;
|
||||
viewCount = initialViewCount;
|
||||
if(pager is JSVODEventPager)
|
||||
handleEvents(listOf(LiveEventComment("SYSTEM", null, "VOD chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
|
||||
else
|
||||
handleEvents(listOf(LiveEventComment("SYSTEM", null, "Live chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n")));
|
||||
|
||||
if(pager is JSVODEventPager) {
|
||||
var replayResults = pager.getResults().filter { it.time > _eventsPosition || it is LiveEventEmojis };
|
||||
//TODO: Remove this once dripfeed is done properly
|
||||
replayResults = replayResults.filter{ it.time < _eventsPosition + 1500 || it is LiveEventEmojis };
|
||||
if(replayResults.size > 0) {
|
||||
_eventsPosition = replayResults.maxOf { it.time };
|
||||
Logger.i(TAG, "VOD Events last event: " + _eventsPosition);
|
||||
}
|
||||
else
|
||||
_eventsPosition = _eventsPosition + 1500;
|
||||
}
|
||||
else
|
||||
handleEvents(pager.getResults());
|
||||
}
|
||||
|
||||
@@ -52,6 +74,10 @@ class LiveChatManager {
|
||||
_startCounter++;
|
||||
}
|
||||
|
||||
fun setVideoPosition(ms: Long) {
|
||||
_position = ms;
|
||||
}
|
||||
|
||||
fun getHistory(): List<IPlatformLiveEvent> {
|
||||
synchronized(_history) {
|
||||
return _history.toList();
|
||||
@@ -85,13 +111,36 @@ class LiveChatManager {
|
||||
try {
|
||||
while(_startCounter == counter) {
|
||||
var nextInterval = 1000L;
|
||||
if(_pager is JSVODEventPager && _eventsPosition > _position) {
|
||||
delay(500);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if(_pager == null || !_pager.hasMorePages())
|
||||
return@launch;
|
||||
val newEvents = if(_pager is JSVODEventPager) {
|
||||
val requestPosition = _position;
|
||||
_pager.nextPage(requestPosition.toInt());
|
||||
var replayResults = _pager.getResults().filter { it.time > requestPosition || it is LiveEventEmojis };
|
||||
//TODO: Remove this once dripfeed is done properly
|
||||
replayResults = replayResults.filter{ it.time < requestPosition + 1500 || it is LiveEventEmojis };
|
||||
if(replayResults.size > 0) {
|
||||
_eventsPosition = replayResults.maxOf { it.time };
|
||||
Logger.i(TAG, "VOD Events last event: " + _eventsPosition);
|
||||
}
|
||||
else
|
||||
_eventsPosition = requestPosition + _pager.nextRequest.coerceAtLeast(800).toLong();
|
||||
replayResults;
|
||||
}
|
||||
else {
|
||||
_pager.nextPage();
|
||||
val newEvents = _pager.getResults();
|
||||
_pager.getResults();
|
||||
}
|
||||
if(_pager is JSLiveEventPager)
|
||||
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
|
||||
else if(_pager is JSVODEventPager)
|
||||
nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong();
|
||||
|
||||
if(newEvents.size > 0)
|
||||
Logger.i(TAG, "New Live Events (${newEvents.size}) [${newEvents.map { it.type.name }.joinToString(", ")}]");
|
||||
|
||||
@@ -20,7 +20,8 @@ data class PlatformClientCapabilities(
|
||||
val hasGetContentChapters: Boolean = false,
|
||||
val hasPeekChannelContents: Boolean = false,
|
||||
val hasGetChannelPlaylists: Boolean = false,
|
||||
val hasGetContentRecommendations: Boolean = false
|
||||
val hasGetContentRecommendations: Boolean = false,
|
||||
val hasGetUserHistory: Boolean = false
|
||||
) {
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import com.futo.platformplayer.getOrThrow
|
||||
|
||||
interface IPlatformLiveEvent {
|
||||
val type : LiveEventType;
|
||||
var time: Long;
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -18,12 +18,15 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
|
||||
val colorName: String?;
|
||||
val badges: List<String>;
|
||||
|
||||
constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List<String>? = null) {
|
||||
override var time: Long = -1;
|
||||
|
||||
constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List<String>? = null, time: Long = -1) {
|
||||
this.name = name;
|
||||
this.message = message;
|
||||
this.thumbnail = thumbnail;
|
||||
this.colorName = colorName;
|
||||
this.badges = badges ?: listOf();
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -39,7 +42,8 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage {
|
||||
obj.getOrThrow(config, "name", contextName),
|
||||
obj.getOrThrow(config, "thumbnail", contextName, true),
|
||||
obj.getOrThrow(config, "message", contextName),
|
||||
colorName, badges);
|
||||
colorName, badges,
|
||||
obj.getOrDefault(config, "time", contextName, -1) ?: -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage {
|
||||
|
||||
var expire: Int = 6000;
|
||||
|
||||
override var time: Long = -1;
|
||||
|
||||
|
||||
constructor(name: String, thumbnail: String?, message: String, amount: String, expire: Int = 6000, colorDonation: String? = null) {
|
||||
this.name = name;
|
||||
|
||||
@@ -10,6 +10,8 @@ class LiveEventEmojis: IPlatformLiveEvent {
|
||||
|
||||
val emojis: HashMap<String, String>;
|
||||
|
||||
override var time: Long = -1;
|
||||
|
||||
constructor(emojis: HashMap<String, String>) {
|
||||
this.emojis = emojis;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ class LiveEventRaid: IPlatformLiveEvent {
|
||||
val targetUrl: String;
|
||||
val isOutgoing: Boolean;
|
||||
|
||||
override var time: Long = -1;
|
||||
|
||||
constructor(name: String, url: String, thumbnail: String, isOutgoing: Boolean) {
|
||||
this.targetName = name;
|
||||
this.targetUrl = url;
|
||||
|
||||
@@ -10,6 +10,8 @@ class LiveEventViewCount: IPlatformLiveEvent {
|
||||
|
||||
val viewCount: Int;
|
||||
|
||||
override var time: Long = -1;
|
||||
|
||||
constructor(viewCount: Int) {
|
||||
this.viewCount = viewCount;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.video
|
||||
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
/**
|
||||
* A search result representing a video (overview data)
|
||||
@@ -12,6 +13,9 @@ interface IPlatformVideo : IPlatformContent {
|
||||
val duration: Long;
|
||||
val viewCount: Long;
|
||||
|
||||
val playbackTime: Long;
|
||||
val playbackDate: OffsetDateTime?;
|
||||
|
||||
val isLive : Boolean;
|
||||
|
||||
val isShort: Boolean;
|
||||
|
||||
+5
-3
@@ -6,8 +6,6 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import com.futo.polycentric.core.combineHashCodes
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
@@ -18,7 +16,7 @@ open class SerializedPlatformVideo(
|
||||
override val contentType: ContentType = ContentType.MEDIA,
|
||||
override val id: PlatformID,
|
||||
override val name: String,
|
||||
override val thumbnails: Thumbnails,
|
||||
override val thumbnails: Thumbnails = Thumbnails(),
|
||||
override val author: PlatformAuthorLink,
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
@JsonNames("datetime", "dateTime")
|
||||
@@ -33,6 +31,10 @@ open class SerializedPlatformVideo(
|
||||
|
||||
override val isLive: Boolean = false;
|
||||
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
|
||||
override fun toJson() : String {
|
||||
return Json.encodeToString(this);
|
||||
}
|
||||
|
||||
+4
-1
@@ -13,7 +13,6 @@ import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.OffsetDateTime
|
||||
@@ -43,6 +42,10 @@ open class SerializedPlatformVideoDetails(
|
||||
) : IPlatformVideo, IPlatformVideoDetails {
|
||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
|
||||
override val isLive: Boolean get() = false;
|
||||
|
||||
override val dash: IDashManifestSource? get() = null;
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
|
||||
@@ -43,6 +44,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistPager
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoPager
|
||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
@@ -124,6 +126,7 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
val enableInSearch get() = descriptor.appSettings.tabEnabled.enableSearch ?: true
|
||||
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
|
||||
val enableInShorts get() = descriptor.appSettings.tabEnabled.enableShorts ?: true
|
||||
|
||||
fun getSubscriptionRateLimit(): Int? {
|
||||
val pluginRateLimit = config.subscriptionRateLimit;
|
||||
@@ -269,7 +272,8 @@ open class JSClient : IPlatformClient {
|
||||
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
||||
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
|
||||
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false,
|
||||
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false
|
||||
hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false,
|
||||
hasGetUserHistory = plugin.executeBoolean("!!source.getUserHistory") ?: false
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -328,6 +332,13 @@ open class JSClient : IPlatformClient {
|
||||
plugin.executeTyped("source.getHome()"));
|
||||
}
|
||||
|
||||
@JSDocs(2, "source.getShorts()", "Gets the Shorts feed of the platform")
|
||||
override fun getShorts(): IPager<IPlatformVideo> = isBusyWith("getShorts") {
|
||||
ensureEnabled()
|
||||
return@isBusyWith JSVideoPager(config, this,
|
||||
plugin.executeTyped("source.getShorts()"))
|
||||
}
|
||||
|
||||
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
|
||||
@JSDocsParameter("query", "Query to complete suggestions for")
|
||||
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
|
||||
@@ -702,6 +713,13 @@ open class JSClient : IPlatformClient {
|
||||
.toTypedArray();
|
||||
}
|
||||
|
||||
@JSOptional
|
||||
@JSDocs(23, "source.getUserHistory()", "Gets the history of the current user")
|
||||
override fun getUserHistory(): IPager<IPlatformContent> {
|
||||
ensureEnabled();
|
||||
return JSContentPager(config, this, plugin.executeTyped("source.getUserHistory()"));
|
||||
}
|
||||
|
||||
fun validate() {
|
||||
try {
|
||||
plugin.start();
|
||||
|
||||
@@ -48,6 +48,7 @@ class SourcePluginConfig(
|
||||
var subscriptionRateLimit: Int? = null,
|
||||
var enableInSearch: Boolean = true,
|
||||
var enableInHome: Boolean = true,
|
||||
var enableInShorts: Boolean = true,
|
||||
var supportedClaimTypes: List<Int> = listOf(),
|
||||
var primaryClaimFieldType: Int? = null,
|
||||
var developerSubmitUrl: String? = null,
|
||||
|
||||
+33
-2
@@ -5,10 +5,16 @@ import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.AnnouncementType
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.views.fields.DropdownFieldOptions
|
||||
import com.futo.platformplayer.views.fields.FieldForm
|
||||
import com.futo.platformplayer.views.fields.FormField
|
||||
import com.futo.platformplayer.views.fields.FormFieldButton
|
||||
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@@ -103,12 +109,35 @@ class SourcePluginDescriptor {
|
||||
@FormField(R.string.home, FieldForm.TOGGLE, R.string.show_content_in_home_tab, 1)
|
||||
var enableHome: Boolean? = null;
|
||||
|
||||
|
||||
@FormField(R.string.search, FieldForm.TOGGLE, R.string.show_content_in_search_results, 2)
|
||||
var enableSearch: Boolean? = null;
|
||||
|
||||
@FormField(R.string.shorts, FieldForm.TOGGLE, R.string.show_content_in_shorts_tab, 3)
|
||||
var enableShorts: Boolean? = null;
|
||||
}
|
||||
|
||||
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 3)
|
||||
@FormField(R.string.sync, "group", R.string.sync_desc, 3)
|
||||
var sync = Sync();
|
||||
@Serializable
|
||||
class Sync {
|
||||
@FormField(R.string.sync_history, FieldForm.TOGGLE, R.string.sync_history_desc, 1)
|
||||
var enableHistorySync: Boolean? = null;
|
||||
|
||||
@FormField(R.string.sync_history, FieldForm.BUTTON, R.string.sync_history_desc, 2)
|
||||
@FormFieldButton()
|
||||
fun syncHistoryNow() {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
val clients = StatePlatform.instance.getEnabledClients();
|
||||
for (client in clients) {
|
||||
if (client is JSClient) {//) && client.descriptor.appSettings.sync.enableHistorySync == true) {
|
||||
StateHistory.instance.syncRemoteHistory(client);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 4)
|
||||
var rateLimit = RateLimit();
|
||||
@Serializable
|
||||
class RateLimit {
|
||||
@@ -143,6 +172,8 @@ class SourcePluginDescriptor {
|
||||
tabEnabled.enableHome = config.enableInHome
|
||||
if(tabEnabled.enableSearch == null)
|
||||
tabEnabled.enableSearch = config.enableInSearch
|
||||
if(tabEnabled.enableShorts == null)
|
||||
tabEnabled.enableShorts = config.enableInShorts
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ abstract class JSPager<T> : IPager<T> {
|
||||
protected var pager: V8ValueObject;
|
||||
|
||||
private var _lastResults: List<T>? = null;
|
||||
private var _resultChanged: Boolean = true;
|
||||
private var _hasMorePages: Boolean = false;
|
||||
protected var _resultChanged: Boolean = true;
|
||||
protected var _hasMorePages: Boolean = false;
|
||||
//private var _morePagesWasFalse: Boolean = false;
|
||||
|
||||
val isAvailable get() = plugin.getUnderlyingPlugin()._runtime?.let { !it.isClosed && !it.isDead } ?: false;
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js.models
|
||||
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||
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.IPlatformLiveEventPager
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.warnIfMainThread
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class JSVODEventPager : JSPager<IPlatformLiveEvent>, IPlatformLiveEventPager {
|
||||
override var nextRequest: Int;
|
||||
|
||||
constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) {
|
||||
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
|
||||
}
|
||||
|
||||
fun nextPage(ms: Int) = plugin.isBusyWith("JSLiveEventPager.nextPage") {
|
||||
warnIfMainThread("VODEventPager.nextPage");
|
||||
|
||||
val pluginV8 = plugin.getUnderlyingPlugin();
|
||||
pluginV8.busy {
|
||||
val newPager: V8Value = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage(...)") {
|
||||
pager.invokeV8<V8Value>("nextPage", ms);
|
||||
};
|
||||
if(newPager is V8ValueObject)
|
||||
pager = newPager;
|
||||
_hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false;
|
||||
_resultChanged = true;
|
||||
}
|
||||
nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager");
|
||||
}
|
||||
|
||||
override fun nextPage() = nextPage(0);
|
||||
|
||||
override fun convertResult(obj: V8ValueObject): IPlatformLiveEvent {
|
||||
return IPlatformLiveEvent.fromV8(config, obj, "LiveEventPager");
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,10 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import java.time.LocalDateTime
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||
@@ -17,6 +21,10 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||
final override val duration: Long;
|
||||
final override val viewCount: Long;
|
||||
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
|
||||
final override val isLive: Boolean;
|
||||
final override val isShort: Boolean;
|
||||
|
||||
@@ -29,5 +37,11 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||
viewCount = _content.getOrThrow(config, "viewCount", contextName);
|
||||
isLive = _content.getOrThrow(config, "isLive", contextName);
|
||||
isShort = _content.getOrDefault(config, "isShort", contextName, false) ?: false;
|
||||
playbackTime = _content.getOrDefault<Long>(config, "playbackTime", contextName, -1)?.toLong() ?: -1;
|
||||
val playbackDateInt = _content.getOrDefault<Int>(config, "playbackDate", contextName, null)?.toLong();
|
||||
if(playbackDateInt == null || playbackDateInt == 0.toLong())
|
||||
playbackDate = null;
|
||||
else
|
||||
playbackDate = OffsetDateTime.of(LocalDateTime.ofEpochSecond(playbackDateInt, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
||||
}
|
||||
}
|
||||
+16
-1
@@ -7,6 +7,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
|
||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
@@ -26,12 +27,15 @@ import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.getOrThrowNullable
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
private val _plugin: JSClient;
|
||||
private val _hasGetComments: Boolean;
|
||||
private val _hasGetContentRecommendations: Boolean;
|
||||
private val _hasGetPlaybackTracker: Boolean;
|
||||
private val _hasGetVODEvents: Boolean;
|
||||
|
||||
//Details
|
||||
override val description : String;
|
||||
@@ -47,7 +51,6 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
|
||||
override val subtitles: List<ISubtitleSource>;
|
||||
|
||||
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) {
|
||||
val contextName = "VideoDetails";
|
||||
_plugin = plugin;
|
||||
@@ -72,6 +75,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
_hasGetComments = _content.has("getComments");
|
||||
_hasGetPlaybackTracker = _content.has("getPlaybackTracker");
|
||||
_hasGetContentRecommendations = _content.has("getContentRecommendations");
|
||||
_hasGetVODEvents = _content.has("getVODEvents");
|
||||
}
|
||||
|
||||
override fun getPlaybackTracker(): IPlaybackTracker? {
|
||||
@@ -138,4 +142,15 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
|
||||
return@busy JSCommentPager(_pluginConfig, client, commentPager);
|
||||
}
|
||||
}
|
||||
|
||||
fun hasVODEvents(): Boolean{
|
||||
return _hasGetVODEvents;
|
||||
}
|
||||
fun getVODEvents(url: String): IPager<IPlatformLiveEvent>? = _plugin.busy {
|
||||
if(!_hasGetVODEvents)
|
||||
return@busy null;
|
||||
|
||||
return@busy JSVODEventPager(_plugin.config, _plugin,
|
||||
_content.invokeV8<V8ValueObject>("getVODEvents", arrayOf<Any>()));
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -11,7 +11,6 @@ import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.DownloadedVideoMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
@@ -19,7 +18,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.downloads.VideoLocal
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import java.io.File
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
@@ -53,6 +52,10 @@ class LocalVideoDetails: IPlatformVideoDetails {
|
||||
override val isLive: Boolean = false;
|
||||
override val isShort: Boolean = false;
|
||||
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
|
||||
constructor(file: File) {
|
||||
id = PlatformID("Local", file.path, "LOCAL")
|
||||
name = file.name;
|
||||
|
||||
+5
-7
@@ -7,12 +7,12 @@ import java.util.stream.IntStream
|
||||
* A Content MultiPager that returns results based on a specified distribution
|
||||
* TODO: Merge all basic distribution pagers
|
||||
*/
|
||||
class MultiDistributionContentPager : MultiPager<IPlatformContent> {
|
||||
class MultiDistributionContentPager<T : IPlatformContent> : MultiPager<T> {
|
||||
|
||||
private val dist : HashMap<IPager<IPlatformContent>, Float>;
|
||||
private val distConsumed : HashMap<IPager<IPlatformContent>, Float>;
|
||||
private val dist : HashMap<IPager<T>, Float>;
|
||||
private val distConsumed : HashMap<IPager<T>, Float>;
|
||||
|
||||
constructor(pagers : Map<IPager<IPlatformContent>, Float>) : super(pagers.keys.toMutableList()) {
|
||||
constructor(pagers : Map<IPager<T>, Float>) : super(pagers.keys.toMutableList()) {
|
||||
val distTotal = pagers.values.sum();
|
||||
dist = HashMap();
|
||||
|
||||
@@ -25,7 +25,7 @@ class MultiDistributionContentPager : MultiPager<IPlatformContent> {
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun selectItemIndex(options: Array<SelectionOption<IPlatformContent>>): Int {
|
||||
override fun selectItemIndex(options: Array<SelectionOption<T>>): Int {
|
||||
if(options.size == 0)
|
||||
return -1;
|
||||
var bestIndex = 0;
|
||||
@@ -42,6 +42,4 @@ class MultiDistributionContentPager : MultiPager<IPlatformContent> {
|
||||
distConsumed[options[bestIndex].pager.getPager()] = bestConsumed;
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -62,6 +62,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
private val MAX_LAUNCH_RETRIES = 3
|
||||
private var _lastLaunchTime_ms = 0L
|
||||
private var _retryJob: Job? = null
|
||||
private var _autoLaunchEnabled = true
|
||||
|
||||
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
||||
this.name = name;
|
||||
@@ -305,6 +306,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
return;
|
||||
}
|
||||
|
||||
_autoLaunchEnabled = true
|
||||
_started = true;
|
||||
_sessionId = null;
|
||||
_launchRetries = 0
|
||||
@@ -546,6 +548,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
|
||||
if (appId == "CC1AD845") {
|
||||
sessionIsRunning = true;
|
||||
_autoLaunchEnabled = false
|
||||
|
||||
if (_sessionId == null) {
|
||||
connectionState = CastConnectionState.CONNECTED;
|
||||
@@ -558,7 +561,6 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
_transportId = transportId;
|
||||
|
||||
requestMediaStatus();
|
||||
playVideo();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -568,21 +570,22 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
if (System.currentTimeMillis() - _lastLaunchTime_ms > 5000) {
|
||||
_sessionId = null
|
||||
_mediaSessionId = null
|
||||
setTime(0.0)
|
||||
_transportId = null
|
||||
|
||||
if (_autoLaunchEnabled) {
|
||||
if (_launching && _launchRetries < MAX_LAUNCH_RETRIES) {
|
||||
Logger.i(TAG, "No player yet; attempting launch #${_launchRetries + 1}")
|
||||
_launchRetries++
|
||||
launchPlayer()
|
||||
} else if (!_launching && _launchRetries < MAX_LAUNCH_RETRIES) {
|
||||
} else {
|
||||
// Maybe the first GET_STATUS came back empty; still try launching
|
||||
Logger.i(TAG, "Player not found; triggering launch #${_launchRetries + 1}")
|
||||
_launching = true
|
||||
_launchRetries++
|
||||
launchPlayer()
|
||||
}
|
||||
} else {
|
||||
Logger.e(TAG, "Player not found after $_launchRetries attempts; giving up.")
|
||||
Logger.e(TAG, "Player not found ($_launchRetries, _autoLaunchEnabled = $_autoLaunchEnabled); giving up.")
|
||||
Logger.i(TAG, "Unable to start media receiver on device")
|
||||
stop()
|
||||
}
|
||||
@@ -599,6 +602,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
} else {
|
||||
_launching = false
|
||||
_launchRetries = 0
|
||||
_autoLaunchEnabled = false
|
||||
}
|
||||
|
||||
val volume = status.getJSONObject("volume");
|
||||
@@ -636,10 +640,16 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
stopVideo();
|
||||
}
|
||||
}
|
||||
|
||||
val needsLoad = statuses.length() == 0 || (statuses.getJSONObject(0).getString("playerState") == "IDLE")
|
||||
if (needsLoad && _contentId != null && _mediaSessionId == null) {
|
||||
Logger.i(TAG, "Receiver idle, sending initial LOAD")
|
||||
playVideo()
|
||||
}
|
||||
} else if (type == "CLOSE") {
|
||||
if (message.sourceId == "receiver-0") {
|
||||
Logger.i(TAG, "Close received.");
|
||||
stop();
|
||||
stopCasting();
|
||||
} else if (_transportId == message.sourceId) {
|
||||
throw Exception("Transport id closed.")
|
||||
}
|
||||
@@ -676,6 +686,10 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
localAddress = null;
|
||||
_started = false;
|
||||
|
||||
_contentId = null
|
||||
_contentType = null
|
||||
_streamType = null
|
||||
|
||||
_retryJob?.cancel()
|
||||
_retryJob = null
|
||||
|
||||
|
||||
@@ -348,7 +348,7 @@ class FCastCastingDevice : CastingDevice {
|
||||
headerBytesRead += read
|
||||
}
|
||||
|
||||
val size = ((buffer[3].toLong() shl 24) or (buffer[2].toLong() shl 16) or (buffer[1].toLong() shl 8) or buffer[0].toLong()).toInt();
|
||||
val size = ((buffer[3].toUByte().toLong() shl 24) or (buffer[2].toUByte().toLong() shl 16) or (buffer[1].toUByte().toLong() shl 8) or buffer[0].toUByte().toLong()).toInt();
|
||||
if (size > buffer.size) {
|
||||
Logger.w(TAG, "Packets larger than $size bytes are not supported.")
|
||||
break
|
||||
|
||||
@@ -39,6 +39,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestMergingRawSource
|
||||
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.api.media.platforms.js.models.sources.JSSource
|
||||
import com.futo.platformplayer.builders.DashBuilder
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
@@ -486,7 +487,7 @@ class StateCasting {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
|
||||
val proxyStreams = shouldProxyStreams(ad, videoSource, audioSource)
|
||||
val url = getLocalUrl(ad);
|
||||
val id = UUID.randomUUID();
|
||||
|
||||
@@ -751,7 +752,7 @@ class StateCasting {
|
||||
|
||||
private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||
val proxyStreams = shouldProxyStreams(ad, videoSource, audioSource)
|
||||
|
||||
val url = getLocalUrl(ad);
|
||||
val id = UUID.randomUUID();
|
||||
@@ -1114,9 +1115,14 @@ class StateCasting {
|
||||
return listOf(hlsUrl, videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||
}
|
||||
|
||||
private fun shouldProxyStreams(castingDevice: CastingDevice, videoSource: IVideoSource?, audioSource: IAudioSource?): Boolean {
|
||||
val hasRequestModifier = (videoSource as? JSSource)?.hasRequestModifier == true || (audioSource as? JSSource)?.hasRequestModifier == true
|
||||
return Settings.instance.casting.alwaysProxyRequests || castingDevice !is FCastCastingDevice || hasRequestModifier
|
||||
}
|
||||
|
||||
private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||
val proxyStreams = shouldProxyStreams(ad, videoSource, audioSource)
|
||||
|
||||
val url = getLocalUrl(ad);
|
||||
val id = UUID.randomUUID();
|
||||
|
||||
@@ -47,10 +47,10 @@ class DeveloperEndpoints(private val context: Context) {
|
||||
private val testPluginOrThrow: V8Plugin get() = _testPlugin ?: throw IllegalStateException("Attempted to use test plugin without plugin");
|
||||
private val _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf();
|
||||
|
||||
private inline fun <reified T> createRemoteObjectArray(objs: Iterable<T>): List<V8RemoteObject> {
|
||||
val remotes = mutableListOf<V8RemoteObject>();
|
||||
private inline fun <reified T> createRemoteObjectArray(objs: Iterable<T>): List<V8RemoteObject?> {
|
||||
val remotes = mutableListOf<V8RemoteObject?>();
|
||||
for(obj in objs)
|
||||
remotes.add(createRemoteObject(obj)!!);
|
||||
remotes.add(createRemoteObject(obj));
|
||||
return remotes;
|
||||
}
|
||||
private inline fun <reified T> createRemoteObject(obj: T): V8RemoteObject? {
|
||||
|
||||
@@ -106,7 +106,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
||||
};
|
||||
|
||||
_buttonTutorial.setOnClickListener {
|
||||
UIDialogs.showCastingTutorialDialog(context)
|
||||
UIDialogs.showCastingTutorialDialog(context, ownerActivity)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
||||
|
||||
private fun performDismiss(shouldShowCastingDialog: Boolean = true) {
|
||||
if (shouldShowCastingDialog) {
|
||||
UIDialogs.showCastingDialog(context);
|
||||
UIDialogs.showCastingDialog(context, ownerActivity);
|
||||
}
|
||||
|
||||
dismiss();
|
||||
|
||||
@@ -53,7 +53,7 @@ class CastingHelpDialog(context: Context?) : AlertDialog(context) {
|
||||
|
||||
findViewById<BigButton>(R.id.button_close).onClick.subscribe {
|
||||
dismiss()
|
||||
UIDialogs.showCastingAddDialog(context)
|
||||
UIDialogs.showCastingAddDialog(context, ownerActivity)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
|
||||
_buttonClose.setOnClickListener { dismiss(); };
|
||||
_buttonAdd.setOnClickListener {
|
||||
UIDialogs.showCastingAddDialog(context);
|
||||
UIDialogs.showCastingAddDialog(context, ownerActivity);
|
||||
dismiss();
|
||||
};
|
||||
|
||||
@@ -139,9 +139,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_textNoDevicesFound.visibility = if (_devices.isEmpty()) View.VISIBLE else View.GONE;
|
||||
_recyclerDevices.visibility = if (_devices.isNotEmpty()) View.VISIBLE else View.GONE;
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
|
||||
@@ -303,9 +303,10 @@ class VideoDownload {
|
||||
try {
|
||||
val playlistResponse = client.get(source.url)
|
||||
if (playlistResponse.isOk) {
|
||||
val resolvedPlaylistUrl = playlistResponse.url
|
||||
val playlistContent = playlistResponse.body?.string()
|
||||
if (playlistContent != null) {
|
||||
videoSources.addAll(HLS.parseAndGetVideoSources(source, playlistContent, source.url))
|
||||
videoSources.addAll(HLS.parseAndGetVideoSources(source, playlistContent, resolvedPlaylistUrl))
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
@@ -351,9 +352,10 @@ class VideoDownload {
|
||||
try {
|
||||
val playlistResponse = client.get(source.url)
|
||||
if (playlistResponse.isOk) {
|
||||
val resolvedPlaylistUrl = playlistResponse.url
|
||||
val playlistContent = playlistResponse.body?.string()
|
||||
if (playlistContent != null) {
|
||||
audioSources.addAll(HLS.parseAndGetAudioSources(source, playlistContent, source.url))
|
||||
audioSources.addAll(HLS.parseAndGetAudioSources(source, playlistContent, resolvedPlaylistUrl))
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
|
||||
@@ -73,6 +73,10 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
|
||||
|
||||
override val isShort: Boolean get() = videoSerialized.isShort;
|
||||
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
|
||||
//TODO: Offline subtitles
|
||||
override val subtitles: List<ISubtitleSource> = listOf();
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ class V8RemoteObject {
|
||||
}
|
||||
|
||||
|
||||
fun List<V8RemoteObject>.serialize() : String {
|
||||
fun List<V8RemoteObject?>.serialize() : String {
|
||||
return _gson.toJson(this);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-3
@@ -25,6 +25,7 @@ import com.futo.platformplayer.api.media.structures.IReplacerPager
|
||||
import com.futo.platformplayer.api.media.structures.MultiPager
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.constructs.Event3
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
@@ -61,7 +62,7 @@ class ChannelContentsFragment(private val subType: String? = null) : Fragment(),
|
||||
private var _query: String? = null
|
||||
private var _searchView: SearchView? = null
|
||||
|
||||
val onContentClicked = Event2<IPlatformContent, Long>();
|
||||
val onContentClicked = Event3<IPlatformContent, Long, Pair<IPager<IPlatformContent>, ArrayList<IPlatformContent>>?>();
|
||||
val onContentUrlClicked = Event2<String, ContentType>();
|
||||
val onUrlClicked = Event1<String>();
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||
@@ -208,10 +209,13 @@ class ChannelContentsFragment(private val subType: String? = null) : Fragment(),
|
||||
_searchView = searchView
|
||||
updateSearchViewVisibility()
|
||||
|
||||
_adapterResults = PreviewContentListAdapter(view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar, viewsToPrepend = arrayListOf(searchView)).apply {
|
||||
_adapterResults = PreviewContentListAdapter(lifecycleScope, view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar, viewsToPrepend = arrayListOf(searchView)).apply {
|
||||
this.onContentUrlClicked.subscribe(this@ChannelContentsFragment.onContentUrlClicked::emit);
|
||||
this.onUrlClicked.subscribe(this@ChannelContentsFragment.onUrlClicked::emit);
|
||||
this.onContentClicked.subscribe(this@ChannelContentsFragment.onContentClicked::emit);
|
||||
this.onContentClicked.subscribe { content, num ->
|
||||
val results = ArrayList(_results)
|
||||
this@ChannelContentsFragment.onContentClicked.emit(content, num, Pair(_pager!!, results))
|
||||
}
|
||||
this.onChannelClicked.subscribe(this@ChannelContentsFragment.onChannelClicked::emit);
|
||||
this.onAddToClicked.subscribe(this@ChannelContentsFragment.onAddToClicked::emit);
|
||||
this.onAddToQueueClicked.subscribe(this@ChannelContentsFragment.onAddToQueueClicked::emit);
|
||||
|
||||
+1
-1
@@ -148,7 +148,7 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
||||
_recyclerResults = view.findViewById(R.id.recycler_videos)
|
||||
|
||||
_adapterResults = PreviewContentListAdapter(
|
||||
view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar
|
||||
lifecycleScope, view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar
|
||||
).apply {
|
||||
this.onContentUrlClicked.subscribe(this@ChannelPlaylistsFragment.onContentUrlClicked::emit)
|
||||
this.onUrlClicked.subscribe(this@ChannelPlaylistsFragment.onUrlClicked::emit)
|
||||
|
||||
+3
@@ -15,6 +15,7 @@ import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
@@ -375,6 +376,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
|
||||
fun newInstance() = MenuBottomBarFragment().apply { }
|
||||
|
||||
@UnstableApi
|
||||
//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 }, {
|
||||
@@ -390,6 +392,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>(withHistory = false) }),
|
||||
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>(withHistory = false) }),
|
||||
ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate<PlaylistsFragment>(withHistory = false) }),
|
||||
ButtonDefinition(11, R.drawable.ic_smart_display, R.drawable.ic_smart_display_filled, R.string.shorts, canToggle = true, { it.currentMain is ShortsFragment && !(it.currentMain as ShortsFragment).isChannelShortsMode }, { it.navigate<ShortsFragment>(withHistory = false) }),
|
||||
ButtonDefinition(5, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate<HistoryFragment>(withHistory = false) }),
|
||||
ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>(withHistory = false) }),
|
||||
ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>(withHistory = false) }),
|
||||
|
||||
+8
@@ -211,6 +211,14 @@ class ChannelFragment : MainFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter.onShortClicked.subscribe { v, _, pagerPair ->
|
||||
when (v) {
|
||||
is IPlatformVideo -> {
|
||||
StatePlayer.instance.clearQueue()
|
||||
fragment.navigate<ShortsFragment>(Triple(v, pagerPair!!.first, pagerPair.second))
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter.onAddToClicked.subscribe { content ->
|
||||
_overlayContainer.let {
|
||||
if (content is IPlatformVideo) _slideUpOverlay =
|
||||
|
||||
+14
-2
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
@@ -19,6 +20,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSWeb
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ShortView.Companion
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateMeta
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
@@ -34,6 +36,9 @@ import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoViewHolder
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||
import com.futo.platformplayer.withTimestamp
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
|
||||
@@ -59,7 +64,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
player.modifyState("ThumbnailPlayer") { state -> state.muted = true };
|
||||
_exoPlayer = player;
|
||||
|
||||
return PreviewContentListAdapter(context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(), arrayListOf(), shouldShowTimeBar).apply {
|
||||
return PreviewContentListAdapter(fragment.lifecycleScope, context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(), arrayListOf(), shouldShowTimeBar).apply {
|
||||
attachAdapterEvents(this);
|
||||
}
|
||||
}
|
||||
@@ -246,8 +251,15 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
}
|
||||
|
||||
//TODO: Is this still necessary?
|
||||
if(viewHolder.childViewHolder is ContentPreviewViewHolder)
|
||||
if(viewHolder.childViewHolder is ContentPreviewViewHolder) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
(recyclerData.adapter as PreviewContentListAdapter?)?.preview(viewHolder.childViewHolder)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "playPreview failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopVideo() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+359
@@ -0,0 +1,359 @@
|
||||
package com.futo.platformplayer.fragment.mainactivity.main
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.SoundEffectConstants
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
|
||||
@UnstableApi
|
||||
class ShortsFragment : MainFragment() {
|
||||
override val isMainView: Boolean = true
|
||||
override val isTab: Boolean = true
|
||||
override val hasBottomBar: Boolean get() = true
|
||||
|
||||
private var loadPagerTask: TaskHandler<ShortsFragment, IPager<IPlatformVideo>>? = null
|
||||
private var nextPageTask: TaskHandler<ShortsFragment, List<IPlatformVideo>>? = null
|
||||
|
||||
private var mainShortsPager: IPager<IPlatformVideo>? = null
|
||||
private val mainShorts: MutableList<IPlatformVideo> = mutableListOf()
|
||||
|
||||
// the pager to call next on
|
||||
private var currentShortsPager: IPager<IPlatformVideo>? = null
|
||||
|
||||
// the shorts array bound to the ViewPager2 adapter
|
||||
private val currentShorts: MutableList<IPlatformVideo> = mutableListOf()
|
||||
|
||||
private var channelShortsPager: IPager<IPlatformVideo>? = null
|
||||
private val channelShorts: MutableList<IPlatformVideo> = mutableListOf()
|
||||
val isChannelShortsMode: Boolean
|
||||
get() = channelShortsPager != null
|
||||
|
||||
private var viewPager: ViewPager2? = null
|
||||
private lateinit var zeroState: LinearLayout
|
||||
private lateinit var sourcesButton: BigButton
|
||||
private lateinit var overlayLoading: FrameLayout
|
||||
private lateinit var overlayLoadingSpinner: ImageView
|
||||
private lateinit var overlayQualityContainer: FrameLayout
|
||||
private var customViewAdapter: CustomViewAdapter? = null
|
||||
|
||||
// we just completely reset the data structure so we want to tell the adapter that
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
(activity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
|
||||
super.onShownWithView(parameter, isBack)
|
||||
|
||||
if (parameter is Triple<*, *, *>) {
|
||||
setLoading(false)
|
||||
channelShorts.clear()
|
||||
@Suppress("UNCHECKED_CAST") // TODO replace with a strongly typed parameter
|
||||
channelShorts.addAll(parameter.third as ArrayList<IPlatformVideo>)
|
||||
@Suppress("UNCHECKED_CAST") // TODO replace with a strongly typed parameter
|
||||
channelShortsPager = parameter.second as IPager<IPlatformVideo>
|
||||
|
||||
currentShorts.clear()
|
||||
currentShorts.addAll(channelShorts)
|
||||
currentShortsPager = channelShortsPager
|
||||
|
||||
viewPager?.adapter?.notifyDataSetChanged()
|
||||
|
||||
viewPager?.post {
|
||||
viewPager?.currentItem = channelShorts.indexOfFirst {
|
||||
return@indexOfFirst (parameter.first as IPlatformVideo).id == it.id
|
||||
}
|
||||
}
|
||||
} else if (isChannelShortsMode) {
|
||||
channelShortsPager = null
|
||||
channelShorts.clear()
|
||||
currentShorts.clear()
|
||||
|
||||
if (loadPagerTask == null) {
|
||||
currentShorts.addAll(mainShorts)
|
||||
currentShortsPager = mainShortsPager
|
||||
} else {
|
||||
setLoading(true)
|
||||
}
|
||||
|
||||
viewPager?.adapter?.notifyDataSetChanged()
|
||||
viewPager?.currentItem = 0
|
||||
}
|
||||
|
||||
updateZeroState()
|
||||
}
|
||||
|
||||
override fun onCreateMainView(
|
||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||
): View {
|
||||
return inflater.inflate(R.layout.fragment_shorts, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
viewPager = view.findViewById(R.id.view_pager)
|
||||
zeroState = view.findViewById(R.id.zero_state)
|
||||
sourcesButton = view.findViewById(R.id.sources_button)
|
||||
overlayLoading = view.findViewById(R.id.short_view_loading_overlay)
|
||||
overlayLoadingSpinner = view.findViewById(R.id.short_view_loader)
|
||||
overlayQualityContainer = view.findViewById(R.id.shorts_quality_overview)
|
||||
|
||||
sourcesButton.onClick.subscribe {
|
||||
sourcesButton.playSoundEffect(SoundEffectConstants.CLICK)
|
||||
navigate<SourcesFragment>()
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
Logger.i(TAG, "Creating adapter")
|
||||
val customViewAdapter =
|
||||
CustomViewAdapter(currentShorts, layoutInflater, this@ShortsFragment, overlayQualityContainer, { isChannelShortsMode }) {
|
||||
if (!currentShortsPager!!.hasMorePages()) {
|
||||
return@CustomViewAdapter
|
||||
}
|
||||
nextPage()
|
||||
}
|
||||
customViewAdapter.onResetTriggered.subscribe {
|
||||
setLoading(true)
|
||||
loadPager()
|
||||
|
||||
loadPagerTask!!.success {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
val viewPager = viewPager!!
|
||||
viewPager.adapter = customViewAdapter
|
||||
|
||||
this.customViewAdapter = customViewAdapter
|
||||
|
||||
if (loadPagerTask == null && currentShorts.isEmpty()) {
|
||||
loadPager()
|
||||
|
||||
loadPagerTask!!.success {
|
||||
setLoading(false)
|
||||
updateZeroState()
|
||||
}
|
||||
} else {
|
||||
setLoading(false)
|
||||
updateZeroState()
|
||||
}
|
||||
|
||||
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
fun play(adapter: CustomViewAdapter, position: Int) {
|
||||
val recycler = (viewPager.getChildAt(0) as RecyclerView)
|
||||
val viewHolder =
|
||||
recycler.findViewHolderForAdapterPosition(position) as CustomViewHolder?
|
||||
|
||||
if (viewHolder == null) {
|
||||
adapter.needToPlay = position
|
||||
} else {
|
||||
val focusedView = viewHolder.shortView
|
||||
focusedView.play()
|
||||
adapter.previousShownView = focusedView
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
val adapter = (viewPager.adapter as CustomViewAdapter)
|
||||
if (adapter.previousShownView == null) {
|
||||
// play if this page selection didn't trigger by a swipe from another page
|
||||
play(adapter, position)
|
||||
} else {
|
||||
adapter.previousShownView?.stop()
|
||||
adapter.previousShownView = null
|
||||
adapter.newPosition = position
|
||||
}
|
||||
}
|
||||
|
||||
// wait for the state to idle to prevent UI lag
|
||||
override fun onPageScrollStateChanged(state: Int) {
|
||||
super.onPageScrollStateChanged(state)
|
||||
if (state == ViewPager2.SCROLL_STATE_IDLE) {
|
||||
val adapter = (viewPager.adapter as CustomViewAdapter)
|
||||
val position = adapter.newPosition ?: return
|
||||
adapter.newPosition = null
|
||||
|
||||
play(adapter, position)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun updateZeroState() {
|
||||
if (mainShorts.isEmpty() && !isChannelShortsMode && loadPagerTask == null) {
|
||||
zeroState.visibility = View.VISIBLE
|
||||
} else {
|
||||
zeroState.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun nextPage() {
|
||||
nextPageTask?.cancel()
|
||||
|
||||
val nextPageTask =
|
||||
TaskHandler<ShortsFragment, List<IPlatformVideo>>(StateApp.instance.scopeGetter, {
|
||||
currentShortsPager!!.nextPage()
|
||||
|
||||
return@TaskHandler currentShortsPager!!.getResults()
|
||||
}).success { newVideos ->
|
||||
val prevCount = customViewAdapter!!.itemCount
|
||||
currentShorts.addAll(newVideos)
|
||||
if (isChannelShortsMode) {
|
||||
channelShorts.addAll(newVideos)
|
||||
} else {
|
||||
mainShorts.addAll(newVideos)
|
||||
}
|
||||
customViewAdapter!!.notifyItemRangeInserted(prevCount, newVideos.size)
|
||||
nextPageTask = null
|
||||
}
|
||||
|
||||
nextPageTask.run(this)
|
||||
|
||||
this.nextPageTask = nextPageTask
|
||||
}
|
||||
|
||||
// we just completely reset the data structure so we want to tell the adapter that
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun loadPager() {
|
||||
loadPagerTask?.cancel()
|
||||
|
||||
val loadPagerTask =
|
||||
TaskHandler<ShortsFragment, IPager<IPlatformVideo>>(StateApp.instance.scopeGetter, {
|
||||
val pager = StatePlatform.instance.getShorts()
|
||||
|
||||
return@TaskHandler pager
|
||||
}).success { pager ->
|
||||
mainShorts.clear()
|
||||
mainShorts.addAll(pager.getResults())
|
||||
mainShortsPager = pager
|
||||
|
||||
if (!isChannelShortsMode) {
|
||||
currentShorts.clear()
|
||||
currentShorts.addAll(mainShorts)
|
||||
currentShortsPager = pager
|
||||
|
||||
// if the view pager exists go back to the beginning
|
||||
viewPager?.adapter?.notifyDataSetChanged()
|
||||
viewPager?.currentItem = 0
|
||||
}
|
||||
|
||||
loadPagerTask = null
|
||||
}.exception<Throwable> { err ->
|
||||
val message = "Unable to load shorts $err"
|
||||
Logger.i(TAG, message)
|
||||
if (context != null) {
|
||||
UIDialogs.showDialog(
|
||||
requireContext(), R.drawable.ic_sources, message, null, null, 0, UIDialogs.Action(
|
||||
"Close", { }, UIDialogs.ActionStyle.PRIMARY
|
||||
)
|
||||
)
|
||||
}
|
||||
return@exception
|
||||
}
|
||||
|
||||
this.loadPagerTask = loadPagerTask
|
||||
|
||||
loadPagerTask.run(this)
|
||||
}
|
||||
|
||||
private fun setLoading(isLoading: Boolean) {
|
||||
if (isLoading) {
|
||||
(overlayLoadingSpinner.drawable as Animatable?)?.start()
|
||||
overlayLoading.visibility = View.VISIBLE
|
||||
} else {
|
||||
overlayLoading.visibility = View.GONE
|
||||
(overlayLoadingSpinner.drawable as Animatable?)?.stop()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
customViewAdapter?.previousShownView?.pause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
loadPagerTask?.cancel()
|
||||
loadPagerTask = null
|
||||
nextPageTask?.cancel()
|
||||
nextPageTask = null
|
||||
customViewAdapter?.previousShownView?.stop()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ShortsFragment"
|
||||
|
||||
fun newInstance() = ShortsFragment()
|
||||
}
|
||||
|
||||
class CustomViewAdapter(
|
||||
private val videos: MutableList<IPlatformVideo>,
|
||||
private val inflater: LayoutInflater,
|
||||
private val fragment: MainFragment,
|
||||
private val overlayQualityContainer: FrameLayout,
|
||||
private val isChannelShortsMode: () -> Boolean,
|
||||
private val onNearEnd: () -> Unit,
|
||||
) : RecyclerView.Adapter<CustomViewHolder>() {
|
||||
val onResetTriggered = Event0()
|
||||
var previousShownView: ShortView? = null
|
||||
var newPosition: Int? = null
|
||||
var needToPlay: Int? = null
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
|
||||
val shortView = ShortView(inflater, fragment, overlayQualityContainer)
|
||||
shortView.onResetTriggered.subscribe {
|
||||
onResetTriggered.emit()
|
||||
}
|
||||
return CustomViewHolder(shortView)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
|
||||
holder.shortView.changeVideo(videos[position], isChannelShortsMode())
|
||||
|
||||
if (position == itemCount - 1) {
|
||||
onNearEnd()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: CustomViewHolder) {
|
||||
super.onViewRecycled(holder)
|
||||
holder.shortView.cancel()
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(holder: CustomViewHolder) {
|
||||
super.onViewAttachedToWindow(holder)
|
||||
|
||||
if (holder.absoluteAdapterPosition == needToPlay) {
|
||||
holder.shortView.play()
|
||||
needToPlay = null
|
||||
previousShownView = holder.shortView
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = videos.size
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class CustomViewHolder(val shortView: ShortView) : RecyclerView.ViewHolder(shortView)
|
||||
}
|
||||
+1
-1
@@ -25,7 +25,7 @@ data class SuggestionsFragmentData(val query: String, val searchType: SearchType
|
||||
|
||||
class SuggestionsFragment : MainFragment {
|
||||
override val isMainView : Boolean = true;
|
||||
override val hasBottomBar: Boolean = false;
|
||||
override val hasBottomBar: Boolean = true;
|
||||
override val isHistory: Boolean = false;
|
||||
|
||||
private var _recyclerSuggestions: RecyclerView? = null;
|
||||
|
||||
+4
@@ -32,6 +32,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import com.futo.platformplayer.views.pills.WidePillButton
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@@ -152,6 +153,9 @@ class TutorialFragment : MainFragment() {
|
||||
override val viewCount: Long = -1
|
||||
override val video: IVideoSourceDescriptor = TutorialVideoSourceDescriptor(videoUrl, duration, width, height)
|
||||
override val isShort: Boolean = false;
|
||||
override var playbackTime: Long = -1;
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
override var playbackDate: OffsetDateTime? = null;
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
|
||||
return EmptyPager()
|
||||
}
|
||||
|
||||
+8
-10
@@ -337,13 +337,6 @@ class VideoDetailFragment() : MainFragment() {
|
||||
closeVideoDetails();
|
||||
};
|
||||
it.onMaximize.subscribe { maximizeVideoDetail(it) };
|
||||
it.onPlayChanged.subscribe {
|
||||
if(isInPictureInPicture) {
|
||||
val params = _viewDetail?.getPictureInPictureParams();
|
||||
if (params != null)
|
||||
activity?.setPictureInPictureParams(params);
|
||||
}
|
||||
};
|
||||
it.onEnterPictureInPicture.subscribe {
|
||||
Logger.i(TAG, "onEnterPictureInPicture")
|
||||
isInPictureInPicture = true;
|
||||
@@ -446,9 +439,14 @@ class VideoDetailFragment() : MainFragment() {
|
||||
val viewDetail = _viewDetail;
|
||||
Logger.i(TAG, "onUserLeaveHint preventPictureInPicture=${viewDetail?.preventPictureInPicture} isCasting=${StateCasting.instance.isCasting} isBackgroundPictureInPicture=${Settings.instance.playback.isBackgroundPictureInPicture()} allowBackground=${viewDetail?.allowBackground}");
|
||||
|
||||
if(viewDetail?.preventPictureInPicture == false && !StateCasting.instance.isCasting && Settings.instance.playback.isBackgroundPictureInPicture() && !viewDetail.allowBackground) {
|
||||
_leavingPiP = false;
|
||||
if (viewDetail === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (viewDetail.shouldEnterPictureInPicture) {
|
||||
_leavingPiP = false
|
||||
}
|
||||
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.S && viewDetail.preventPictureInPicture == false && !StateCasting.instance.isCasting && Settings.instance.playback.isBackgroundPictureInPicture() && !viewDetail.allowBackground) {
|
||||
val params = _viewDetail?.getPictureInPictureParams();
|
||||
if(params != null) {
|
||||
Logger.i(TAG, "enterPictureInPictureMode")
|
||||
@@ -457,7 +455,7 @@ class VideoDetailFragment() : MainFragment() {
|
||||
}
|
||||
|
||||
if (isFullscreen) {
|
||||
viewDetail?.restoreBrightness()
|
||||
viewDetail.restoreBrightness()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+198
-37
@@ -16,6 +16,7 @@ import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.Icon
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import android.text.Spanned
|
||||
import android.util.AttributeSet
|
||||
@@ -50,6 +51,7 @@ import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.UISlideOverlays
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.activities.SyncShowPairingCodeActivity.Companion.activity
|
||||
import com.futo.platformplayer.api.media.IPluginSourced
|
||||
import com.futo.platformplayer.api.media.LiveChatManager
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
@@ -247,7 +249,13 @@ class VideoDetailView : ConstraintLayout {
|
||||
private val _buttonPins: RoundButtonGroup;
|
||||
//private val _buttonMore: RoundButton;
|
||||
|
||||
var preventPictureInPicture: Boolean = false;
|
||||
var preventPictureInPicture: Boolean = false
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
onShouldEnterPictureInPictureChanged.emit()
|
||||
}
|
||||
}
|
||||
|
||||
private val _addCommentView: AddCommentView;
|
||||
private var _tabIndex: Int? = null;
|
||||
@@ -316,11 +324,24 @@ class VideoDetailView : ConstraintLayout {
|
||||
val onClose = Event0();
|
||||
val onFullscreenChanged = Event1<Boolean>();
|
||||
val onEnterPictureInPicture = Event0();
|
||||
val onPlayChanged = Event1<Boolean>();
|
||||
val onVideoChanged = Event2<Int, Int>()
|
||||
|
||||
var allowBackground: Boolean = false
|
||||
private set;
|
||||
private set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
onShouldEnterPictureInPictureChanged.emit()
|
||||
}
|
||||
}
|
||||
|
||||
val shouldEnterPictureInPicture: Boolean
|
||||
get() = !preventPictureInPicture &&
|
||||
!StateCasting.instance.isCasting &&
|
||||
Settings.instance.playback.isBackgroundPictureInPicture() &&
|
||||
!allowBackground &&
|
||||
isPlaying
|
||||
|
||||
val onShouldEnterPictureInPictureChanged = Event0();
|
||||
|
||||
val onTouchCancel = Event0();
|
||||
private var _lastPositionSaveTime: Long = -1;
|
||||
@@ -433,6 +454,29 @@ class VideoDetailView : ConstraintLayout {
|
||||
fragment.navigate<VideoDetailFragment>(it.targetUrl);
|
||||
};
|
||||
|
||||
_container_content_liveChat.onUrlClick.subscribe { uri ->
|
||||
val c = context
|
||||
if (c is MainActivity) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
if (!c.handleUrl(uri.toString())) {
|
||||
Intent(Intent.ACTION_VIEW, uri).apply {
|
||||
addCategory(Intent.CATEGORY_BROWSABLE)
|
||||
context.startActivity(this)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to handle live chat URL")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Intent(Intent.ACTION_VIEW, uri).apply {
|
||||
addCategory(Intent.CATEGORY_BROWSABLE)
|
||||
context.startActivity(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_monetization.onSupportTap.subscribe {
|
||||
_container_content_support.setPolycentricProfile(_polycentricProfile);
|
||||
switchContentView(_container_content_support);
|
||||
@@ -457,11 +501,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
_player.attachPlayer();
|
||||
|
||||
_container_content_liveChat.onRaidNow.subscribe {
|
||||
StatePlayer.instance.clearQueue();
|
||||
fragment.navigate<VideoDetailFragment>(it.targetUrl);
|
||||
};
|
||||
|
||||
StateApp.instance.preventPictureInPicture.subscribe(this) {
|
||||
Logger.i(TAG, "StateApp.instance.preventPictureInPicture.subscribe preventPictureInPicture = true");
|
||||
preventPictureInPicture = true;
|
||||
@@ -622,8 +661,13 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
};
|
||||
|
||||
onShouldEnterPictureInPictureChanged.subscribe {
|
||||
val params = getPictureInPictureParams()
|
||||
fragment.activity?.setPictureInPictureParams(params)
|
||||
}
|
||||
|
||||
if (!isInEditMode) {
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { device, connectionState ->
|
||||
if (_onPauseCalled) {
|
||||
return@subscribe;
|
||||
}
|
||||
@@ -635,7 +679,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
setCastEnabled(true);
|
||||
}
|
||||
CastConnectionState.DISCONNECTED -> {
|
||||
loadCurrentVideo(lastPositionMilliseconds);
|
||||
loadCurrentVideo(lastPositionMilliseconds, playWhenReady = device.isPlaying);
|
||||
updatePillButtonVisibilities();
|
||||
setCastEnabled(false);
|
||||
|
||||
@@ -719,7 +763,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
};
|
||||
MediaControlReceiver.onBackgroundReceived.subscribe(this) {
|
||||
Logger.i(TAG, "MediaControlReceiver.onBackgroundReceived")
|
||||
_player.switchToAudioMode();
|
||||
_player.switchToAudioMode(video);
|
||||
allowBackground = true;
|
||||
StateApp.instance.contextOrNull?.let {
|
||||
try {
|
||||
@@ -938,6 +982,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
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) {
|
||||
@@ -966,7 +1011,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
} else null,
|
||||
if (!isLimitedVersion) RoundButton(context, R.drawable.ic_screen_share, if (allowBackground) context.getString(R.string.background_revert) else context.getString(R.string.background), TAG_BACKGROUND) {
|
||||
if (!allowBackground) {
|
||||
_player.switchToAudioMode();
|
||||
_player.switchToAudioMode(video);
|
||||
allowBackground = true;
|
||||
it.text.text = resources.getString(R.string.background_revert);
|
||||
} else {
|
||||
@@ -1113,6 +1158,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
//Requested behavior to leave it in audio mode. leaving it commented if it causes issues, revert?
|
||||
if(!allowBackground) {
|
||||
_player.switchToVideoMode();
|
||||
allowBackground = false;
|
||||
_buttonPins.getButtonByTag(TAG_BACKGROUND)?.text?.text = resources.getString(R.string.background);
|
||||
}
|
||||
}
|
||||
@@ -1136,12 +1182,18 @@ class VideoDetailView : ConstraintLayout {
|
||||
when (Settings.instance.playback.backgroundPlay) {
|
||||
0 -> handlePause();
|
||||
1 -> {
|
||||
if(!(video?.isLive ?: false))
|
||||
_player.switchToAudioMode();
|
||||
if(!(video?.isLive ?: false)) {
|
||||
_player.switchToAudioMode(video);
|
||||
allowBackground = true;
|
||||
}
|
||||
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_player.isFullScreen) {
|
||||
restoreBrightness()
|
||||
}
|
||||
}
|
||||
fun onStop() {
|
||||
Logger.i(TAG, "onStop");
|
||||
@@ -1743,12 +1795,19 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
_liveChat?.stop();
|
||||
_liveChat = null;
|
||||
var gotLive = false;
|
||||
if (video.isLive && video.live != null) {
|
||||
loadLiveChat(video);
|
||||
gotLive = true;
|
||||
}
|
||||
if (video.isLive && video.live == null && !video.video.videoSources.any())
|
||||
if (video.isLive && video.live == null && !video.video.videoSources.any()) {
|
||||
startLiveTry(video);
|
||||
|
||||
gotLive = true;
|
||||
}
|
||||
if(!gotLive && video is JSVideoDetails && video.hasVODEvents()) {
|
||||
Logger.i(TAG, "Loading VOD chat");
|
||||
loadVODChat(video);
|
||||
}
|
||||
|
||||
_player.updateNextPrevious();
|
||||
updateMoreButtons();
|
||||
@@ -1772,6 +1831,43 @@ class VideoDetailView : ConstraintLayout {
|
||||
_taskLoadRecommendations.run(videoDetail.url)
|
||||
}
|
||||
}
|
||||
fun loadVODChat(video: IPlatformVideoDetails) {
|
||||
_liveChat?.stop();
|
||||
_container_content_liveChat.cancel();
|
||||
_liveChat = null;
|
||||
if(video !is JSVideoDetails)
|
||||
return;
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
var livePager: IPager<IPlatformLiveEvent>?;
|
||||
try {
|
||||
//TODO: Create video.getLiveEvents shortcut/optimalization
|
||||
livePager = video.getVODEvents(video.url);
|
||||
} catch (ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to obtain VODEvents pager", ex);
|
||||
livePager = null;
|
||||
}
|
||||
val liveChat = livePager?.let {
|
||||
val liveChatManager = LiveChatManager(fragment.lifecycleScope, livePager, video.viewCount);
|
||||
liveChatManager.start();
|
||||
return@let liveChatManager;
|
||||
}
|
||||
_liveChat = liveChat;
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
_container_content_liveChat.load(fragment.lifecycleScope, liveChat, null, if(liveChat != null) video.viewCount else null);
|
||||
switchContentView(_container_content_liveChat);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to switch content view to vod chat.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to load vod chat", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
fun loadLiveChat(video: IPlatformVideoDetails) {
|
||||
_liveChat?.stop();
|
||||
_container_content_liveChat.cancel();
|
||||
@@ -1846,7 +1942,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
|
||||
//Source Loads
|
||||
private fun loadCurrentVideo(resumePositionMs: Long = 0) {
|
||||
private fun loadCurrentVideo(resumePositionMs: Long = 0, playWhenReady: Boolean = true) {
|
||||
_didStop = false;
|
||||
|
||||
val video = (videoLocal ?: video) ?: return;
|
||||
@@ -1867,8 +1963,18 @@ class VideoDetailView : ConstraintLayout {
|
||||
if (!isCasting) {
|
||||
setCastEnabled(false);
|
||||
|
||||
val isLimitedVersion = StatePlatform.instance.getContentClientOrNull(video.url)?.let {
|
||||
if (it is JSClient)
|
||||
return@let it.config.reduceFunctionsInLimitedVersion && BuildConfig.IS_PLAYSTORE_BUILD
|
||||
else false;
|
||||
} ?: false;
|
||||
|
||||
if (isLimitedVersion && _player.isAudioMode) {
|
||||
_player.switchToVideoMode()
|
||||
allowBackground = false;
|
||||
} else {
|
||||
val thumbnail = video.thumbnails.getHQThumbnail();
|
||||
if (videoSource == null && !thumbnail.isNullOrBlank())
|
||||
if ((videoSource == null || _player.isAudioMode) && !thumbnail.isNullOrBlank())
|
||||
Glide.with(context).asBitmap().load(thumbnail)
|
||||
.into(object: CustomTarget<Bitmap>() {
|
||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||
@@ -1880,13 +1986,29 @@ class VideoDetailView : ConstraintLayout {
|
||||
});
|
||||
else
|
||||
_player.setArtwork(null);
|
||||
_player.setSource(videoSource, audioSource, _playWhenReady, false, resume = resumePositionMs > 0);
|
||||
if(subtitleSource != null)
|
||||
_player.swapSubtitles(fragment.lifecycleScope, subtitleSource);
|
||||
_player.seekTo(resumePositionMs);
|
||||
}
|
||||
else
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
_player.setSource(videoSource, audioSource, _playWhenReady && playWhenReady, false, resume = resumePositionMs > 0);
|
||||
if(subtitleSource != null)
|
||||
_player.swapSubtitles(subtitleSource);
|
||||
_player.seekTo(resumePositionMs);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "loadCurrentVideo failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
loadCurrentVideoCast(video, videoSource, audioSource, subtitleSource, resumePositionMs, Settings.instance.playback.getDefaultPlaybackSpeed().toDouble());
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "loadCurrentVideo failed (casting)", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_lastVideoSource = videoSource;
|
||||
_lastAudioSource = audioSource;
|
||||
@@ -1901,13 +2023,12 @@ class VideoDetailView : ConstraintLayout {
|
||||
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_load_media), ex);
|
||||
}
|
||||
}
|
||||
private fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) {
|
||||
private suspend fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) {
|
||||
Logger.i(TAG, "loadCurrentVideoCast(video=$video, videoSource=$videoSource, audioSource=$audioSource, resumePositionMs=$resumePositionMs)")
|
||||
castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed)
|
||||
}
|
||||
|
||||
private fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
private suspend fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) {
|
||||
try {
|
||||
val plugin = if (videoSource is JSSource) videoSource.getUnderlyingPlugin()
|
||||
else if (audioSource is JSSource) audioSource.getUnderlyingPlugin()
|
||||
@@ -1943,7 +2064,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
Logger.e(TAG, "loadCurrentVideoCast", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Events
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
@@ -1982,6 +2102,10 @@ class VideoDetailView : ConstraintLayout {
|
||||
videoTrackFormats.distinctBy { it.height }.sortedByDescending { it.height },
|
||||
audioTrackFormats.distinctBy { it.bitrate }.sortedByDescending { it.bitrate });
|
||||
}
|
||||
|
||||
_layoutPlayerContainer.post {
|
||||
onShouldEnterPictureInPictureChanged.emit()
|
||||
}
|
||||
}
|
||||
|
||||
private var _didTriggerDatasourceErrorCount = 0;
|
||||
@@ -2442,7 +2566,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
|
||||
isPlaying = playing;
|
||||
onPlayChanged.emit(playing);
|
||||
updateTracker(lastPositionMilliseconds, playing, true);
|
||||
}
|
||||
|
||||
@@ -2453,11 +2576,17 @@ class VideoDetailView : ConstraintLayout {
|
||||
if(_lastVideoSource == videoSource)
|
||||
return;
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
||||
castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
|
||||
else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true))
|
||||
_player.hideControls(false); //TODO: Disable player?
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "handleSelectVideoTrack failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
_lastVideoSource = videoSource;
|
||||
}
|
||||
@@ -2468,11 +2597,17 @@ class VideoDetailView : ConstraintLayout {
|
||||
if(_lastAudioSource == audioSource)
|
||||
return;
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
||||
castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed)
|
||||
else(!_player.swapSources(_lastVideoSource, audioSource, true, true, true))
|
||||
else if (!_player.swapSources(_lastVideoSource, audioSource, true, true, true))
|
||||
_player.hideControls(false); //TODO: Disable player?
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "handleSelectAudioTrack failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
_lastAudioSource = audioSource;
|
||||
}
|
||||
@@ -2484,12 +2619,18 @@ class VideoDetailView : ConstraintLayout {
|
||||
if(_lastSubtitleSource == subtitleSource)
|
||||
toSet = null;
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
||||
castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
|
||||
else
|
||||
_player.swapSubtitles(fragment.lifecycleScope, toSet);
|
||||
|
||||
else {
|
||||
_player.swapSubtitles(toSet);
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "handleSelectSubtitleTrack failed", e)
|
||||
}
|
||||
}
|
||||
_lastSubtitleSource = toSet;
|
||||
}
|
||||
|
||||
@@ -2576,6 +2717,9 @@ class VideoDetailView : ConstraintLayout {
|
||||
setProgressBarOverlayed(false);
|
||||
}
|
||||
onFullscreenChanged.emit(fullscreen);
|
||||
_layoutPlayerContainer.post {
|
||||
onShouldEnterPictureInPictureChanged.emit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCastEnabled(isCasting: Boolean) {
|
||||
@@ -2603,6 +2747,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
if (changed) {
|
||||
stopAllGestures();
|
||||
}
|
||||
|
||||
onShouldEnterPictureInPictureChanged.emit()
|
||||
}
|
||||
|
||||
fun isLandscapeVideo(): Boolean? {
|
||||
@@ -2833,6 +2979,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
_overlayContainer.removeAllViews();
|
||||
_overlay_quality_selector?.hide();
|
||||
_container_content.visibility = GONE
|
||||
|
||||
_player.fillHeight(false)
|
||||
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
|
||||
@@ -2841,6 +2988,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
Logger.i(TAG, "handleLeavePictureInPicture")
|
||||
|
||||
if(!_player.isFullScreen) {
|
||||
_container_content.visibility = VISIBLE
|
||||
_player.fitHeight();
|
||||
_layoutPlayerContainer.setPadding(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, Resources.getSystem().displayMetrics).toInt());
|
||||
} else {
|
||||
@@ -2856,29 +3004,40 @@ class VideoDetailView : ConstraintLayout {
|
||||
videoSourceHeight = 9;
|
||||
}
|
||||
val aspectRatio = videoSourceWidth.toDouble() / videoSourceHeight;
|
||||
val r = _player.getVideoRect()
|
||||
if(aspectRatio > 2.38) {
|
||||
videoSourceWidth = 16;
|
||||
videoSourceHeight = 9;
|
||||
|
||||
// shrink the left and right equally to get the rect to be 16 by 9 aspect ratio
|
||||
// we don't want a picture in picture mode that's more squashed than 16 by 9
|
||||
val targetWidth = r.height() * 16 / 9
|
||||
val shrinkAmount = (r.width() - targetWidth) / 2
|
||||
r.left += shrinkAmount
|
||||
r.right -= shrinkAmount
|
||||
}
|
||||
else if(aspectRatio < 0.43) {
|
||||
videoSourceHeight = 16;
|
||||
videoSourceWidth = 9;
|
||||
}
|
||||
|
||||
val r = Rect();
|
||||
_player.getGlobalVisibleRect(r);
|
||||
r.right = r.right - _player.paddingEnd;
|
||||
val playpauseAction = if(_player.playing)
|
||||
RemoteAction(Icon.createWithResource(context, R.drawable.ic_pause_notif), context.getString(R.string.pause), context.getString(R.string.pauses_the_video), MediaControlReceiver.getPauseIntent(context, 5));
|
||||
else
|
||||
RemoteAction(Icon.createWithResource(context, R.drawable.ic_play_notif), context.getString(R.string.play), context.getString(R.string.resumes_the_video), MediaControlReceiver.getPlayIntent(context, 6));
|
||||
|
||||
val toBackgroundAction = RemoteAction(Icon.createWithResource(context, R.drawable.ic_screen_share), context.getString(R.string.background), context.getString(R.string.background_switch_audio), MediaControlReceiver.getToBackgroundIntent(context, 7));
|
||||
return PictureInPictureParams.Builder()
|
||||
|
||||
val params = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(Rational(videoSourceWidth, videoSourceHeight))
|
||||
.setSourceRectHint(r)
|
||||
.setActions(listOf(toBackgroundAction, playpauseAction))
|
||||
.build();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
params.setAutoEnterEnabled(shouldEnterPictureInPicture)
|
||||
}
|
||||
|
||||
return params.build()
|
||||
}
|
||||
|
||||
//Other
|
||||
@@ -2896,6 +3055,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
private fun setLastPositionMilliseconds(positionMilliseconds: Long, updateHistory: Boolean) {
|
||||
lastPositionMilliseconds = positionMilliseconds;
|
||||
|
||||
_liveChat?.setVideoPosition(lastPositionMilliseconds);
|
||||
|
||||
val v = video ?: return;
|
||||
val currentTime = System.currentTimeMillis();
|
||||
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
|
||||
|
||||
@@ -13,6 +13,8 @@ import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||
import com.futo.platformplayer.timestampRegex
|
||||
import com.futo.platformplayer.views.behavior.NonScrollingTextView
|
||||
import com.futo.platformplayer.views.behavior.NonScrollingTextView.Companion
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -91,7 +93,11 @@ class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMe
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to start activity.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ class DownloadService : Service() {
|
||||
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
|
||||
private var _notificationManager: NotificationManager? = null;
|
||||
private var _notificationChannel: NotificationChannel? = null;
|
||||
private var _isForeground = false
|
||||
|
||||
private val _client = ManagedHttpClient(OkHttpClient.Builder()
|
||||
//.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(InetAddress.getByName("192.168.1.175"), 8081)))
|
||||
@@ -66,6 +67,7 @@ class DownloadService : Service() {
|
||||
|
||||
if(!FragmentedStorage.isInitialized) {
|
||||
Logger.i(TAG, "Attempted to start DownloadService without initialized files");
|
||||
stopSelf()
|
||||
closeDownloadSession();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
@@ -116,6 +118,22 @@ class DownloadService : Service() {
|
||||
override fun onCreate() {
|
||||
Logger.i(TAG, "onCreate");
|
||||
super.onCreate()
|
||||
|
||||
setupNotificationRequirements()
|
||||
|
||||
val bootstrapNotif = NotificationCompat.Builder(this, DOWNLOAD_NOTIF_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_download)
|
||||
.setContentTitle("Preparing downloads...")
|
||||
.setOngoing(true)
|
||||
.setSilent(true)
|
||||
.build()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
startForeground(DOWNLOAD_NOTIF_ID, bootstrapNotif, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
else
|
||||
startForeground(DOWNLOAD_NOTIF_ID, bootstrapNotif)
|
||||
|
||||
_isForeground = true
|
||||
}
|
||||
|
||||
override fun onBind(p0: Intent?): IBinder? {
|
||||
@@ -246,15 +264,14 @@ class DownloadService : Service() {
|
||||
}
|
||||
|
||||
private fun notifyDownload(download: VideoDownload?) {
|
||||
val channel = _notificationChannel ?: return;
|
||||
|
||||
val channelId = DOWNLOAD_NOTIF_CHANNEL_ID
|
||||
val bringUpIntent = Intent(this, MainActivity::class.java);
|
||||
bringUpIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||
bringUpIntent.action = "TAB";
|
||||
bringUpIntent.putExtra("TAB", "Downloads");
|
||||
|
||||
var builder = if(download != null)
|
||||
NotificationCompat.Builder(this, DOWNLOAD_NOTIF_TAG)
|
||||
val builder = if(download != null)
|
||||
NotificationCompat.Builder(this, channelId)
|
||||
.setSmallIcon(R.drawable.ic_download)
|
||||
.setOngoing(true)
|
||||
.setSilent(true)
|
||||
@@ -262,16 +279,16 @@ class DownloadService : Service() {
|
||||
.setContentTitle("${download.state}: ${download.name}")
|
||||
.setContentText(download.getDownloadInfo())
|
||||
.setProgress(100, (download.progress * 100).toInt(), download.progress == 0.0)
|
||||
.setChannelId(channel.id)
|
||||
.setChannelId(channelId)
|
||||
else
|
||||
NotificationCompat.Builder(this, DOWNLOAD_NOTIF_TAG)
|
||||
NotificationCompat.Builder(this, channelId)
|
||||
.setSmallIcon(R.drawable.ic_download)
|
||||
.setOngoing(true)
|
||||
.setSilent(true)
|
||||
.setContentIntent(PendingIntent.getActivity(this, 5, bringUpIntent, PendingIntent.FLAG_IMMUTABLE))
|
||||
.setContentTitle("Preparing for download...")
|
||||
.setContentText("Initializing download process...")
|
||||
.setChannelId(channel.id)
|
||||
.setChannelId(channelId)
|
||||
|
||||
val notif = builder.build();
|
||||
notif.flags = notif.flags or NotificationCompat.FLAG_ONGOING_EVENT or NotificationCompat.FLAG_NO_CLEAR;
|
||||
|
||||
@@ -657,6 +657,20 @@ class StateApp {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scopeOrNull?.launch(Dispatchers.IO) {
|
||||
val enabledPlugins = StatePlatform.instance.getEnabledClients();
|
||||
for(plugin in enabledPlugins) {
|
||||
try {
|
||||
if(plugin is JSClient) {
|
||||
if(plugin.descriptor.appSettings.sync.enableHistorySync == true)
|
||||
StateHistory.instance.syncRemoteHistory(plugin);
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to update remote history for ${plugin.name}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun mainAppStartedWithExternalFiles(context: Context) {
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package com.futo.platformplayer.states
|
||||
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.HistoryVideo
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.states.StatePlaylists.Companion
|
||||
import com.futo.platformplayer.states.StateApp.Companion
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringDateMapStorage
|
||||
import com.futo.platformplayer.stores.db.ManagedDBStore
|
||||
import com.futo.platformplayer.stores.db.types.DBHistory
|
||||
import com.futo.platformplayer.stores.v2.ReconstructStore
|
||||
@@ -19,7 +22,6 @@ import kotlinx.coroutines.launch
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentMap
|
||||
import kotlin.math.min
|
||||
|
||||
class StateHistory {
|
||||
//Legacy
|
||||
@@ -31,6 +33,8 @@ class StateHistory {
|
||||
})
|
||||
.load();
|
||||
|
||||
private val _remoteHistoryDatesStore = FragmentedStorage.get<StringDateMapStorage>("remoteHistoryDates");
|
||||
|
||||
private val historyIndex: ConcurrentMap<Any, DBHistory.Index> = ConcurrentHashMap();
|
||||
val _historyDBStore = ManagedDBStore.create("history", DBHistory.Descriptor())
|
||||
.withIndex({ it.url }, historyIndex, false, true)
|
||||
@@ -186,8 +190,95 @@ class StateHistory {
|
||||
val toDelete = _historyDBStore.getAllIndexes().filter { minutesToDelete == -1L || (now - it.datetime) < minutesToDelete * 60 };
|
||||
for(item in toDelete)
|
||||
_historyDBStore.delete(item);
|
||||
_remoteHistoryDatesStore.map = HashMap<String, Long>();
|
||||
_remoteHistoryDatesStore.save();
|
||||
}
|
||||
|
||||
fun syncRemoteHistory(plugin: JSClient) {
|
||||
if (plugin.capabilities.hasGetUserHistory &&
|
||||
plugin.isLoggedIn) {
|
||||
Logger.i(TAG, "Syncing remote history for plugin [${plugin.name}]");
|
||||
|
||||
val hist = StatePlatform.instance.getUserHistory(plugin.id);
|
||||
|
||||
syncRemoteHistory(plugin.id, hist, 100, 3);
|
||||
}
|
||||
}
|
||||
fun syncRemoteHistory(pluginId: String, videos: IPager<IPlatformContent>, maxVideos: Int, maxPages: Int) {
|
||||
val lastDate = _remoteHistoryDatesStore.get(pluginId) ?: OffsetDateTime.MIN;
|
||||
val maxVideosCount = if(maxVideos <= 0) 500 else maxVideos;
|
||||
val maxPageCount = if(maxPages <= 0) 3 else maxPages;
|
||||
var exceededDate = false;
|
||||
try {
|
||||
val toSync = mutableListOf<IPlatformVideo>();
|
||||
var pageCount = 0;
|
||||
var videoCount = 0;
|
||||
var isFirst = true;
|
||||
var oldestPlayback = OffsetDateTime.MAX;
|
||||
var newestPlayback = OffsetDateTime.MIN;
|
||||
do {
|
||||
if (!isFirst) videos.nextPage();
|
||||
val newVideos = videos.getResults();
|
||||
|
||||
var foundVideos = false;
|
||||
var toSyncAddedCount = 0;
|
||||
for(video in newVideos) {
|
||||
if(video is IPlatformVideo && video.playbackDate != null) {
|
||||
|
||||
if(video.playbackDate!! < lastDate) {
|
||||
exceededDate = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if(video.playbackTime > 0) {
|
||||
toSync.add(video);
|
||||
toSyncAddedCount++;
|
||||
foundVideos = true;
|
||||
oldestPlayback = video.playbackDate!!;
|
||||
if(newestPlayback == OffsetDateTime.MIN)
|
||||
newestPlayback = video.playbackDate!!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pageCount++;
|
||||
videoCount += newVideos.size;
|
||||
isFirst = false;
|
||||
|
||||
if(!foundVideos)
|
||||
{
|
||||
Logger.i(TAG, "Found no more videos in remote history");
|
||||
break;
|
||||
}
|
||||
}
|
||||
while(videos.hasMorePages() && videoCount <= maxVideosCount && pageCount <= maxPageCount && !exceededDate);
|
||||
|
||||
var updated = 0;
|
||||
if(oldestPlayback < OffsetDateTime.MAX) {
|
||||
for(video in toSync){
|
||||
val hist = getHistoryByVideo(video, true, video.playbackDate);
|
||||
if(hist != null && hist.position < video.playbackTime) {
|
||||
Logger.i(TAG, "Updated history for video [${video.name}] from remote history");
|
||||
updateHistoryPosition(video, hist, true, video.playbackTime, video.playbackDate, false);
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
if(updated > 0) {
|
||||
_remoteHistoryDatesStore.setAndSave(pluginId, newestPlayback);
|
||||
|
||||
try {
|
||||
val client = StatePlatform.instance.getClient(pluginId);
|
||||
UIDialogs.appToast("Updated ${updated} history from ${client.name}")
|
||||
}
|
||||
catch(ex: Throwable){}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
val plugin = if(pluginId != StateDeveloper.DEV_ID) StatePlugins.instance.getPlugin(pluginId) else null;
|
||||
Logger.e(TAG, "Sync Remote History failed for [${plugin?.config?.name}] due to: " + ex.message)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = "StateHistory";
|
||||
|
||||
@@ -463,6 +463,47 @@ class StatePlatform {
|
||||
pager.initialize();
|
||||
return pager;
|
||||
}
|
||||
fun getShorts(): IPager<IPlatformVideo> {
|
||||
Logger.i(TAG, "Platform - getShorts");
|
||||
var clientIdsOngoing = mutableListOf<String>();
|
||||
val clients = getSortedEnabledClient().filter { if (it is JSClient) it.enableInShorts else true };
|
||||
|
||||
StateApp.instance.scopeOrNull?.let {
|
||||
it.launch(Dispatchers.Default) {
|
||||
try {
|
||||
// plugins that take longer than 5 seconds to load are considered "slow"
|
||||
delay(5000);
|
||||
val slowClients = synchronized(clientIdsOngoing) {
|
||||
return@synchronized clients.filter { clientIdsOngoing.contains(it.id) };
|
||||
};
|
||||
for(client in slowClients)
|
||||
UIDialogs.toast("${client.name} is still loading..\nConsider disabling it for Home", false);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to show toast for slow source.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val pages = clients.parallelStream()
|
||||
.map {
|
||||
Logger.i(TAG, "getShorts - ${it.name}")
|
||||
synchronized(clientIdsOngoing) {
|
||||
clientIdsOngoing.add(it.id);
|
||||
}
|
||||
val shortsResult = it.fromPool(_pagerClientPool).getShorts();
|
||||
synchronized(clientIdsOngoing) {
|
||||
clientIdsOngoing.remove(it.id);
|
||||
}
|
||||
return@map shortsResult;
|
||||
}
|
||||
.asSequence()
|
||||
.toList()
|
||||
.associateWith { 1f };
|
||||
|
||||
val pager = MultiDistributionContentPager(pages);
|
||||
pager.initialize();
|
||||
return pager;
|
||||
}
|
||||
suspend fun getHomeRefresh(scope: CoroutineScope): IPager<IPlatformContent> {
|
||||
Logger.i(TAG, "Platform - getHome (Refresh)");
|
||||
val clients = getSortedEnabledClient().filter { if (it is JSClient) it.enableInHome else true };
|
||||
@@ -995,6 +1036,16 @@ class StatePlatform {
|
||||
return client.getLiveChatWindow(url);
|
||||
}
|
||||
|
||||
//Account
|
||||
fun getUserHistory(id: String): IPager<IPlatformContent> {
|
||||
val client = getClient(id);
|
||||
if(client is JSClient && client.isLoggedIn) {
|
||||
return client.fromPool(_pagerClientPool).getUserHistory()
|
||||
}
|
||||
return EmptyPager<IPlatformContent>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun injectDevPlugin(source: SourcePluginConfig, script: String): String? {
|
||||
var devId: String? = null;
|
||||
|
||||
@@ -38,6 +38,7 @@ class StatePlayer {
|
||||
//Players
|
||||
private var _exoplayer : PlayerManager? = null;
|
||||
private var _thumbnailExoPlayer : PlayerManager? = null;
|
||||
private var _shortExoPlayer: PlayerManager? = null
|
||||
|
||||
//Video Status
|
||||
var rotationLock: Boolean = false
|
||||
@@ -633,6 +634,13 @@ class StatePlayer {
|
||||
}
|
||||
return _thumbnailExoPlayer!!;
|
||||
}
|
||||
fun getShortPlayerOrCreate(context: Context) : PlayerManager {
|
||||
if(_shortExoPlayer == null) {
|
||||
val player = createExoPlayer(context);
|
||||
_shortExoPlayer = PlayerManager(player);
|
||||
}
|
||||
return _shortExoPlayer!!;
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun createExoPlayer(context : Context): ExoPlayer {
|
||||
@@ -656,10 +664,13 @@ class StatePlayer {
|
||||
fun dispose(){
|
||||
val player = _exoplayer;
|
||||
val thumbPlayer = _thumbnailExoPlayer;
|
||||
val shortPlayer = _shortExoPlayer
|
||||
_exoplayer = null;
|
||||
_thumbnailExoPlayer = null;
|
||||
_shortExoPlayer = null
|
||||
player?.release();
|
||||
thumbPlayer?.release();
|
||||
shortPlayer?.release()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -35,14 +35,17 @@ class PlayerManager {
|
||||
@Synchronized
|
||||
fun attach(view: PlayerView, stateName: String) {
|
||||
if (view != _currentView) {
|
||||
_currentView?.player = null;
|
||||
switchState(stateName);
|
||||
view.player = player;
|
||||
_currentView = view;
|
||||
_currentView?.player = null
|
||||
_currentView = null
|
||||
switchState(stateName)
|
||||
view.player = player
|
||||
_currentView = view
|
||||
}
|
||||
}
|
||||
|
||||
fun detach() {
|
||||
_currentView?.player = null;
|
||||
_currentView?.player = null
|
||||
_currentView = null
|
||||
}
|
||||
|
||||
fun getState(name: String): PlayerState {
|
||||
|
||||
@@ -9,8 +9,10 @@ import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.constructs.Event3
|
||||
import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment
|
||||
import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment
|
||||
import com.futo.platformplayer.fragment.channel.tab.ChannelListFragment
|
||||
@@ -38,6 +40,7 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
|
||||
val onContentUrlClicked = Event2<String, ContentType>()
|
||||
val onUrlClicked = Event1<String>()
|
||||
val onContentClicked = Event2<IPlatformContent, Long>()
|
||||
val onShortClicked = Event3<IPlatformContent, Long, Pair<IPager<IPlatformContent>, ArrayList<IPlatformContent>>?>()
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>()
|
||||
val onAddToClicked = Event1<IPlatformContent>()
|
||||
val onAddToQueueClicked = Event1<IPlatformContent>()
|
||||
@@ -81,7 +84,9 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
|
||||
when (_tabs[position]) {
|
||||
ChannelTab.VIDEOS -> {
|
||||
fragment = ChannelContentsFragment.newInstance().apply {
|
||||
onContentClicked.subscribe(this@ChannelViewPagerAdapter.onContentClicked::emit)
|
||||
onContentClicked.subscribe { video, num, _ ->
|
||||
this@ChannelViewPagerAdapter.onContentClicked.emit(video, num)
|
||||
}
|
||||
onContentUrlClicked.subscribe(this@ChannelViewPagerAdapter.onContentUrlClicked::emit)
|
||||
onUrlClicked.subscribe(this@ChannelViewPagerAdapter.onUrlClicked::emit)
|
||||
onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit)
|
||||
@@ -94,7 +99,7 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
|
||||
|
||||
ChannelTab.SHORTS -> {
|
||||
fragment = ChannelContentsFragment.newInstance(ResultCapabilities.TYPE_SHORTS).apply {
|
||||
onContentClicked.subscribe(this@ChannelViewPagerAdapter.onContentClicked::emit)
|
||||
onContentClicked.subscribe(this@ChannelViewPagerAdapter.onShortClicked::emit)
|
||||
onContentUrlClicked.subscribe(this@ChannelViewPagerAdapter.onContentUrlClicked::emit)
|
||||
onUrlClicked.subscribe(this@ChannelViewPagerAdapter.onUrlClicked::emit)
|
||||
onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit)
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ abstract class ContentPreviewViewHolder(itemView: View) : ViewHolder(itemView) {
|
||||
|
||||
abstract fun bind(content: IPlatformContent);
|
||||
|
||||
abstract fun preview(details: IPlatformContentDetails?, paused: Boolean);
|
||||
abstract suspend fun preview(details: IPlatformContentDetails?, paused: Boolean);
|
||||
abstract fun stopPreview();
|
||||
abstract fun pausePreview();
|
||||
abstract fun resumePreview();
|
||||
|
||||
@@ -11,7 +11,7 @@ class EmptyPreviewViewHolder(viewGroup: ViewGroup) : ContentPreviewViewHolder(Vi
|
||||
|
||||
override fun bind(content: IPlatformContent) {}
|
||||
|
||||
override fun preview(details: IPlatformContentDetails?, paused: Boolean) {}
|
||||
override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) {}
|
||||
|
||||
override fun stopPreview() {}
|
||||
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ class PreviewChannelViewHolder : ContentPreviewViewHolder {
|
||||
|
||||
override fun bind(content: IPlatformContent) = view.bind(content);
|
||||
|
||||
override fun preview(details: IPlatformContentDetails?, paused: Boolean) = Unit;
|
||||
override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) = Unit;
|
||||
override fun stopPreview() = Unit;
|
||||
override fun pausePreview() = Unit;
|
||||
override fun resumePreview() = Unit;
|
||||
|
||||
+36
-11
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
@@ -15,6 +16,8 @@ import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.debug.Stopwatch
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ShortView
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ShortView.Companion
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
@@ -23,6 +26,9 @@ import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||
import com.futo.platformplayer.views.adapters.EmptyPreviewViewHolder
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.internal.platform.Platform
|
||||
|
||||
class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewViewHolder> {
|
||||
@@ -33,6 +39,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||
private val _feedStyle : FeedStyle;
|
||||
private var _paused: Boolean = false;
|
||||
private val _shouldShowTimeBar: Boolean
|
||||
private val _scope: CoroutineScope
|
||||
|
||||
val onUrlClicked = Event1<String>();
|
||||
val onContentUrlClicked = Event2<String, ContentType>();
|
||||
@@ -43,15 +50,9 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
|
||||
val onLongPress = Event1<IPlatformContent>();
|
||||
|
||||
private var _taskLoadContent = TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>(
|
||||
StateApp.instance.scopeGetter, { (viewHolder, video) ->
|
||||
val stopwatch = Stopwatch()
|
||||
val contentDetails = StatePlatform.instance.getContentDetails(video.url).await();
|
||||
stopwatch.logAndNext(TAG, "Retrieving video detail (IO thread)")
|
||||
return@TaskHandler Pair(viewHolder, contentDetails)
|
||||
}).exception<Throwable> { Logger.e(TAG, "Failed to retrieve preview content.", it) }.success { previewContentDetails(it.first, it.second) }
|
||||
private var _taskLoadContent: TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>
|
||||
|
||||
constructor(context: Context, feedStyle : FeedStyle, dataSet: ArrayList<IPlatformContent>, exoPlayer: PlayerManager? = null,
|
||||
constructor(scope: CoroutineScope, context: Context, feedStyle : FeedStyle, dataSet: ArrayList<IPlatformContent>, exoPlayer: PlayerManager? = null,
|
||||
initialPlay: Boolean = false, viewsToPrepend: ArrayList<View> = arrayListOf(),
|
||||
viewsToAppend: ArrayList<View> = arrayListOf(), shouldShowTimeBar: Boolean = true) : super(context, viewsToPrepend, viewsToAppend) {
|
||||
|
||||
@@ -60,6 +61,24 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||
this._initialPlay = initialPlay;
|
||||
this._exoPlayer = exoPlayer;
|
||||
this._shouldShowTimeBar = shouldShowTimeBar
|
||||
this._scope = scope
|
||||
|
||||
_taskLoadContent = TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>(
|
||||
{ scope }, { (viewHolder, video) ->
|
||||
val stopwatch = Stopwatch()
|
||||
val contentDetails = StatePlatform.instance.getContentDetails(video.url).await();
|
||||
stopwatch.logAndNext(TAG, "Retrieving video detail (IO thread)")
|
||||
return@TaskHandler Pair(viewHolder, contentDetails)
|
||||
}).exception<Throwable> { Logger.e(TAG, "Failed to retrieve preview content.", it) }.success {
|
||||
|
||||
_scope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
previewContentDetails(it.first, it.second)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "bindChild preview failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getChildCount(): Int = _dataSet.size;
|
||||
@@ -132,12 +151,18 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||
_initialPlay = false;
|
||||
|
||||
if (_feedStyle != FeedStyle.THUMBNAIL) {
|
||||
preview(holder);
|
||||
_scope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
preview(holder)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "bindChild preview failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun preview(viewHolder: ContentPreviewViewHolder) {
|
||||
suspend fun preview(viewHolder: ContentPreviewViewHolder) {
|
||||
Log.v(TAG, "previewing content");
|
||||
if (viewHolder == _previewingViewHolder)
|
||||
return
|
||||
@@ -175,7 +200,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||
onAddToWatchLaterClicked.clear();
|
||||
}
|
||||
|
||||
private fun previewContentDetails(viewHolder: ContentPreviewViewHolder, videoDetails: IPlatformContentDetails?) {
|
||||
private suspend fun previewContentDetails(viewHolder: ContentPreviewViewHolder, videoDetails: IPlatformContentDetails?) {
|
||||
_previewingViewHolder?.stopPreview();
|
||||
viewHolder.preview(videoDetails, _paused);
|
||||
_previewingViewHolder = viewHolder;
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ class PreviewLockedViewHolder : ContentPreviewViewHolder {
|
||||
|
||||
override fun bind(content: IPlatformContent) = view.bind(content);
|
||||
|
||||
override fun preview(details: IPlatformContentDetails?, paused: Boolean) { }
|
||||
override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) { }
|
||||
override fun stopPreview() { }
|
||||
override fun pausePreview() { }
|
||||
override fun resumePreview() { }
|
||||
|
||||
+1
-1
@@ -185,7 +185,7 @@ class PreviewNestedVideoView : PreviewVideoView {
|
||||
}
|
||||
}
|
||||
|
||||
override fun preview(video: IPlatformContentDetails?, paused: Boolean) {
|
||||
override suspend fun preview(video: IPlatformContentDetails?, paused: Boolean) {
|
||||
if(video != null) {
|
||||
super.preview(video, paused);
|
||||
} else if(_content is IPlatformVideoDetails) _contentNested?.let {
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ class PreviewNestedVideoViewHolder : ContentPreviewViewHolder {
|
||||
view.bind(content);
|
||||
}
|
||||
|
||||
override fun preview(details: IPlatformContentDetails?, paused: Boolean) {
|
||||
override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) {
|
||||
view.preview(details, paused);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ class PreviewPlaceholderViewHolder : ContentPreviewViewHolder {
|
||||
}
|
||||
}
|
||||
|
||||
override fun preview(details: IPlatformContentDetails?, paused: Boolean) { }
|
||||
override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) { }
|
||||
override fun stopPreview() { }
|
||||
override fun pausePreview() { }
|
||||
override fun resumePreview() { }
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ class PreviewPlaylistViewHolder : ContentPreviewViewHolder {
|
||||
|
||||
override fun bind(content: IPlatformContent) = view.bind(content);
|
||||
|
||||
override fun preview(details: IPlatformContentDetails?, paused: Boolean) = Unit;
|
||||
override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) = Unit;
|
||||
override fun stopPreview() = Unit;
|
||||
override fun pausePreview() = Unit;
|
||||
override fun resumePreview() = Unit;
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ class PreviewPostViewHolder : ContentPreviewViewHolder {
|
||||
|
||||
override fun bind(content: IPlatformContent) = view.bind(content);
|
||||
|
||||
override fun preview(details: IPlatformContentDetails?, paused: Boolean) {};
|
||||
override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) {};
|
||||
override fun stopPreview() {};
|
||||
override fun pausePreview() {};
|
||||
override fun resumePreview() {};
|
||||
|
||||
+1
-1
@@ -248,7 +248,7 @@ open class PreviewVideoView : LinearLayout {
|
||||
_textVideoMetadata.text = metadata + timeMeta;
|
||||
}
|
||||
|
||||
open fun preview(video: IPlatformContentDetails?, paused: Boolean) {
|
||||
open suspend fun preview(video: IPlatformContentDetails?, paused: Boolean) {
|
||||
if(video == null)
|
||||
return;
|
||||
Logger.i(TAG, "Previewing");
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ class PreviewVideoViewHolder : ContentPreviewViewHolder {
|
||||
|
||||
override fun bind(content: IPlatformContent) = view.bind(content);
|
||||
|
||||
override fun preview(details: IPlatformContentDetails?, paused: Boolean) = view.preview(details, paused);
|
||||
override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) = view.preview(details, paused);
|
||||
override fun stopPreview() = view.stopPreview();
|
||||
override fun pausePreview() = view.pausePreview();
|
||||
override fun resumePreview() = view.resumePreview();
|
||||
|
||||
@@ -108,12 +108,20 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to start activity.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
try {
|
||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to start activity.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ package com.futo.platformplayer.views.overlays
|
||||
|
||||
import android.animation.LayoutTransition
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.PointF
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.View
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Button
|
||||
@@ -19,6 +22,7 @@ import androidx.recyclerview.widget.LinearSmoothScroller
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.LiveChatManager
|
||||
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
|
||||
import com.futo.platformplayer.api.media.models.live.ILiveEventChatMessage
|
||||
@@ -41,6 +45,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import toAndroidColor
|
||||
import androidx.core.net.toUri
|
||||
|
||||
|
||||
class LiveChatOverlay : LinearLayout {
|
||||
@@ -92,6 +97,7 @@ class LiveChatOverlay : LinearLayout {
|
||||
|
||||
val onRaidNow = Event1<LiveEventRaid>();
|
||||
val onRaidPrevent = Event1<LiveEventRaid>();
|
||||
val onUrlClick = Event1<Uri>()
|
||||
|
||||
private val _argJsonSerializer = Json;
|
||||
|
||||
@@ -116,6 +122,18 @@ class LiveChatOverlay : LinearLayout {
|
||||
view?.evaluateJavascript("setInterval(()=>{" + toRemoveJSInterval + "}, 1000)") {};
|
||||
};
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
|
||||
onUrlClick.emit(request.url)
|
||||
return true
|
||||
}
|
||||
|
||||
// API < 24
|
||||
@Suppress("DEPRECATION")
|
||||
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||
onUrlClick.emit(url.toUri())
|
||||
return true
|
||||
}
|
||||
};
|
||||
|
||||
_chatContainer = findViewById(R.id.chatContainer);
|
||||
@@ -202,6 +220,8 @@ class LiveChatOverlay : LinearLayout {
|
||||
|
||||
if(viewerCount != null)
|
||||
_textViewers.text = viewerCount.toHumanNumber() + " " + context.getString(R.string.viewers);
|
||||
else if(manager != null && manager.isVOD)
|
||||
_textViewers.text = manager.viewCount.toHumanNumber() + " past viewers";
|
||||
else if(manager != null)
|
||||
_textViewers.text = manager.viewCount.toHumanNumber() + " " + context.getString(R.string.viewers);
|
||||
else
|
||||
|
||||
@@ -19,7 +19,9 @@ class WebviewOverlay : LinearLayout {
|
||||
inflate(context, R.layout.overlay_webview, this)
|
||||
_topbar = findViewById(R.id.topbar);
|
||||
_webview = findViewById(R.id.webview);
|
||||
if (!isInEditMode){
|
||||
_webview.settings.javaScriptEnabled = true;
|
||||
}
|
||||
|
||||
_topbar.onClose.subscribe(this, onClose::emit);
|
||||
}
|
||||
|
||||
@@ -68,15 +68,7 @@ class CommentsList : ConstraintLayout {
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadNextPage() });
|
||||
};
|
||||
|
||||
private val _scrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
onScrolled();
|
||||
|
||||
val totalScrollDistance = recyclerView.computeVerticalScrollOffset()
|
||||
_layoutScrollToTop.visibility = if (totalScrollDistance > recyclerView.height) View.VISIBLE else View.GONE
|
||||
}
|
||||
};
|
||||
private val _scrollListener: RecyclerView.OnScrollListener
|
||||
|
||||
private var _loader: (suspend () -> IPager<IPlatformComment>)? = null;
|
||||
private val _adapterComments: InsertedViewAdapterWithLoader<CommentViewHolder>;
|
||||
@@ -131,6 +123,14 @@ class CommentsList : ConstraintLayout {
|
||||
_llmReplies = LinearLayoutManager(context);
|
||||
_recyclerComments.layoutManager = _llmReplies;
|
||||
_recyclerComments.adapter = _adapterComments;
|
||||
_scrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
onScrolled();
|
||||
|
||||
_layoutScrollToTop.visibility = if (_llmReplies.findFirstCompletelyVisibleItemPosition() > 5) View.VISIBLE else View.GONE
|
||||
}
|
||||
};
|
||||
_recyclerComments.addOnScrollListener(_scrollListener);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.futo.platformplayer.views.video
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.animation.LinearInterpolator
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.DefaultTimeBar
|
||||
import androidx.media3.ui.PlayerView
|
||||
import androidx.media3.ui.TimeBar
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.video.PlayerManager
|
||||
|
||||
@UnstableApi
|
||||
class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) :
|
||||
FutoVideoPlayerBase(PLAYER_STATE_NAME, context, attrs) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FutoShortVideoPlayer"
|
||||
private const val PLAYER_STATE_NAME: String = "ShortPlayer"
|
||||
}
|
||||
|
||||
private var playerAttached = false
|
||||
private val videoView: PlayerView
|
||||
private val progressBar: DefaultTimeBar
|
||||
private lateinit var player: PlayerManager
|
||||
private var progressAnimator: ValueAnimator = createProgressBarAnimator()
|
||||
|
||||
val onPlaybackStateChanged = Event1<Int>();
|
||||
|
||||
private var playerEventListener = object : Player.Listener {
|
||||
override fun onEvents(player: Player, events: Player.Events) {
|
||||
if (events.containsAny(
|
||||
Player.EVENT_POSITION_DISCONTINUITY, Player.EVENT_IS_PLAYING_CHANGED, Player.EVENT_PLAYBACK_STATE_CHANGED
|
||||
)
|
||||
) {
|
||||
progressAnimator.cancel()
|
||||
if (player.duration >= 0) {
|
||||
progressAnimator.duration = player.duration
|
||||
setProgressBarDuration(player.duration)
|
||||
progressAnimator.currentPlayTime = player.currentPosition
|
||||
}
|
||||
|
||||
if (player.isPlaying) {
|
||||
progressAnimator.start()
|
||||
}
|
||||
}
|
||||
|
||||
if (events.containsAny(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
|
||||
onPlaybackStateChanged.emit(player.playbackState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_short_player, this, true)
|
||||
videoView = findViewById(R.id.short_player_view)
|
||||
progressBar = findViewById(R.id.short_player_progress_bar)
|
||||
|
||||
if (!isInEditMode) {
|
||||
player = StatePlayer.instance.getShortPlayerOrCreate(context)
|
||||
player.player.repeatMode = Player.REPEAT_MODE_ONE
|
||||
}
|
||||
|
||||
progressBar.addListener(object : TimeBar.OnScrubListener {
|
||||
override fun onScrubStart(timeBar: TimeBar, position: Long) {
|
||||
progressAnimator.cancel()
|
||||
}
|
||||
|
||||
override fun onScrubMove(timeBar: TimeBar, position: Long) {}
|
||||
|
||||
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
|
||||
if (canceled) {
|
||||
progressAnimator.currentPlayTime = player.player.currentPosition
|
||||
progressAnimator.duration = player.player.duration
|
||||
progressAnimator.start()
|
||||
return
|
||||
}
|
||||
|
||||
// the progress bar should never be available to the user without the player being attached to this view
|
||||
assert(playerAttached)
|
||||
seekTo(position)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun createProgressBarAnimator(): ValueAnimator {
|
||||
return ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
interpolator = LinearInterpolator()
|
||||
|
||||
addUpdateListener { animation ->
|
||||
progressBar.setPosition(animation.currentPlayTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setProgressBarDuration(duration: Long) {
|
||||
progressBar.setDuration(duration)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches this short player instance to the exo player instance for shorts
|
||||
*/
|
||||
fun attach() {
|
||||
// connect the exo player for shorts to the view for this instance
|
||||
player.attach(videoView, PLAYER_STATE_NAME)
|
||||
|
||||
// direct the base player what exo player instance to use
|
||||
changePlayer(player)
|
||||
|
||||
playerAttached = true
|
||||
|
||||
player.player.addListener(playerEventListener)
|
||||
}
|
||||
|
||||
fun detach() {
|
||||
playerAttached = false
|
||||
player.player.removeListener(playerEventListener)
|
||||
player.detach()
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun setArtwork(drawable: Drawable?) {
|
||||
if (drawable != null) {
|
||||
videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_FILL
|
||||
videoView.defaultArtwork = drawable
|
||||
} else {
|
||||
videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_OFF
|
||||
videoView.defaultArtwork = null
|
||||
}
|
||||
}
|
||||
|
||||
fun getPlaybackRate(): Float {
|
||||
return exoPlayer?.player?.playbackParameters?.speed ?: 1.0f
|
||||
}
|
||||
|
||||
fun setPlaybackRate(playbackRate: Float) {
|
||||
val exoPlayer = exoPlayer?.player
|
||||
Logger.i(TAG, "setPlaybackRate playbackRate=$playbackRate exoPlayer=${exoPlayer}")
|
||||
|
||||
val param = PlaybackParameters(playbackRate)
|
||||
exoPlayer?.playbackParameters = param
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,7 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
|
||||
_evMuteChanged.add(callback);
|
||||
}
|
||||
|
||||
fun setPreview(video: IPlatformVideoDetails) {
|
||||
suspend fun setPreview(video: IPlatformVideoDetails) {
|
||||
if (video.live != null) {
|
||||
setSource(video.live, null,true, false);
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,10 @@ import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
@@ -29,6 +32,9 @@ import androidx.media3.ui.AspectRatioFrameLayout
|
||||
import androidx.media3.ui.PlayerControlView
|
||||
import androidx.media3.ui.PlayerView
|
||||
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.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
@@ -36,6 +42,7 @@ 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.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
@@ -491,6 +498,13 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
_control_autoplay_fullscreen.setColorFilter(ContextCompat.getColor(context, if (StatePlayer.instance.autoplay) com.futo.futopay.R.color.primary else R.color.white))
|
||||
}
|
||||
|
||||
fun getVideoRect(): Rect {
|
||||
val r = Rect()
|
||||
// this is the only way i could reliably get a reference to a view that matches perfectly with the video playback
|
||||
_videoView.subtitleView?.getGlobalVisibleRect(r)
|
||||
return r
|
||||
}
|
||||
|
||||
private fun setSystemBrightness(brightness: Float) {
|
||||
Log.i(TAG, "setSystemBrightness $brightness")
|
||||
if (android.provider.Settings.System.canWrite(context)) {
|
||||
@@ -890,4 +904,29 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
_loaderGame.visibility = View.VISIBLE
|
||||
_loaderGame.startLoader(expectedDurationMs.toLong())
|
||||
}
|
||||
|
||||
override fun switchToVideoMode() {
|
||||
super.switchToVideoMode()
|
||||
setArtwork(null)
|
||||
}
|
||||
|
||||
override fun switchToAudioMode(video: IPlatformVideoDetails?) {
|
||||
super.switchToAudioMode(video)
|
||||
val thumbnail = video?.thumbnails?.getHQThumbnail()
|
||||
if (!thumbnail.isNullOrBlank()) {
|
||||
Glide.with(context).asBitmap().load(thumbnail)
|
||||
.into(object : CustomTarget<Bitmap>() {
|
||||
override fun onResourceReady(
|
||||
resource: Bitmap,
|
||||
transition: Transition<in Bitmap>?
|
||||
) {
|
||||
setArtwork(BitmapDrawable(resources, resource));
|
||||
}
|
||||
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
setArtwork(null);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
package com.futo.platformplayer.views.video
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -30,6 +34,10 @@ import androidx.media3.exoplayer.source.MergingMediaSource
|
||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||
import androidx.media3.exoplayer.source.SingleSampleMediaSource
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
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.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
@@ -86,8 +94,6 @@ import kotlin.math.abs
|
||||
abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
private val TAG = "FutoVideoPlayerBase"
|
||||
|
||||
private val TEMP_DIRECTORY = StateApp.instance.getTempDirectory();
|
||||
|
||||
private var _mediaSource: MediaSource? = null;
|
||||
|
||||
var lastVideoSource: IVideoSource? = null
|
||||
@@ -267,7 +273,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
StateApp.instance.onConnectionAvailable.remove(_referenceObject);
|
||||
}
|
||||
|
||||
fun switchToVideoMode() {
|
||||
open fun switchToVideoMode() {
|
||||
Logger.i(TAG, "Switching to Video Mode");
|
||||
isAudioMode = false;
|
||||
val player = exoPlayer ?: return
|
||||
@@ -277,7 +283,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, isAudioMode)
|
||||
.build()
|
||||
}
|
||||
fun switchToAudioMode() {
|
||||
open fun switchToAudioMode(video: IPlatformVideoDetails?) {
|
||||
Logger.i(TAG, "Switching to Audio Mode");
|
||||
isAudioMode = true;
|
||||
val player = exoPlayer ?: return
|
||||
@@ -359,10 +365,11 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
return _chapters?.let { chaps -> chaps.find { pos.toDouble() / 1000 > it.timeStart && pos.toDouble() / 1000 < it.timeEnd && (toIgnore.isEmpty() || !toIgnore.contains(it)) } };
|
||||
}
|
||||
|
||||
fun setSource(videoSource: IVideoSource?, audioSource: IAudioSource? = null, play: Boolean = false, keepSubtitles: Boolean = false, resume: Boolean = false) {
|
||||
suspend fun setSource(videoSource: IVideoSource?, audioSource: IAudioSource? = null, play: Boolean = false, keepSubtitles: Boolean = false, resume: Boolean = false) {
|
||||
swapSources(videoSource, audioSource,resume, play, keepSubtitles);
|
||||
}
|
||||
fun swapSources(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true, keepSubtitles: Boolean = false): Boolean {
|
||||
suspend fun swapSources(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true, keepSubtitles: Boolean = false): Boolean {
|
||||
val didSet = withContext(Dispatchers.IO) {
|
||||
var videoSourceUsed = videoSource;
|
||||
var audioSourceUsed = audioSource;
|
||||
if(videoSource is JSDashManifestRawSource && audioSource is JSDashManifestRawAudioSource){
|
||||
@@ -376,31 +383,45 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
val didSetAudio = swapSourceInternal(audioSourceUsed, play, resume);
|
||||
if(!keepSubtitles)
|
||||
_lastSubtitleMediaSource = null;
|
||||
if(didSetVideo && didSetAudio)
|
||||
return loadSelectedSources(play, resume);
|
||||
else
|
||||
return true;
|
||||
|
||||
return@withContext didSetVideo && didSetAudio
|
||||
}
|
||||
fun swapSource(videoSource: IVideoSource?, resume: Boolean = true, play: Boolean = true): Boolean {
|
||||
|
||||
return withContext(Dispatchers.Main) {
|
||||
if (didSet)
|
||||
return@withContext loadSelectedSources(play, resume)
|
||||
else
|
||||
return@withContext true
|
||||
}
|
||||
}
|
||||
suspend fun swapSource(videoSource: IVideoSource?, resume: Boolean = true, play: Boolean = true): Boolean {
|
||||
val didSet = withContext(Dispatchers.IO) {
|
||||
var videoSourceUsed = videoSource;
|
||||
if (videoSource is JSDashManifestRawSource && lastVideoSource is JSDashManifestMergingRawSource)
|
||||
videoSourceUsed = JSDashManifestMergingRawSource(videoSource, (lastVideoSource as JSDashManifestMergingRawSource).audio);
|
||||
val didSet = swapSourceInternal(videoSourceUsed, play, resume);
|
||||
if(didSet)
|
||||
return loadSelectedSources(play, resume);
|
||||
else
|
||||
return true;
|
||||
return@withContext swapSourceInternal(videoSourceUsed, play, resume);
|
||||
}
|
||||
fun swapSource(audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true): Boolean {
|
||||
return withContext(Dispatchers.Main) {
|
||||
if (didSet)
|
||||
return@withContext loadSelectedSources(play, resume);
|
||||
else
|
||||
return@withContext true;
|
||||
}
|
||||
}
|
||||
suspend fun swapSource(audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true): Boolean {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (audioSource is JSDashManifestRawAudioSource && lastVideoSource is JSDashManifestMergingRawSource)
|
||||
swapSourceInternal(JSDashManifestMergingRawSource((lastVideoSource as JSDashManifestMergingRawSource).video, audioSource), play, resume);
|
||||
else
|
||||
swapSourceInternal(audioSource, play, resume);
|
||||
return loadSelectedSources(play, resume);
|
||||
}
|
||||
return withContext(Dispatchers.Main) {
|
||||
return@withContext loadSelectedSources(play, resume);
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun swapSubtitles(scope: CoroutineScope, subtitles: ISubtitleSource?) {
|
||||
suspend fun swapSubtitles(subtitles: ISubtitleSource?) {
|
||||
if(subtitles == null)
|
||||
clearSubtitles();
|
||||
else {
|
||||
@@ -414,9 +435,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
C.TIME_UNSET);
|
||||
loadSelectedSources(true, true);
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val subUri = subtitles.getSubtitlesURI() ?: return@launch;
|
||||
val subUri = subtitles.getSubtitlesURI() ?: return@withContext;
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
_lastSubtitleMediaSource = SingleSampleMediaSource.Factory(DefaultDataSource.Factory(context, DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)))
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<gradient
|
||||
android:centerX="50%"
|
||||
android:centerY="50%"
|
||||
android:endColor="#00FFFFFF"
|
||||
android:gradientRadius="35%p"
|
||||
android:startColor="#40FFFFFF"
|
||||
android:type="radial" />
|
||||
</shape>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6.31516 18.2609C6.86932 17.8571 7.58672 17.754 8.23108 17.9859C9.36947 18.3983 10.6453 18.6346 12.0028 18.6346C17.3596 18.6346 20.938 15.1765 20.938 11.7613C20.938 8.3462 17.3596 4.88809 12.0028 4.88809C6.64594 4.88809 3.06755 8.3462 3.06755 11.7613C3.06755 13.136 3.60022 14.4591 4.60114 15.5932C4.97058 16.0099 5.151 16.5597 5.10805 17.1182C5.04791 17.8957 4.86319 18.6088 4.62262 19.2403C5.35291 18.9009 5.95861 18.5229 6.31516 18.2652V18.2609ZM1.91628 20.005C1.9936 19.889 2.06663 19.773 2.13536 19.657C2.56494 18.9439 2.97304 18.0074 3.05466 16.955C1.76592 15.4901 1.00557 13.6987 1.00557 11.7613C1.00557 6.82549 5.92854 2.82611 12.0028 2.82611C18.077 2.82611 23 6.82549 23 11.7613C23 16.6972 18.077 20.6966 12.0028 20.6966C10.409 20.6966 8.89693 20.4217 7.53087 19.9276C7.01967 20.3014 6.18629 20.8126 5.19826 21.2422C4.54959 21.5257 3.81072 21.7834 3.04607 21.9338C3.0117 21.9424 2.97734 21.9467 2.94297 21.9553C2.75396 21.9896 2.56924 22.0197 2.37593 22.0369C2.36733 22.0369 2.35445 22.0412 2.34586 22.0412C2.12677 22.0626 1.90769 22.0755 1.6886 22.0755C1.40937 22.0755 1.16022 21.908 1.05282 21.6503C0.945429 21.3925 1.00557 21.1004 1.19888 20.9028C1.37501 20.7224 1.53395 20.529 1.6843 20.3229C1.75733 20.224 1.82607 20.1252 1.8905 20.0264C1.8948 20.0179 1.89909 20.0136 1.90339 20.005H1.91628Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M13.7562 3.3125C13.24 1.5625 10.76 1.5625 10.2437 3.3125L10.1187 3.7375C10.0416 3.99949 9.90682 4.2409 9.72424 4.44402C9.54166 4.64713 9.31594 4.80681 9.06362 4.91133C8.8113 5.01585 8.53878 5.06258 8.26606 5.04807C7.99333 5.03356 7.72731 4.9582 7.4875 4.8275L7.1 4.615C5.49625 3.7425 3.7425 5.49625 4.61625 7.09875L4.8275 7.4875C5.385 8.5125 4.85625 9.78875 3.7375 10.1187L3.3125 10.2437C1.5625 10.76 1.5625 13.24 3.3125 13.7562L3.7375 13.8813C3.99949 13.9584 4.2409 14.0932 4.44402 14.2758C4.64713 14.4583 4.80681 14.6841 4.91133 14.9364C5.01585 15.1887 5.06258 15.4612 5.04807 15.7339C5.03356 16.0067 4.9582 16.2727 4.8275 16.5125L4.615 16.9C3.7425 18.5037 5.49625 20.2575 7.09875 19.3837L7.4875 19.1725C7.72731 19.0418 7.99333 18.9664 8.26606 18.9519C8.53878 18.9374 8.8113 18.9841 9.06362 19.0887C9.31594 19.1932 9.54166 19.3529 9.72424 19.556C9.90682 19.7591 10.0416 20.0005 10.1187 20.2625L10.2437 20.6875C10.76 22.4375 13.24 22.4375 13.7562 20.6875L13.8813 20.2625C13.9584 20.0005 14.0932 19.7591 14.2758 19.556C14.4583 19.3529 14.6841 19.1932 14.9364 19.0887C15.1887 18.9841 15.4612 18.9374 15.7339 18.9519C16.0067 18.9664 16.2727 19.0418 16.5125 19.1725L16.9 19.385C18.5037 20.2575 20.2575 18.5037 19.3837 16.9012L19.1725 16.5125C19.0418 16.2727 18.9664 16.0067 18.9519 15.7339C18.9374 15.4612 18.9841 15.1887 19.0887 14.9364C19.1932 14.6841 19.3529 14.4583 19.556 14.2758C19.7591 14.0932 20.0005 13.9584 20.2625 13.8813L20.6875 13.7562C22.4375 13.24 22.4375 10.76 20.6875 10.2437L20.2625 10.1187C20.0005 10.0416 19.7591 9.90682 19.556 9.72424C19.3529 9.54166 19.1932 9.31594 19.0887 9.06362C18.9841 8.8113 18.9374 8.53878 18.9519 8.26606C18.9664 7.99333 19.0418 7.72731 19.1725 7.4875L19.385 7.1C20.2575 5.49625 18.5037 3.7425 16.9012 4.61625L16.5125 4.8275C16.2727 4.9582 16.0067 5.03356 15.7339 5.04807C15.4612 5.06258 15.1887 5.01585 14.9364 4.91133C14.6841 4.80681 14.4583 4.64713 14.2758 4.44402C14.0932 4.2409 13.9584 3.99949 13.8813 3.7375L13.7562 3.3125ZM12 15.6625C11.0286 15.6625 10.0971 15.2766 9.41022 14.5898C8.72337 13.9029 8.3375 12.9714 8.3375 12C8.3375 11.0286 8.72337 10.0971 9.41022 9.41022C10.0971 8.72337 11.0286 8.3375 12 8.3375C12.971 8.3375 13.9023 8.72324 14.5889 9.40985C15.2755 10.0965 15.6613 11.0277 15.6613 11.9987C15.6613 12.9698 15.2755 13.901 14.5889 14.5876C13.9023 15.2743 12.971 15.66 12 15.66V15.6625Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4.8338 12.6821H2C2 15.083 2.93455 17.5131 4.7735 19.2991C8.48155 22.9003 14.5109 22.9003 18.219 19.2991C21.927 15.6978 21.927 9.8421 18.219 6.24085C16.5307 4.60125 14.33 3.7229 12.0992 3.57651V1L4.8338 5.06971L12.0992 9.1687V6.32869C13.6065 6.4458 15.0536 7.08993 16.1991 8.20251C18.8219 10.7205 18.8219 14.8194 16.1991 17.3374C13.6065 19.8846 9.38596 19.8846 6.79334 17.3374C5.46688 16.0784 4.80365 14.351 4.8338 12.6821Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11.2046 1.70818C11.4156 1.50196 11.7017 1.38611 12 1.38611C12.2983 1.38611 12.5844 1.50196 12.7954 1.70818L16.1704 5.00821C16.3753 5.21568 16.4887 5.49354 16.4861 5.78196C16.4836 6.07038 16.3653 6.34628 16.1567 6.55023C15.9481 6.75418 15.6659 6.86987 15.371 6.87237C15.076 6.87488 14.7918 6.764 14.5796 6.56363L13.125 5.14131V15.686C13.125 15.9778 13.0065 16.2576 12.7955 16.4639C12.5845 16.6701 12.2984 16.786 12 16.786C11.7016 16.786 11.4155 16.6701 11.2045 16.4639C10.9935 16.2576 10.875 15.9778 10.875 15.686V5.14131L9.42037 6.56363C9.2082 6.764 8.92402 6.87488 8.62905 6.87237C8.33408 6.86987 8.05191 6.75418 7.84333 6.55023C7.63475 6.34628 7.51643 6.07038 7.51387 5.78196C7.5113 5.49354 7.6247 5.21568 7.82963 5.00821L11.2046 1.70818ZM3 11.286C3 10.7025 3.23705 10.1429 3.65901 9.73033C4.08097 9.31774 4.65326 9.08596 5.25 9.08596H7.5C7.79837 9.08596 8.08452 9.20185 8.2955 9.40814C8.50647 9.61443 8.625 9.89423 8.625 10.186C8.625 10.4777 8.50647 10.7575 8.2955 10.9638C8.08452 11.1701 7.79837 11.286 7.5 11.286H5.25V21.1861H18.75V11.286H16.5C16.2016 11.286 15.9155 11.1701 15.7045 10.9638C15.4935 10.7575 15.375 10.4777 15.375 10.186C15.375 9.89423 15.4935 9.61443 15.7045 9.40814C15.9155 9.20185 16.2016 9.08596 16.5 9.08596H18.75C19.3467 9.08596 19.919 9.31774 20.341 9.73033C20.7629 10.1429 21 10.7025 21 11.286V21.1861C21 21.7696 20.7629 22.3292 20.341 22.7417C19.919 23.1543 19.3467 23.3861 18.75 23.3861H5.25C4.65326 23.3861 4.08097 23.1543 3.65901 22.7417C3.23705 22.3292 3 21.7696 3 21.1861V11.286Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:pathData="M9 14.3L5 14.3C4.68082 14.2887 4.36901 14.201 4.09064 14.0444C3.81228 13.8879 3.57546 13.6669 3.4 13.4C3.21079 13.1453 3.08491 12.8492 3.03274 12.5362C2.98058 12.2232 3.00363 11.9023 3.1 11.6L5.5 4.40002C5.8 3.50002 6 3.00002 7.4 3.00002C9.4 3.00002 11.6 3.70002 13.5 4.30002L15 4.70003L15 14.5C13.4053 16.1761 11.9971 18.0203 10.8 20C10.7 20.4 10.3 20.7 9.9 20.9L8.7 20.9C8.3 20.8 8 20.5 7.7 20.2L7.5 18.9L9 14.3ZM19.8 14L17 14L17 6.00002C17 5.46959 17.2107 4.96088 17.5858 4.58581C17.9609 4.21074 18.4696 4.00002 19 4.00002C19.5304 4.00002 20.0391 4.21074 20.4142 4.58581C20.7893 4.96088 21 5.46959 21 6.00002L21 12.8C21 13.5 20.5 14 19.8 14Z"
|
||||
android:strokeColor="@color/white"
|
||||
android:strokeWidth="1.5"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M8.99448 14.4L4.99448 14.4C4.6753 14.3886 4.36348 14.301 4.08512 14.1444C3.80675 13.9878 3.56993 13.7669 3.39448 13.5C3.20527 13.2453 3.07938 12.9492 3.02722 12.6362C2.97506 12.3232 2.99811 12.0023 3.09448 11.7L5.49448 4.5C5.79448 3.6 5.99448 3.1 7.39448 3.1C9.39448 3.1 11.5945 3.8 13.4945 4.4L14.9945 4.8L14.9945 14.6C13.3998 16.2761 11.9915 18.1202 10.7945 20.1C10.6945 20.5 10.2945 20.8 9.89448 21L8.69448 21C8.29448 20.9 7.99448 20.6 7.69448 20.3L7.49448 19L8.99448 14.4ZM19.7945 14.1L16.9945 14.1L16.9945 6.1C16.9945 5.56957 17.2052 5.06086 17.5803 4.68579C17.9553 4.31071 18.464 4.1 18.9945 4.1C19.5249 4.1 20.0336 4.31071 20.4087 4.68579C20.7838 5.06086 20.9945 5.56957 20.9945 6.1L20.9945 12.9C20.9945 13.6 20.4945 14.1 19.7945 14.1Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:pathData="M15 9.69998H19C19.3192 9.71134 19.631 9.79898 19.9094 9.95556C20.1877 10.1121 20.4245 10.3331 20.6 10.6C20.7892 10.8547 20.9151 11.1508 20.9673 11.4638C21.0194 11.7768 20.9964 12.0977 20.9 12.4L18.5 19.6C18.2 20.5 18 21 16.6 21C14.6 21 12.4 20.3 10.5 19.7L9 19.3V9.49998C10.5947 7.82386 12.0029 5.97974 13.2 3.99998C13.3 3.59998 13.7 3.29998 14.1 3.09998H15.3C15.7 3.19998 16 3.49998 16.3 3.79998L16.5 5.09998L15 9.69998ZM4.2 9.99998H7V18C7 18.5304 6.78929 19.0391 6.41421 19.4142C6.03914 19.7893 5.53043 20 5 20C4.46957 20 3.96086 19.7893 3.58579 19.4142C3.21071 19.0391 3 18.5304 3 18V11.2C3 10.5 3.5 9.99998 4.2 9.99998Z"
|
||||
android:strokeColor="@color/white"
|
||||
android:strokeWidth="1.5"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15 9.69998H19C19.3192 9.71134 19.631 9.79898 19.9094 9.95556C20.1877 10.1121 20.4245 10.3331 20.6 10.6C20.7892 10.8547 20.9151 11.1508 20.9673 11.4638C21.0194 11.7768 20.9964 12.0977 20.9 12.4L18.5 19.6C18.2 20.5 18 21 16.6 21C14.6 21 12.4 20.3 10.5 19.7L9 19.3V9.49998C10.5947 7.82386 12.0029 5.97974 13.2 3.99998C13.3 3.59998 13.7 3.29998 14.1 3.09998H15.3C15.7 3.19998 16 3.49998 16.3 3.79998L16.5 5.09998L15 9.69998ZM4.2 9.99998H7V18C7 18.5304 6.78929 19.0391 6.41421 19.4142C6.03914 19.7893 5.53043 20 5 20C4.46957 20 3.96086 19.7893 3.58579 19.4142C3.21071 19.0391 3 18.5304 3 18V11.2C3 10.5 3.5 9.99998 4.2 9.99998Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M260,540L700,540L700,509.23L260,509.23L260,540ZM260,415.38L700,415.38L700,384.62L260,384.62L260,415.38ZM260,290.77L700,290.77L700,260L260,260L260,290.77ZM840,803.08L716.92,680L175.38,680Q152.33,680 136.16,663.84Q120,647.67 120,624.62L120,175.38Q120,152.33 136.16,136.16Q152.33,120 175.38,120L784.62,120Q807.67,120 823.84,136.16Q840,152.33 840,175.38L840,803.08ZM175.38,649.23L730.62,649.23L809.23,730.77L809.23,175.38Q809.23,166.15 801.54,158.46Q793.85,150.77 784.62,150.77L175.38,150.77Q166.15,150.77 158.46,158.46Q150.77,166.15 150.77,175.38L150.77,624.62Q150.77,633.85 158.46,641.54Q166.15,649.23 175.38,649.23ZM150.77,649.23Q150.77,649.23 150.77,641.54Q150.77,633.85 150.77,624.62L150.77,175.38Q150.77,166.15 150.77,158.46Q150.77,150.77 150.77,150.77L150.77,150.77Q150.77,150.77 150.77,158.46Q150.77,166.15 150.77,175.38L150.77,649.23Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M592.69,651.08L623.46,651.08L623.46,589.15L655.23,589.15Q671.76,589.15 683.57,577.41Q695.38,565.66 695.38,549.23L695.38,411Q695.38,394.47 683.57,382.66Q671.76,370.85 655.23,370.85L563.08,370.85Q546.77,370.85 533.38,382.66Q520,394.47 520,411L520,549.23Q520,565.66 533.38,577.41Q546.77,589.15 563.08,589.15L592.69,589.15L592.69,651.08ZM264.62,589.15L295.38,589.15L295.38,504.77L409.23,504.77L409.23,589.15L440,589.15L440,370.85L409.23,370.85L409.23,474L295.38,474L295.38,370.85L264.62,370.85L264.62,589.15ZM563.08,558.38Q558.46,558.38 554.62,554.54Q550.77,550.69 550.77,546.08L550.77,413.92Q550.77,409.31 554.62,405.46Q558.46,401.62 563.08,401.62L652.31,401.62Q656.92,401.62 660.77,405.46Q664.62,409.31 664.62,413.92L664.62,546.08Q664.62,550.69 660.77,554.54Q656.92,558.38 652.31,558.38L563.08,558.38ZM175.38,760Q152.33,760 136.16,743.84Q120,727.67 120,704.62L120,255.38Q120,232.33 136.16,216.16Q152.33,200 175.38,200L784.62,200Q807.67,200 823.84,216.16Q840,232.33 840,255.38L840,704.62Q840,727.67 823.84,743.84Q807.67,760 784.62,760L175.38,760ZM175.38,729.23L784.62,729.23Q793.85,729.23 801.54,721.54Q809.23,713.85 809.23,704.62L809.23,255.38Q809.23,246.15 801.54,238.46Q793.85,230.77 784.62,230.77L175.38,230.77Q166.15,230.77 158.46,238.46Q150.77,246.15 150.77,255.38L150.77,704.62Q150.77,713.85 158.46,721.54Q166.15,729.23 175.38,729.23ZM150.77,729.23Q150.77,729.23 150.77,721.54Q150.77,713.85 150.77,704.62L150.77,255.38Q150.77,246.15 150.77,238.46Q150.77,230.77 150.77,230.77L150.77,230.77Q150.77,230.77 150.77,238.46Q150.77,246.15 150.77,255.38L150.77,704.62Q150.77,713.85 150.77,721.54Q150.77,729.23 150.77,729.23Z"/>
|
||||
</vector>
|
||||
@@ -5,5 +5,5 @@
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M405.85,617L619.69,478.77L405.85,341.31L405.85,617ZM175.38,760Q152.33,760 136.16,743.84Q120,727.67 120,704.62L120,255.38Q120,232.33 136.16,216.16Q152.33,200 175.38,200L784.62,200Q807.67,200 823.84,216.16Q840,232.33 840,255.38L840,704.62Q840,727.67 823.84,743.84Q807.67,760 784.62,760L175.38,760ZM175.38,729.23L784.62,729.23Q793.85,729.23 801.54,721.54Q809.23,713.85 809.23,704.62L809.23,255.38Q809.23,246.15 801.54,238.46Q793.85,230.77 784.62,230.77L175.38,230.77Q166.15,230.77 158.46,238.46Q150.77,246.15 150.77,255.38L150.77,704.62Q150.77,713.85 158.46,721.54Q166.15,729.23 175.38,729.23ZM150.77,729.23Q150.77,729.23 150.77,721.54Q150.77,713.85 150.77,704.62L150.77,255.38Q150.77,246.15 150.77,238.46Q150.77,230.77 150.77,230.77L150.77,230.77Q150.77,230.77 150.77,238.46Q150.77,246.15 150.77,255.38L150.77,704.62Q150.77,713.85 150.77,721.54Q150.77,729.23 150.77,729.23L150.77,729.23Z"/>
|
||||
android:pathData="M405.85,617L619.69,478.77L405.85,341.31L405.85,617ZM175.38,760Q152.33,760 136.16,743.84Q120,727.67 120,704.62L120,255.38Q120,232.33 136.16,216.16Q152.33,200 175.38,200L784.62,200Q807.67,200 823.84,216.16Q840,232.33 840,255.38L840,704.62Q840,727.67 823.84,743.84Q807.67,760 784.62,760L175.38,760ZM175.38,729.23L784.62,729.23Q793.85,729.23 801.54,721.54Q809.23,713.85 809.23,704.62L809.23,255.38Q809.23,246.15 801.54,238.46Q793.85,230.77 784.62,230.77L175.38,230.77Q166.15,230.77 158.46,238.46Q150.77,246.15 150.77,255.38L150.77,704.62Q150.77,713.85 158.46,721.54Q166.15,729.23 175.38,729.23ZM150.77,729.23Q150.77,729.23 150.77,721.54Q150.77,713.85 150.77,704.62L150.77,255.38Q150.77,246.15 150.77,238.46Q150.77,230.77 150.77,230.77L150.77,230.77Q150.77,230.77 150.77,238.46Q150.77,246.15 150.77,255.38L150.77,704.62Q150.77,713.85 150.77,721.54Q150.77,729.23 150.77,729.23Z"/>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M405.85,617L619.69,478.77L405.85,341.31L405.85,617ZM175.38,760Q152.15,760 136.08,743.92Q120,727.85 120,704.62L120,255.38Q120,232.15 136.08,216.08Q152.15,200 175.38,200L784.62,200Q807.85,200 823.92,216.08Q840,232.15 840,255.38L840,704.62Q840,727.85 823.92,743.92Q807.85,760 784.62,760L175.38,760Z"/>
|
||||
</vector>
|
||||
@@ -1,9 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M13.15,9.6H33.3V31.45L20.8,44L20.2,43.55Q19.9,43.3 19.75,42.95Q19.6,42.6 19.6,42.2V42L21.7,31.45H6.85Q5.75,31.45 4.9,30.6Q4.05,29.75 4.05,28.7V26.15Q4.05,25.9 4.05,25.575Q4.05,25.25 4.15,24.95L9.75,11.85Q10.1,10.9 11.1,10.25Q12.1,9.6 13.15,9.6ZM31.75,11.15H12.85Q12.4,11.15 11.975,11.375Q11.55,11.6 11.3,12.15L5.6,25.6V28.7Q5.6,29.2 5.95,29.55Q6.3,29.9 6.85,29.9H23.55L21.25,41.45L31.75,30.75ZM31.75,30.75V29.9Q31.75,29.9 31.75,29.55Q31.75,29.2 31.75,28.7V25.6V12.15Q31.75,11.6 31.75,11.375Q31.75,11.15 31.75,11.15ZM33.3,31.45V29.9H40.45V11.15H33.3V9.6H42V31.45Z"/>
|
||||
android:pathData="M262.65,192.31L666,192.31L666,628.92L415.69,880L403.6,871.19Q398.38,866.31 395.38,859.23Q392.38,852.15 392.38,843.77L392.38,839.92L433.54,628.92L136.85,628.92Q115.46,628.92 98.46,611.92Q81.46,594.92 81.46,573.54L81.46,523.18Q81.46,517.62 81.12,511.27Q80.77,504.92 83,499.46L195.15,237.15Q202.49,218.21 222.44,205.26Q242.39,192.31 262.65,192.31ZM635.23,223.08L256.69,223.08Q248.23,223.08 239.38,227.69Q230.54,232.31 225.92,243.08L112.23,512.08L112.23,573.54Q112.23,583.54 119.15,590.85Q126.08,598.15 136.85,598.15L470.62,598.15L424.54,829.46L635.23,615.46L635.23,223.08ZM635.23,615.46L635.23,615.46L635.23,598.15L635.23,598.15Q635.23,598.15 635.23,590.85Q635.23,583.54 635.23,573.54L635.23,512.08L635.23,243.08Q635.23,232.31 635.23,227.69Q635.23,223.08 635.23,223.08L635.23,223.08L635.23,615.46ZM666,628.92L666,598.15L809,598.15L809,223.08L666,223.08L666,192.31L839.77,192.31L839.77,628.92L666,628.92Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M261.31,192.31L684.46,192.31L684.46,628.92L434.15,880L425,873.23Q417.08,866.77 413.96,859.46Q410.85,852.15 410.85,843.77L410.85,839.92L452,628.92L135.38,628.92Q113.92,628.92 96.96,611.96Q80,595 80,573.54L80,523.08Q80,515.15 81.73,505.65Q83.46,496.15 86.15,489.77L193.69,237.15Q201.92,218.46 221.27,205.38Q240.62,192.31 261.31,192.31ZM715.23,628.92L715.23,192.31L840,192.31L840,628.92L715.23,628.92Z"/>
|
||||
</vector>
|
||||
@@ -1,9 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M34.85,40H14.7V18.15L27.15,5.6L27.8,6.05Q28.05,6.3 28.2,6.65Q28.35,7 28.35,7.4V7.6L26.25,18.15H41.15Q42.2,18.15 43.05,19Q43.9,19.85 43.9,20.95V23.45Q43.9,23.75 43.9,24.05Q43.9,24.35 43.8,24.65L38.2,37.75Q37.8,38.7 36.8,39.35Q35.8,40 34.85,40ZM16.2,38.45H35.15Q35.55,38.45 36,38.225Q36.45,38 36.7,37.45L42.35,24V20.95Q42.35,20.45 42,20.075Q41.65,19.7 41.15,19.7H24.45L26.7,8.15L16.2,18.85ZM16.2,18.85V19.7Q16.2,19.7 16.2,20.075Q16.2,20.45 16.2,20.95V24V37.45Q16.2,38 16.2,38.225Q16.2,38.45 16.2,38.45ZM14.7,18.15V19.7H7.55V38.45H14.7V40H6V18.15Z"/>
|
||||
android:pathData="M696.77,800L293.54,800L293.54,363.38L543.08,112.31L555.84,121.12Q561.15,126 564.15,133.08Q567.15,140.15 567.15,147.77L567.15,152.38L525.23,363.38L822.69,363.38Q844.08,363.38 861.08,380.38Q878.08,397.38 878.08,418.77L878.08,469.13Q878.08,474.69 878.04,481.04Q878,487.38 875.77,492.85L764.38,755.15Q756,774.22 736.19,787.11Q716.38,800 696.77,800ZM324.31,769.23L702.85,769.23Q710.54,769.23 719.77,764.62Q729,760 733.62,749.23L847.31,480.23L847.31,418.77Q847.31,408.77 840,401.46Q832.69,394.15 822.69,394.15L488.92,394.15L534.23,162.85L324.31,376.85L324.31,769.23ZM324.31,376.85L324.31,376.85L324.31,394.15L324.31,394.15Q324.31,394.15 324.31,401.46Q324.31,408.77 324.31,418.77L324.31,480.23L324.31,749.23Q324.31,760 324.31,764.62Q324.31,769.23 324.31,769.23L324.31,769.23L324.31,376.85ZM293.54,363.38L293.54,394.15L150.54,394.15L150.54,769.23L293.54,769.23L293.54,800L119.77,800L119.77,363.38L293.54,363.38Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M698.69,800L275.54,800L275.54,363.38L525.85,112.31L535.77,119.08Q542.92,125.54 546.04,132.85Q549.15,140.15 549.15,147.77L549.15,152.38L508,363.38L824.62,363.38Q846.08,363.38 863.04,380.35Q880,397.31 880,418.77L880,469.23Q880,477.15 878.27,486.65Q876.54,496.15 873.85,502.54L766.31,755.15Q758.08,773.85 738.73,786.92Q719.38,800 698.69,800ZM244.77,363.38L244.77,800L120,800L120,363.38L244.77,363.38Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:drawable="@drawable/desktop_thumb_down_filled" />
|
||||
<item android:drawable="@drawable/desktop_thumb_down" />
|
||||
</selector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:drawable="@drawable/desktop_thumb_up_filled" />
|
||||
<item android:drawable="@drawable/desktop_thumb_up" />
|
||||
</selector>
|
||||
@@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.motion.widget.MotionLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
tools:context=".activities.MainActivity"
|
||||
android:background="@color/black"
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="4dp">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/view_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/short_view_loading_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#77000000"
|
||||
android:elevation="4dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/short_view_loader"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_gravity="center_vertical|center_horizontal"
|
||||
android:alpha="0.7"
|
||||
android:contentDescription="@string/loading"
|
||||
app:srcCompat="@drawable/ic_loader_animated" />
|
||||
</FrameLayout>
|
||||
|
||||
<!-- zero state -->
|
||||
<LinearLayout
|
||||
android:id="@+id/zero_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_help" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:fontFamily="@font/inter_bold"
|
||||
android:gravity="center"
|
||||
android:text="@string/no_results"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="22dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_centered"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:gravity="center"
|
||||
android:text="@string/no_results_shorts"
|
||||
android:textColor="@color/gray_ac"
|
||||
android:textSize="13dp" />
|
||||
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/sources_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:buttonIcon="@drawable/ic_creators"
|
||||
app:buttonSubText="Go to the sources tab"
|
||||
app:buttonText="Sources" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/shorts_quality_overview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:elevation="100dp"
|
||||
android:visibility="gone" />
|
||||
</FrameLayout>
|
||||
@@ -3,24 +3,21 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintDimensionRatio="H,16:13">
|
||||
<FrameLayout
|
||||
android:id="@+id/player_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintDimensionRatio="H,16:9"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/video_meta">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_video_thumbnail"
|
||||
@@ -113,10 +110,10 @@
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/video_meta"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="-6dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/player_container">
|
||||
@@ -226,6 +223,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingEnd="6dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/creator_thumbnail"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/standard_bottom_sheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Drag handle for accessibility -->
|
||||
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
|
||||
android:id="@+id/drag_handle"
|
||||
style="@style/Widget.Material3.BottomSheet.DragHandle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="0dp" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/content_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/videodetail_container_main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="SpeakableTextPresentCheck">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_top"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!--Title Segment-->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<!--Title + Meta-->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="14dp"
|
||||
android:layout_marginEnd="14dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/videodetail_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:fontFamily="@font/inter_medium"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="17sp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/layout_title_right"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Some Text" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/videodetail_meta"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="#ACACAC"
|
||||
android:textSize="11sp"
|
||||
app:layout_constraintLeft_toLeftOf="@id/videodetail_title"
|
||||
app:layout_constraintRight_toLeftOf="@id/layout_title_right"
|
||||
app:layout_constraintTop_toBottomOf="@id/videodetail_title"
|
||||
tools:text="51K views - 3 years ago" />
|
||||
|
||||
<!--Source Button-->
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_title_right"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
android:id="@+id/videodetail_platform"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_rating"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_like_icon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:contentDescription="@string/cd_image_like_icon"
|
||||
app:srcCompat="@drawable/ic_thumb_up"
|
||||
app:tint="#ACACAC" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_likes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textColor="#ACACAC"
|
||||
android:textSize="11sp"
|
||||
tools:text="500K" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_dislike_icon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:contentDescription="@string/cd_image_dislike_icon"
|
||||
app:srcCompat="@drawable/ic_thumb_down"
|
||||
app:tint="#ACACAC" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_dislikes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textColor="#ACACAC"
|
||||
android:textSize="11sp"
|
||||
tools:text="500K" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!--Channel/Subscribe Segment-->
|
||||
<LinearLayout
|
||||
android:id="@+id/videodetail_channel_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="14dp"
|
||||
android:layout_marginTop="17dp"
|
||||
android:layout_marginRight="14dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
android:id="@+id/creator_thumbnail"
|
||||
android:layout_width="35dp"
|
||||
android:layout_height="35dp"
|
||||
android:contentDescription="@string/cd_creator_thumbnail" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/videodetail_channel_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="11sp"
|
||||
tools:text="Channel Name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/videodetail_channel_meta"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="#ACACAC"
|
||||
android:textSize="11sp"
|
||||
tools:text="" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!--Description-->
|
||||
<LinearLayout
|
||||
android:id="@+id/videodetail_description_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="14dp"
|
||||
android:layout_marginRight="14dp"
|
||||
android:animateLayoutChanges="true"
|
||||
android:background="@drawable/background_videodetail_description"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingTop="3dp"
|
||||
android:paddingRight="12dp"
|
||||
android:paddingBottom="5dp">
|
||||
|
||||
<com.futo.platformplayer.views.behavior.NonScrollingTextView
|
||||
android:id="@+id/videodetail_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:background="@color/transparent"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:isScrollContainer="false"
|
||||
android:maxWidth="500dp"
|
||||
android:maxLines="3"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="11sp"
|
||||
tools:text="@string/lorem_ipsum" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/videodetail_description_view_more"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@color/transparent"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:text="@string/click_to_read_more"
|
||||
android:textAlignment="center"
|
||||
android:textColor="#838181"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.futo.platformplayer.views.MonetizationView
|
||||
android:id="@+id/monetization"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_change_bottom_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="14dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginRight="14dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:background="@drawable/background_videodetail_description"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_polycentric"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="90dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:ellipsize="marquee"
|
||||
android:lines="1"
|
||||
android:maxWidth="100dp"
|
||||
android:padding="10dp"
|
||||
android:text="@string/polycentric"
|
||||
android:textColor="#fff"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_platform"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:ellipsize="marquee"
|
||||
android:lines="1"
|
||||
android:maxWidth="100dp"
|
||||
android:padding="10dp"
|
||||
android:text="@string/platform"
|
||||
android:textColor="#fff"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.futo.platformplayer.views.comments.AddCommentView
|
||||
android:id="@+id/add_comment_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="28dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="28dp"
|
||||
android:layout_marginBottom="12dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.futo.platformplayer.views.segments.CommentsList
|
||||
android:id="@+id/comments_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="400dp"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_top" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.futo.platformplayer.views.overlays.DescriptionOverlay
|
||||
android:id="@+id/videodetail_container_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.futo.platformplayer.views.overlays.WebviewOverlay
|
||||
android:id="@+id/videodetail_container_webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||
android:id="@+id/videodetail_container_replies"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.futo.platformplayer.views.overlays.SupportOverlay
|
||||
android:id="@+id/videodetail_container_support"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
@@ -16,7 +16,7 @@
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
<ScrollView
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="15dp"
|
||||
@@ -37,6 +37,6 @@
|
||||
android:textSize="13sp"
|
||||
android:textIsSelectable="true"
|
||||
tools:text="@string/lorem_ipsum" />
|
||||
</ScrollView>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/bottom_menu_border">
|
||||
|
||||
<com.futo.platformplayer.views.video.FutoShortPlayer
|
||||
android:id="@+id/short_player"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/transparent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<include layout="@layout/view_short_overlay" />
|
||||
|
||||
<!-- make the overlay clickable to intercept touches -->
|
||||
<FrameLayout
|
||||
android:id="@+id/short_view_loading_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#77000000"
|
||||
android:clickable="true"
|
||||
android:elevation="4dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/short_view_loader"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_gravity="center_vertical|center_horizontal"
|
||||
android:alpha="0.7"
|
||||
android:contentDescription="@string/loading"
|
||||
app:srcCompat="@drawable/ic_loader_animated" />
|
||||
</FrameLayout>
|
||||
</merge>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user