Grayjay logo, WatchLater button, WatchLater download, Download notification dismiss fix, Polycentric open platform, Minor utility additions, Dev method documentation url support

This commit is contained in:
Kelvin 2024-04-12 23:39:33 +02:00
parent 40b86cb5de
commit b370af9d91
38 changed files with 546 additions and 101 deletions

View File

@ -402,6 +402,11 @@
<div class="code"> <div class="code">
{{req.code}} {{req.code}}
</div> </div>
<div class="documentation" v-if="req.docUrl" style="position: absolute; right: 15px; top: 15px;">
<a :href="req.docUrl" target="_blank">
Documentation
</a>
</div>
<div> <div>
<div class="parameter" v-for="parameter in req.parameters"> <div class="parameter" v-for="parameter in req.parameters">
<div class="name"> <div class="name">
@ -538,6 +543,7 @@
<!--<script src="./dependencies/vue.js"></script>--> <!--<script src="./dependencies/vue.js"></script>-->
<!--<script src="./dependencies/vuetify.js"></script>--> <!--<script src="./dependencies/vuetify.js"></script>-->
<script src="./source_docs.js"></script> <script src="./source_docs.js"></script>
<script src="./source_doc_urls.js"></script>
<script src="./source.js"></script> <script src="./source.js"></script>
<script src="./dev_bridge.js"></script> <script src="./dev_bridge.js"></script>
<script> <script>
@ -574,6 +580,9 @@
Testing: { Testing: {
requests: sourceDocs.map(x=>{ requests: sourceDocs.map(x=>{
x.parameters.forEach(y=>y.value = null); x.parameters.forEach(y=>y.value = null);
if(sourceDocUrls[x.title])
x.docUrl = sourceDocUrls[x.title];
return x; return x;
}), }),
lastResult: "", lastResult: "",

View File

@ -510,10 +510,15 @@ class UISlideOverlays {
} }
} }
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) { fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
showUnknownVideoDownload(container.context.getString(R.string.video), container) { px, bitrate -> showUnknownVideoDownload(container.context.getString(R.string.playlist), container) { px, bitrate ->
StateDownloads.instance.download(playlist, px, bitrate); StateDownloads.instance.download(playlist, px, bitrate);
}; };
} }
fun showDownloadWatchlaterOverlay(container: ViewGroup) {
showUnknownVideoDownload(container.context.getString(R.string.watch_later), container, { px, bitrate ->
StateDownloads.instance.downloadWatchLater(px, bitrate);
})
}
private fun showUnknownVideoDownload(toDownload: String, container: ViewGroup, cb: (Long?, Long?)->Unit) { private fun showUnknownVideoDownload(toDownload: String, container: ViewGroup, cb: (Long?, Long?)->Unit) {
val items = arrayListOf<View>(); val items = arrayListOf<View>();
var menu: SlideUpMenuOverlay? = null; var menu: SlideUpMenuOverlay? = null;

View File

@ -56,8 +56,10 @@ import com.futo.platformplayer.states.StatePlugins
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.Dictionary
import kotlin.reflect.full.findAnnotations import kotlin.reflect.full.findAnnotations
import kotlin.reflect.jvm.kotlinFunction import kotlin.reflect.jvm.kotlinFunction
import kotlin.streams.asSequence
open class JSClient : IPlatformClient { open class JSClient : IPlatformClient {
val config: SourcePluginConfig; val config: SourcePluginConfig;
@ -662,10 +664,43 @@ open class JSClient : IPlatformClient {
companion object { companion object {
val TAG = "JSClient"; val TAG = "JSClient";
private val _lock = Object();
private var _docs: Map<String, String>? = null;
fun getMethodDocs(names: List<String>): Map<String, String>? {
synchronized(_lock) {
if(_docs == null) {
val client = ManagedHttpClient();
val docs = names
.map { stringWithoutBrackets(it) }
.distinct()
.parallelStream()
.map {
val url = "https://github.com/futo-org/grayjay-android/blob/master/docs/source/${it}.md";
val resp = client.head(url);
if(resp.isOk)
return@map Pair(it, url);
else
return@map null;
}.asSequence()
.filterNotNull()
.toMap();
_docs = docs;
}
return _docs;
}
}
fun getMethodDocUrls(): Map<String, String>? {
if(_docs != null)
return _docs;
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
return getMethodDocs(methods.map { it.name });
}
fun getJSDocs(): List<JSCallDocs> { fun getJSDocs(): List<JSCallDocs> {
val docs = mutableListOf<JSCallDocs>(); val docs = mutableListOf<JSCallDocs>();
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null } val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
for(method in methods.sortedBy { it.getAnnotation(JSDocs::class.java)?.order }) { for(method in methods.sortedBy { it.getAnnotation(JSDocs::class.java)?.order }) {
val doc = method.getAnnotation(JSDocs::class.java); val doc = method.getAnnotation(JSDocs::class.java);
val parameters = method.kotlinFunction!!.findAnnotations<JSDocsParameter>(); val parameters = method.kotlinFunction!!.findAnnotations<JSDocsParameter>();
@ -678,5 +713,12 @@ open class JSClient : IPlatformClient {
} }
return docs; return docs;
} }
private fun stringWithoutBrackets(name: String): String {
val index = name.indexOf('(');
if(index >= 0)
return name.substring(0, index);
return name;
}
} }
} }

View File

@ -14,6 +14,6 @@ annotation class JSOptional()
annotation class JSDocsParameter(val name: String, val description: String, val order: Int = 0) annotation class JSDocsParameter(val name: String, val description: String, val order: Int = 0)
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false); data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false, val docsUrl: String? = null);
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
data class JSParameterDocs(val name: String, val description: String); data class JSParameterDocs(val name: String, val description: String);

View File

@ -104,6 +104,17 @@ class DeveloperEndpoints(private val context: Context) {
@HttpGET("/source_docs.js", "application/javascript") @HttpGET("/source_docs.js", "application/javascript")
val devSourceDocsJS = "const sourceDocs = $devSourceDocsJson"; val devSourceDocsJS = "const sourceDocs = $devSourceDocsJson";
@HttpGET("/source_doc_urls.json", "application/json")
fun devSourceDocUrlsJson(httpContext: HttpContext) {;
val docs = JSClient.getMethodDocUrls();
httpContext.respondCode(200, Json.encodeToString(docs), "application/json");
}
@HttpGET("/source_doc_urls.js", "application/javascript")
fun devSourceDocUrlsJs(httpContext: HttpContext) {;
val docs = JSClient.getMethodDocUrls();
httpContext.respondCode(200, "const sourceDocUrls = " + Json.encodeToString(docs), "application/javascript");
}
//Dependencies //Dependencies
//@HttpGET("/dependencies/vue.js", "application/javascript") //@HttpGET("/dependencies/vue.js", "application/javascript")
//val depVue = StateAssets.readAsset(context, "devportal/dependencies/vue.js", true); //val depVue = StateAssets.readAsset(context, "devportal/dependencies/vue.js", true);

View File

@ -755,6 +755,7 @@ class VideoDownload {
companion object { companion object {
const val TAG = "VideoDownload"; const val TAG = "VideoDownload";
const val GROUP_PLAYLIST = "Playlist"; const val GROUP_PLAYLIST = "Playlist";
const val GROUP_WATCHLATER= "WatchLater";
fun videoContainerToExtension(container: String): String? { fun videoContainerToExtension(container: String): String? {
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl") if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")

View File

@ -4,8 +4,11 @@ import android.util.Base64
import com.caoccao.javet.annotations.V8Function import com.caoccao.javet.annotations.V8Function
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.google.common.hash.Hashing.md5
import java.security.MessageDigest
import java.util.UUID import java.util.UUID
class PackageUtilities : V8Package { class PackageUtilities : V8Package {
@Transient @Transient
private val _config: IV8PluginConfig; private val _config: IV8PluginConfig;
@ -22,6 +25,16 @@ class PackageUtilities : V8Package {
return Base64.encodeToString(arr, Base64.NO_WRAP); return Base64.encodeToString(arr, Base64.NO_WRAP);
} }
@V8Function
fun fromBase64(str: String): ByteArray {
return Base64.decode(str, Base64.DEFAULT)
}
@V8Function
fun md5(arr: ByteArray): ByteArray {
return MessageDigest.getInstance("MD5").digest(arr);
}
@V8Function @V8Function
fun randomUUID(): String { fun randomUUID(): String {
return UUID.randomUUID().toString(); return UUID.randomUUID().toString();

View File

@ -60,8 +60,10 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
val onChannelClicked = Event1<PlatformAuthorLink>(); val onChannelClicked = Event1<PlatformAuthorLink>();
val onAddToClicked = Event1<IPlatformContent>(); val onAddToClicked = Event1<IPlatformContent>();
val onAddToQueueClicked = Event1<IPlatformContent>(); val onAddToQueueClicked = Event1<IPlatformContent>();
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
val onLongPress = Event1<IPlatformContent>(); val onLongPress = Event1<IPlatformContent>();
private fun getContentPager(channel: IPlatformChannel): IPager<IPlatformContent> { private fun getContentPager(channel: IPlatformChannel): IPager<IPlatformContent> {
Logger.i(TAG, "getContentPager"); Logger.i(TAG, "getContentPager");
@ -157,6 +159,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
this.onChannelClicked.subscribe(this@ChannelContentsFragment.onChannelClicked::emit); this.onChannelClicked.subscribe(this@ChannelContentsFragment.onChannelClicked::emit);
this.onAddToClicked.subscribe(this@ChannelContentsFragment.onAddToClicked::emit); this.onAddToClicked.subscribe(this@ChannelContentsFragment.onAddToClicked::emit);
this.onAddToQueueClicked.subscribe(this@ChannelContentsFragment.onAddToQueueClicked::emit); this.onAddToQueueClicked.subscribe(this@ChannelContentsFragment.onAddToQueueClicked::emit);
this.onAddToWatchLaterClicked.subscribe(this@ChannelContentsFragment.onAddToWatchLaterClicked::emit);
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit); this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
} }

View File

@ -26,6 +26,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.post.IPlatformPost import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment
import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment
@ -206,6 +207,12 @@ class ChannelFragment : MainFragment() {
StatePlayer.instance.addToQueue(content); StatePlayer.instance.addToQueue(content);
} }
} }
adapter.onAddToWatchLaterClicked.subscribe { content ->
if(content is IPlatformVideo) {
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content));
UIDialogs.toast("Added to watch later\n[${content.name}]");
}
}
adapter.onUrlClicked.subscribe { url -> adapter.onUrlClicked.subscribe { url ->
fragment.navigate<BrowserFragment>(url); fragment.navigate<BrowserFragment>(url);
} }

View File

@ -2,6 +2,7 @@ package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.provider.Browser
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -118,6 +119,7 @@ class CommentsFragment : MainFragment() {
holder.onDelete.subscribe(::onDelete); holder.onDelete.subscribe(::onDelete);
holder.onRepliesClick.subscribe(::onRepliesClick); holder.onRepliesClick.subscribe(::onRepliesClick);
holder.onClick.subscribe(::onClick); holder.onClick.subscribe(::onClick);
holder.onAuthorClick.subscribe(::onAuthorClick);
return@InsertedViewAdapterWithLoader holder; return@InsertedViewAdapterWithLoader holder;
} }
); );
@ -211,6 +213,17 @@ class CommentsFragment : MainFragment() {
setRepliesOverlayVisible(true, true) setRepliesOverlayVisible(true, true)
} }
} }
private fun onAuthorClick(c: IPlatformComment) {
if (c !is PolycentricPlatformComment) {
return@onAuthorClick;
}
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
_fragment.navigate<BrowserFragment>(navUrl);
}
}
private fun onRepliesClick(c: IPlatformComment) { private fun onRepliesClick(c: IPlatformComment) {
val replyCount = c.replyCount ?: 0; val replyCount = c.replyCount ?: 0;

View File

@ -12,10 +12,12 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.post.IPlatformPost import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.structures.* import com.futo.platformplayer.api.media.structures.*
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateMeta import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.video.PlayerManager import com.futo.platformplayer.video.PlayerManager
import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
@ -81,6 +83,12 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
StatePlayer.instance.addToQueue(it); StatePlayer.instance.addToQueue(it);
} }
}; };
adapter.onAddToWatchLaterClicked.subscribe(this) {
if(it is IPlatformVideo) {
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it));
UIDialogs.toast("Added to watch later\n[${it.name}]");
}
};
adapter.onLongPress.subscribe(this) { adapter.onLongPress.subscribe(this) {
if (it is IPlatformVideo) { if (it is IPlatformVideo) {
showVideoOptionsOverlay(it) showVideoOptionsOverlay(it)
@ -135,6 +143,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
adapter.onChannelClicked.remove(this); adapter.onChannelClicked.remove(this);
adapter.onAddToClicked.remove(this); adapter.onAddToClicked.remove(this);
adapter.onAddToQueueClicked.remove(this); adapter.onAddToQueueClicked.remove(this);
adapter.onAddToWatchLaterClicked.remove(this);
adapter.onLongPress.remove(this); adapter.onLongPress.remove(this);
} }

View File

@ -12,8 +12,10 @@ import com.futo.platformplayer.*
import com.futo.platformplayer.downloads.VideoDownload import com.futo.platformplayer.downloads.VideoDownload
import com.futo.platformplayer.downloads.VideoLocal import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.views.AnyInsertedAdapterView import com.futo.platformplayer.views.AnyInsertedAdapterView
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
import com.futo.platformplayer.views.others.ProgressBar import com.futo.platformplayer.views.others.ProgressBar
@ -143,6 +145,7 @@ class DownloadsFragment : MainFragment() {
val activeDownloads = StateDownloads.instance.getDownloading(); val activeDownloads = StateDownloads.instance.getDownloading();
val playlists = StateDownloads.instance.getCachedPlaylists(); val playlists = StateDownloads.instance.getCachedPlaylists();
val watchLaterDownload = StateDownloads.instance.getWatchLaterDescriptor();
val downloaded = StateDownloads.instance.getDownloadedVideos() val downloaded = StateDownloads.instance.getDownloadedVideos()
.filter { it.groupType != VideoDownload.GROUP_PLAYLIST || it.groupID == null || !StateDownloads.instance.hasCachedPlaylist(it.groupID!!) }; .filter { it.groupType != VideoDownload.GROUP_PLAYLIST || it.groupID == null || !StateDownloads.instance.hasCachedPlaylist(it.groupID!!) };
@ -157,16 +160,28 @@ class DownloadsFragment : MainFragment() {
_listActiveDownloads.addView(view); _listActiveDownloads.addView(view);
} }
if(playlists.isEmpty()) if(playlists.isEmpty() && watchLaterDownload == null)
_listPlaylistsContainer.visibility = GONE; _listPlaylistsContainer.visibility = GONE;
else { else {
_listPlaylistsContainer.visibility = VISIBLE; _listPlaylistsContainer.visibility = VISIBLE;
_listPlaylistsMeta.text = "(${playlists.size} ${context.getString(R.string.playlists).lowercase()}, ${playlists.sumOf { it.playlist.videos.size }} ${context.getString(R.string.videos).lowercase()})";
val watchLater = if(watchLaterDownload != null) StatePlaylists.instance.getWatchLater() else listOf();
_listPlaylistsMeta.text = "(${playlists.size + (if(watchLaterDownload != null) 1 else 0)} ${context.getString(R.string.playlists).lowercase()}, ${playlists.sumOf { it.playlist.videos.size } + watchLater.size} ${context.getString(R.string.videos).lowercase()})";
_listPlaylists.removeAllViews(); _listPlaylists.removeAllViews();
for(view in playlists.map { PlaylistDownloadItem(context, it) }) { if(watchLaterDownload != null) {
val pdView = PlaylistDownloadItem(context, "Watch Later", watchLater.firstOrNull()?.thumbnails?.getHQThumbnail(), "WATCHLATER");
pdView.setOnClickListener {
_frag.navigate<WatchLaterFragment>();
}
_listPlaylists.addView(pdView);
}
for(view in playlists.map { PlaylistDownloadItem(context, it.playlist.name, it.playlist.videos.firstOrNull()?.thumbnails?.getHQThumbnail(), it.playlist) }) {
view.setOnClickListener { view.setOnClickListener {
_frag.navigate<PlaylistFragment>(view.playlist.playlist); if(view.obj is Playlist) {
_frag.navigate<PlaylistFragment>(view.obj);
}
}; };
_listPlaylists.addView(view); _listPlaylists.addView(view);
} }

View File

@ -201,14 +201,18 @@ class PlaylistFragment : MainFragment() {
showConvertPlaylistButton(); showConvertPlaylistButton();
} }
updateDownloadState(); _playlist?.let {
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this::download);
}
} }
fun onResume() { fun onResume() {
StateDownloads.instance.onDownloadsChanged.subscribe(this) { StateDownloads.instance.onDownloadsChanged.subscribe(this) {
_fragment.lifecycleScope.launch(Dispatchers.Main) { _fragment.lifecycleScope.launch(Dispatchers.Main) {
try { try {
updateDownloadState(); _playlist?.let {
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this@PlaylistView::download);
}
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to update download state onDownloadedChanged.") Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
} }
@ -217,7 +221,9 @@ class PlaylistFragment : MainFragment() {
StateDownloads.instance.onDownloadedChanged.subscribe(this) { StateDownloads.instance.onDownloadedChanged.subscribe(this) {
_fragment.lifecycleScope.launch(Dispatchers.Main) { _fragment.lifecycleScope.launch(Dispatchers.Main) {
try { try {
updateDownloadState(); _playlist?.let {
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this@PlaylistView::download);
}
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to update download state onDownloadedChanged.") Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
} }
@ -225,6 +231,12 @@ class PlaylistFragment : MainFragment() {
}; };
} }
private fun download() {
_playlist?.let {
UISlideOverlays.showDownloadPlaylistOverlay(it, overlayContainer);
}
}
fun onPause() { fun onPause() {
StateDownloads.instance.onDownloadsChanged.remove(this); StateDownloads.instance.onDownloadsChanged.remove(this);
StateDownloads.instance.onDownloadedChanged.remove(this); StateDownloads.instance.onDownloadedChanged.remove(this);
@ -268,43 +280,6 @@ class PlaylistFragment : MainFragment() {
} }
} }
private fun updateDownloadState() {
val playlist = _playlist ?: return;
val isDownloading = StateDownloads.instance.getDownloading().any { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == playlist.id };
val isDownloaded = StateDownloads.instance.isPlaylistCached(playlist.id);
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics);
if(isDownloaded && !isDownloading)
_buttonDownload.setBackgroundResource(R.drawable.background_button_round_green);
else
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
if(isDownloading) {
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
_buttonDownload.setOnClickListener {
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
});
}
}
else if(isDownloaded) {
_buttonDownload.setImageResource(R.drawable.ic_download_off);
_buttonDownload.setOnClickListener {
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
});
}
}
else {
_buttonDownload.setImageResource(R.drawable.ic_download);
_buttonDownload.setOnClickListener {
UISlideOverlays.showDownloadPlaylistOverlay(playlist, overlayContainer);
}
}
_buttonDownload.setPadding(dp10.toInt());
}
override fun canEdit(): Boolean { return _playlist != null; } override fun canEdit(): Boolean { return _playlist != null; }

View File

@ -23,6 +23,7 @@ import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.WindowManager import android.view.WindowManager
import android.webkit.WebView
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
@ -124,6 +125,7 @@ import com.futo.platformplayer.views.overlays.LiveChatOverlay
import com.futo.platformplayer.views.overlays.QueueEditorOverlay import com.futo.platformplayer.views.overlays.QueueEditorOverlay
import com.futo.platformplayer.views.overlays.RepliesOverlay import com.futo.platformplayer.views.overlays.RepliesOverlay
import com.futo.platformplayer.views.overlays.SupportOverlay import com.futo.platformplayer.views.overlays.SupportOverlay
import com.futo.platformplayer.views.overlays.WebviewOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuButtonList import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuButtonList
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
@ -244,6 +246,7 @@ class VideoDetailView : ConstraintLayout {
private val _container_content_replies: RepliesOverlay; private val _container_content_replies: RepliesOverlay;
private val _container_content_description: DescriptionOverlay; private val _container_content_description: DescriptionOverlay;
private val _container_content_liveChat: LiveChatOverlay; private val _container_content_liveChat: LiveChatOverlay;
private val _container_content_browser: WebviewOverlay;
private val _container_content_support: SupportOverlay; private val _container_content_support: SupportOverlay;
private var _container_content_current: View; private var _container_content_current: View;
@ -349,7 +352,8 @@ class VideoDetailView : ConstraintLayout {
_container_content_replies = findViewById(R.id.videodetail_container_replies); _container_content_replies = findViewById(R.id.videodetail_container_replies);
_container_content_description = findViewById(R.id.videodetail_container_description); _container_content_description = findViewById(R.id.videodetail_container_description);
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat); _container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
_container_content_support = findViewById(R.id.videodetail_container_support) _container_content_support = findViewById(R.id.videodetail_container_support);
_container_content_browser = findViewById(R.id.videodetail_container_webview)
_textComments = findViewById(R.id.text_comments); _textComments = findViewById(R.id.text_comments);
_addCommentView = findViewById(R.id.add_comment_view); _addCommentView = findViewById(R.id.add_comment_view);
@ -624,6 +628,7 @@ class VideoDetailView : ConstraintLayout {
_container_content_queue.onClose.subscribe { switchContentView(_container_content_main); }; _container_content_queue.onClose.subscribe { switchContentView(_container_content_main); };
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); }; _container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); }; _container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
_container_content_browser.onClose.subscribe { switchContentView(_container_content_main); };
_description_viewMore.setOnClickListener { _description_viewMore.setOnClickListener {
switchContentView(_container_content_description); switchContentView(_container_content_description);
@ -644,6 +649,20 @@ class VideoDetailView : ConstraintLayout {
_container_content_current = _container_content_main; _container_content_current = _container_content_main;
_commentsList.onAuthorClick.subscribe { c ->
if (c !is PolycentricPlatformComment) {
return@subscribe;
}
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
//fragment.navigate<BrowserFragment>(navUrl);
//fragment.minimizeVideoDetail();
_container_content_browser.goto(navUrl);
switchContentView(_container_content_browser);
}
};
_commentsList.onRepliesClick.subscribe { c -> _commentsList.onRepliesClick.subscribe { c ->
val replyCount = c.replyCount ?: 0; val replyCount = c.replyCount ?: 0;
var metadata = ""; var metadata = "";

View File

@ -1,6 +1,7 @@
package com.futo.platformplayer.fragment.mainactivity.main package com.futo.platformplayer.fragment.mainactivity.main
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
@ -8,10 +9,17 @@ import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.view.setPadding
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.assume
import com.futo.platformplayer.downloads.VideoDownload
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.views.lists.VideoListEditorView import com.futo.platformplayer.views.lists.VideoListEditorView
abstract class VideoListEditorView : LinearLayout { abstract class VideoListEditorView : LinearLayout {
@ -85,6 +93,44 @@ abstract class VideoListEditorView : LinearLayout {
} }
protected fun updateDownloadState(groupType: String, playlistId: String, onDownload: ()->Unit) {
//val playlist = _playlist ?: return;
val isDownloading = StateDownloads.instance.getDownloading().any { it.groupType == groupType && it.groupID == playlistId };
val isDownloaded = StateDownloads.instance.isPlaylistCached(playlistId);
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics);
if(isDownloaded && !isDownloading)
_buttonDownload.setBackgroundResource(R.drawable.background_button_round_green);
else
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
if(isDownloading) {
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
_buttonDownload.setOnClickListener {
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
StateDownloads.instance.deleteCachedPlaylist(playlistId);
});
}
}
else if(isDownloaded) {
_buttonDownload.setImageResource(R.drawable.ic_download_off);
_buttonDownload.setOnClickListener {
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
StateDownloads.instance.deleteCachedPlaylist(playlistId);
});
}
}
else {
_buttonDownload.setImageResource(R.drawable.ic_download);
_buttonDownload.setOnClickListener {
onDownload();
//UISlideOverlays.showDownloadPlaylistOverlay(playlist, overlayContainer);
}
}
_buttonDownload.setPadding(dp10.toInt());
}
protected fun setName(name: String?) { protected fun setName(name: String?) {
_textName.text = name ?: ""; _textName.text = name ?: "";

View File

@ -5,10 +5,17 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.downloads.VideoDownload
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class WatchLaterFragment : MainFragment() { class WatchLaterFragment : MainFragment() {
override val isMainView : Boolean = true; override val isMainView : Boolean = true;
@ -28,6 +35,11 @@ class WatchLaterFragment : MainFragment() {
return view; return view;
} }
override fun onResume() {
super.onResume()
_view?.onResume();
}
override fun onDestroyMainView() { override fun onDestroyMainView() {
super.onDestroyMainView(); super.onDestroyMainView();
_view = null; _view = null;
@ -45,6 +57,34 @@ class WatchLaterFragment : MainFragment() {
fun onShown() { fun onShown() {
setName("Watch Later"); setName("Watch Later");
setVideos(StatePlaylists.instance.getWatchLater(), true); setVideos(StatePlaylists.instance.getWatchLater(), true);
setButtonDownloadVisible(true);
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
}
fun onResume(){
StateDownloads.instance.onDownloadsChanged.subscribe(this) {
_fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
}
}
};
StateDownloads.instance.onDownloadedChanged.subscribe(this) {
_fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
}
}
};
}
fun download(){
UISlideOverlays.showDownloadWatchlaterOverlay(overlayContainer);
} }
override fun onPlayAllClick() { override fun onPlayAllClick() {
@ -76,6 +116,7 @@ class WatchLaterFragment : MainFragment() {
} }
companion object { companion object {
val TAG = "WatchLaterFragment";
fun newInstance() = WatchLaterFragment().apply {} fun newInstance() = WatchLaterFragment().apply {}
} }
} }

View File

@ -270,7 +270,7 @@ class DownloadService : Service() {
fun closeDownloadSession() { fun closeDownloadSession() {
Logger.i(TAG, "closeDownloadSession"); Logger.i(TAG, "closeDownloadSession");
stopForeground(STOP_FOREGROUND_DETACH); stopForeground(STOP_FOREGROUND_REMOVE);
_notificationManager?.cancel(DOWNLOAD_NOTIF_ID); _notificationManager?.cancel(DOWNLOAD_NOTIF_ID);
stopService(); stopService();
_started = false; _started = false;

View File

@ -188,7 +188,7 @@ class ExportingService : Service() {
fun closeExportSession() { fun closeExportSession() {
Logger.i(TAG, "closeExportSession"); Logger.i(TAG, "closeExportSession");
stopForeground(STOP_FOREGROUND_DETACH); stopForeground(STOP_FOREGROUND_REMOVE);
_notificationManager?.cancel(EXPORT_NOTIF_ID); _notificationManager?.cancel(EXPORT_NOTIF_ID);
stopService(); stopService();
_started = false; _started = false;

View File

@ -97,6 +97,9 @@ class StateDownloads {
} }
} }
fun getWatchLaterDescriptor(): PlaylistDownloadDescriptor? {
return _downloadPlaylists.getItems().find { it.id == VideoDownload.GROUP_WATCHLATER };
}
fun getCachedPlaylists(): List<PlaylistDownloaded> { fun getCachedPlaylists(): List<PlaylistDownloaded> {
return _downloadPlaylists.getItems() return _downloadPlaylists.getItems()
.map { Pair(it, StatePlaylists.instance.getPlaylist(it.id)) } .map { Pair(it, StatePlaylists.instance.getPlaylist(it.id)) }
@ -124,10 +127,18 @@ class StateDownloads {
val pdl = getPlaylistDownload(id); val pdl = getPlaylistDownload(id);
if(pdl != null) if(pdl != null)
_downloadPlaylists.delete(pdl); _downloadPlaylists.delete(pdl);
getDownloading().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id } if(id == VideoDownload.GROUP_WATCHLATER) {
.forEach { removeDownload(it) }; getDownloading().filter { it.groupType == VideoDownload.GROUP_WATCHLATER && it.groupID == id }
getDownloadedVideos().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id } .forEach { removeDownload(it) };
.forEach { deleteCachedVideo(it.id) }; getDownloadedVideos().filter { it.groupType == VideoDownload.GROUP_WATCHLATER && it.groupID == id }
.forEach { deleteCachedVideo(it.id) };
}
else {
getDownloading().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
.forEach { removeDownload(it) };
getDownloadedVideos().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
.forEach { deleteCachedVideo(it.id) };
}
} }
fun getDownloadedVideos(): List<VideoLocal> { fun getDownloadedVideos(): List<VideoLocal> {
@ -192,9 +203,59 @@ class StateDownloads {
else else
Logger.v(TAG, "Offline playlist [${playlist.playlist.name}] is up to date"); Logger.v(TAG, "Offline playlist [${playlist.playlist.name}] is up to date");
} }
val downloadWatchLater = getWatchLaterDescriptor();
if(downloadWatchLater != null) {
continueDownloadWatchLater(downloadWatchLater);
}
return hasChanged; return hasChanged;
} }
fun continueDownloadWatchLater(playlistDownload: PlaylistDownloadDescriptor) {
var hasNew = false;
val watchLater = StatePlaylists.instance.getWatchLater();
for(item in watchLater) {
val existing = getCachedVideo(item.id);
if(!playlistDownload.shouldDownload(item)) {
Logger.i(TAG, "Not downloading for watchlater [${playlistDownload.id}] Video [${item.name}]:${item.url}")
continue;
}
if(existing == null) {
val ongoingDownload = getDownloading().find { it.id.value == item.id.value && it.id.value != null };
if(ongoingDownload != null) {
Logger.i(TAG, "New watchlater video (already downloading) ${item.name}");
ongoingDownload.groupID = VideoDownload.GROUP_WATCHLATER;
ongoingDownload.groupType = VideoDownload.GROUP_WATCHLATER;
}
else {
Logger.i(TAG, "New watchlater video ${item.name}");
download(VideoDownload(item, playlistDownload.targetPxCount, playlistDownload.targetBitrate)
.withGroup(VideoDownload.GROUP_PLAYLIST, VideoDownload.GROUP_WATCHLATER), false);
hasNew = true;
}
}
else {
Logger.i(TAG, "New watchlater video (already downloaded) ${item.name}");
if(existing.groupID == null) {
existing.groupID = VideoDownload.GROUP_WATCHLATER;
existing.groupType = VideoDownload.GROUP_WATCHLATER;
synchronized(_downloadedSet) {
_downloadedSet.add(existing.id);
}
_downloaded.save(existing);
}
}
}
if(watchLater.isNotEmpty() && Settings.instance.downloads.shouldDownload()) {
if(hasNew) {
UIDialogs.toast("Downloading [Watch Later]")
StateApp.withContext {
DownloadService.getOrCreateService(it);
}
}
onDownloadsChanged.emit();
}
}
fun continueDownload(playlistDownload: PlaylistDownloadDescriptor, playlist: Playlist) { fun continueDownload(playlistDownload: PlaylistDownloadDescriptor, playlist: Playlist) {
var hasNew = false; var hasNew = false;
for(item in playlist.videos) { for(item in playlist.videos) {
@ -240,6 +301,11 @@ class StateDownloads {
onDownloadsChanged.emit(); onDownloadsChanged.emit();
} }
} }
fun downloadWatchLater(targetPixelCount: Long?, targetBitrate: Long?) {
val playlistDownload = PlaylistDownloadDescriptor(VideoDownload.GROUP_WATCHLATER, targetPixelCount, targetBitrate);
_downloadPlaylists.save(playlistDownload);
continueDownloadWatchLater(playlistDownload);
}
fun download(playlist: Playlist, targetPixelcount: Long?, targetBitrate: Long?) { fun download(playlist: Playlist, targetPixelcount: Long?, targetBitrate: Long?) {
val playlistDownload = PlaylistDownloadDescriptor(playlist.id, targetPixelcount, targetBitrate); val playlistDownload = PlaylistDownloadDescriptor(playlist.id, targetPixelcount, targetBitrate);
_downloadPlaylists.save(playlistDownload); _downloadPlaylists.save(playlistDownload);

View File

@ -20,6 +20,7 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
val onChannelClicked = Event1<PlatformAuthorLink>(); val onChannelClicked = Event1<PlatformAuthorLink>();
val onAddToClicked = Event1<IPlatformContent>(); val onAddToClicked = Event1<IPlatformContent>();
val onAddToQueueClicked = Event1<IPlatformContent>(); val onAddToQueueClicked = Event1<IPlatformContent>();
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
val onLongPress = Event1<IPlatformContent>(); val onLongPress = Event1<IPlatformContent>();
override fun getItemCount(): Int { override fun getItemCount(): Int {
@ -56,6 +57,7 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit); onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit);
onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit); onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit);
onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit); onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit);
onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit);
onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit); onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit);
}; };
1 -> ChannelListFragment.newInstance().apply { onClickChannel.subscribe(onChannelClicked::emit) }; 1 -> ChannelListFragment.newInstance().apply { onClickChannel.subscribe(onChannelClicked::emit) };

View File

@ -48,6 +48,7 @@ class CommentViewHolder : ViewHolder {
var onRepliesClick = Event1<IPlatformComment>(); var onRepliesClick = Event1<IPlatformComment>();
var onDelete = Event1<IPlatformComment>(); var onDelete = Event1<IPlatformComment>();
var onAuthorClick = Event1<IPlatformComment>();
var comment: IPlatformComment? = null var comment: IPlatformComment? = null
private set; private set;
@ -95,6 +96,19 @@ class CommentViewHolder : ViewHolder {
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked) StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
}; };
_creatorThumbnail.onClick.subscribe {
val c = comment ?: return@subscribe;
onAuthorClick.emit(c);
}
_creatorThumbnail.setOnClickListener {
val c = comment ?: return@setOnClickListener;
onAuthorClick.emit(c);
}
_textAuthor.setOnClickListener {
val c = comment ?: return@setOnClickListener;
onAuthorClick.emit(c);
}
_buttonReplies.onClick.subscribe { _buttonReplies.onClick.subscribe {
val c = comment ?: return@subscribe; val c = comment ?: return@subscribe;
onRepliesClick.emit(c); onRepliesClick.emit(c);

View File

@ -53,9 +53,10 @@ class CommentWithReferenceViewHolder : ViewHolder {
hideLikesDislikesReplies() hideLikesDislikesReplies()
} }
var onRepliesClick = Event1<IPlatformComment>(); val onRepliesClick = Event1<IPlatformComment>();
var onDelete = Event1<IPlatformComment>(); val onDelete = Event1<IPlatformComment>();
var onClick = Event1<IPlatformComment>(); val onClick = Event1<IPlatformComment>();
val onAuthorClick = Event1<IPlatformComment>();
var comment: IPlatformComment? = null var comment: IPlatformComment? = null
private set; private set;
@ -99,6 +100,14 @@ class CommentWithReferenceViewHolder : ViewHolder {
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked) StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
}; };
_creatorThumbnail.onClick.subscribe {
val c = comment ?: return@subscribe;
onAuthorClick.emit(c);
}
_textAuthor.setOnClickListener {
val c = comment ?: return@setOnClickListener;
onAuthorClick.emit(c);
}
_buttonReplies.onClick.subscribe { _buttonReplies.onClick.subscribe {
val c = comment ?: return@subscribe; val c = comment ?: return@subscribe;
onRepliesClick.emit(c); onRepliesClick.emit(c);

View File

@ -39,6 +39,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
val onChannelClicked = Event1<PlatformAuthorLink>(); val onChannelClicked = Event1<PlatformAuthorLink>();
val onAddToClicked = Event1<IPlatformContent>(); val onAddToClicked = Event1<IPlatformContent>();
val onAddToQueueClicked = Event1<IPlatformContent>(); val onAddToQueueClicked = Event1<IPlatformContent>();
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
val onLongPress = Event1<IPlatformContent>(); val onLongPress = Event1<IPlatformContent>();
private var _taskLoadContent = TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>( private var _taskLoadContent = TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>(
@ -95,6 +96,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit); this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit);
this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit); this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit);
this.onAddToQueueClicked.subscribe(this@PreviewContentListAdapter.onAddToQueueClicked::emit); this.onAddToQueueClicked.subscribe(this@PreviewContentListAdapter.onAddToQueueClicked::emit);
this.onAddToWatchLaterClicked.subscribe(this@PreviewContentListAdapter.onAddToWatchLaterClicked::emit);
}; };
private fun createLockedViewHolder(viewGroup: ViewGroup): PreviewLockedViewHolder = PreviewLockedViewHolder(viewGroup, _feedStyle).apply { private fun createLockedViewHolder(viewGroup: ViewGroup): PreviewLockedViewHolder = PreviewLockedViewHolder(viewGroup, _feedStyle).apply {
this.onLockedUrlClicked.subscribe(this@PreviewContentListAdapter.onUrlClicked::emit); this.onLockedUrlClicked.subscribe(this@PreviewContentListAdapter.onUrlClicked::emit);
@ -106,6 +108,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit); this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit);
this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit); this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit);
this.onAddToQueueClicked.subscribe(this@PreviewContentListAdapter.onAddToQueueClicked::emit); this.onAddToQueueClicked.subscribe(this@PreviewContentListAdapter.onAddToQueueClicked::emit);
this.onAddToWatchLaterClicked.subscribe(this@PreviewContentListAdapter.onAddToWatchLaterClicked::emit);
this.onLongPress.subscribe(this@PreviewContentListAdapter.onLongPress::emit); this.onLongPress.subscribe(this@PreviewContentListAdapter.onLongPress::emit);
}; };
private fun createPlaylistViewHolder(viewGroup: ViewGroup): PreviewPlaylistViewHolder = PreviewPlaylistViewHolder(viewGroup, _feedStyle).apply { private fun createPlaylistViewHolder(viewGroup: ViewGroup): PreviewPlaylistViewHolder = PreviewPlaylistViewHolder(viewGroup, _feedStyle).apply {
@ -161,6 +164,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
onChannelClicked.clear(); onChannelClicked.clear();
onAddToClicked.clear(); onAddToClicked.clear();
onAddToQueueClicked.clear(); onAddToQueueClicked.clear();
onAddToWatchLaterClicked.clear();
} }
private fun previewContentDetails(viewHolder: ContentPreviewViewHolder, videoDetails: IPlatformContentDetails?) { private fun previewContentDetails(viewHolder: ContentPreviewViewHolder, videoDetails: IPlatformContentDetails?) {

View File

@ -19,6 +19,7 @@ class PreviewNestedVideoViewHolder : ContentPreviewViewHolder {
val onChannelClicked = Event1<PlatformAuthorLink>(); val onChannelClicked = Event1<PlatformAuthorLink>();
val onAddToClicked = Event1<IPlatformVideo>(); val onAddToClicked = Event1<IPlatformVideo>();
val onAddToQueueClicked = Event1<IPlatformVideo>(); val onAddToQueueClicked = Event1<IPlatformVideo>();
val onAddToWatchLaterClicked = Event1<IPlatformVideo>();
override val content: IPlatformContent? get() = view.content; override val content: IPlatformContent? get() = view.content;
private val view: PreviewNestedVideoView get() = itemView as PreviewNestedVideoView; private val view: PreviewNestedVideoView get() = itemView as PreviewNestedVideoView;
@ -31,6 +32,7 @@ class PreviewNestedVideoViewHolder : ContentPreviewViewHolder {
view.onChannelClicked.subscribe(onChannelClicked::emit); view.onChannelClicked.subscribe(onChannelClicked::emit);
view.onAddToClicked.subscribe(onAddToClicked::emit); view.onAddToClicked.subscribe(onAddToClicked::emit);
view.onAddToQueueClicked.subscribe(onAddToQueueClicked::emit); view.onAddToQueueClicked.subscribe(onAddToQueueClicked::emit);
view.onAddToWatchLaterClicked.subscribe(onAddToWatchLaterClicked::emit);
} }

View File

@ -61,6 +61,7 @@ open class PreviewVideoView : LinearLayout {
protected val _layoutDownloaded: FrameLayout; protected val _layoutDownloaded: FrameLayout;
protected val _button_add_to_queue : View; protected val _button_add_to_queue : View;
protected val _button_add_to_watch_later : View;
protected val _button_add_to : View; protected val _button_add_to : View;
protected val _exoPlayer: PlayerManager?; protected val _exoPlayer: PlayerManager?;
@ -80,6 +81,7 @@ open class PreviewVideoView : LinearLayout {
val onChannelClicked = Event1<PlatformAuthorLink>(); val onChannelClicked = Event1<PlatformAuthorLink>();
val onAddToClicked = Event1<IPlatformVideo>(); val onAddToClicked = Event1<IPlatformVideo>();
val onAddToQueueClicked = Event1<IPlatformVideo>(); val onAddToQueueClicked = Event1<IPlatformVideo>();
val onAddToWatchLaterClicked = Event1<IPlatformVideo>();
var currentVideo: IPlatformVideo? = null var currentVideo: IPlatformVideo? = null
private set private set
@ -104,6 +106,7 @@ open class PreviewVideoView : LinearLayout {
_containerDuration = findViewById(R.id.thumbnail_duration_container); _containerDuration = findViewById(R.id.thumbnail_duration_container);
_containerLive = findViewById(R.id.thumbnail_live_container); _containerLive = findViewById(R.id.thumbnail_live_container);
_button_add_to_queue = findViewById(R.id.button_add_to_queue); _button_add_to_queue = findViewById(R.id.button_add_to_queue);
_button_add_to_watch_later = findViewById(R.id.button_add_to_watch_later);
_button_add_to = findViewById(R.id.button_add_to); _button_add_to = findViewById(R.id.button_add_to);
_imageNeopassChannel = findViewById(R.id.image_neopass_channel); _imageNeopassChannel = findViewById(R.id.image_neopass_channel);
_layoutDownloaded = findViewById(R.id.layout_downloaded); _layoutDownloaded = findViewById(R.id.layout_downloaded);
@ -124,7 +127,7 @@ open class PreviewVideoView : LinearLayout {
_textVideoMetadata.setOnClickListener { currentVideo?.let { onChannelClicked.emit(it.author) } }; _textVideoMetadata.setOnClickListener { currentVideo?.let { onChannelClicked.emit(it.author) } };
_button_add_to.setOnClickListener { currentVideo?.let { onAddToClicked.emit(it) } }; _button_add_to.setOnClickListener { currentVideo?.let { onAddToClicked.emit(it) } };
_button_add_to_queue.setOnClickListener { currentVideo?.let { onAddToQueueClicked.emit(it) } }; _button_add_to_queue.setOnClickListener { currentVideo?.let { onAddToQueueClicked.emit(it) } };
_button_add_to_watch_later.setOnClickListener { currentVideo?.let { onAddToWatchLaterClicked.emit(it); } }
} }
protected open fun inflate(feedStyle: FeedStyle) { protected open fun inflate(feedStyle: FeedStyle) {

View File

@ -18,6 +18,7 @@ class PreviewVideoViewHolder : ContentPreviewViewHolder {
val onChannelClicked = Event1<PlatformAuthorLink>(); val onChannelClicked = Event1<PlatformAuthorLink>();
val onAddToClicked = Event1<IPlatformVideo>(); val onAddToClicked = Event1<IPlatformVideo>();
val onAddToQueueClicked = Event1<IPlatformVideo>(); val onAddToQueueClicked = Event1<IPlatformVideo>();
val onAddToWatchLaterClicked = Event1<IPlatformVideo>();
val onLongPress = Event1<IPlatformVideo>(); val onLongPress = Event1<IPlatformVideo>();
//val context: Context; //val context: Context;
@ -34,6 +35,7 @@ class PreviewVideoViewHolder : ContentPreviewViewHolder {
view.onChannelClicked.subscribe(onChannelClicked::emit); view.onChannelClicked.subscribe(onChannelClicked::emit);
view.onAddToClicked.subscribe(onAddToClicked::emit); view.onAddToClicked.subscribe(onAddToClicked::emit);
view.onAddToQueueClicked.subscribe(onAddToQueueClicked::emit); view.onAddToQueueClicked.subscribe(onAddToQueueClicked::emit);
view.onAddToWatchLaterClicked.subscribe(onAddToWatchLaterClicked::emit);
view.onLongPress.subscribe(onLongPress::emit); view.onLongPress.subscribe(onLongPress::emit);
} }

View File

@ -9,16 +9,16 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.models.PlaylistDownloaded import com.futo.platformplayer.models.PlaylistDownloaded
class PlaylistDownloadItem(context: Context, val playlist: PlaylistDownloaded): LinearLayout(context) { class PlaylistDownloadItem(context: Context, playlistName: String, playlistThumbnail: String?, val obj: Any): LinearLayout(context) {
init { inflate(context, R.layout.list_downloaded_playlist, this) } init { inflate(context, R.layout.list_downloaded_playlist, this) }
var imageView: ImageView = findViewById(R.id.downloaded_playlist_image); var imageView: ImageView = findViewById(R.id.downloaded_playlist_image);
var imageText: TextView = findViewById(R.id.downloaded_playlist_name); var imageText: TextView = findViewById(R.id.downloaded_playlist_name);
init { init {
imageText.text = playlist.playlist.name; imageText.text = playlistName;
Glide.with(imageView) Glide.with(imageView)
.load(playlist.playlist.videos.firstOrNull()?.thumbnails?.getHQThumbnail()) .load(playlistThumbnail)
.crossfade() .crossfade()
.into(imageView); .into(imageView);
} }

View File

@ -0,0 +1,38 @@
package com.futo.platformplayer.views.overlays
import android.content.Context
import android.util.AttributeSet
import android.webkit.WebView
import android.widget.LinearLayout
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.views.SupportView
class WebviewOverlay : LinearLayout {
val onClose = Event0();
private val _topbar: OverlayTopbar;
private val _webview: WebView;
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.overlay_webview, this)
_topbar = findViewById(R.id.topbar);
_webview = findViewById(R.id.webview);
_webview.settings.javaScriptEnabled = true;
_topbar.onClose.subscribe(this, onClose::emit);
}
fun goto(url: String) {
Logger.i("WebviewOverlay", "Loading [${url}]");
_topbar.setInfo(url, "");
_webview.loadUrl(url);
}
fun cleanup() {
_topbar.onClose.remove(this);
}
}

View File

@ -88,6 +88,7 @@ class CommentsList : ConstraintLayout {
private val _layoutScrollToTop: FrameLayout; private val _layoutScrollToTop: FrameLayout;
var onRepliesClick = Event1<IPlatformComment>(); var onRepliesClick = Event1<IPlatformComment>();
var onAuthorClick = Event1<IPlatformComment>();
var onCommentsLoaded = Event1<Int>(); var onCommentsLoaded = Event1<Int>();
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
@ -120,6 +121,7 @@ class CommentsList : ConstraintLayout {
childViewHolderFactory = { viewGroup, _ -> childViewHolderFactory = { viewGroup, _ ->
val holder = CommentViewHolder(viewGroup); val holder = CommentViewHolder(viewGroup);
holder.onRepliesClick.subscribe { c -> onRepliesClick.emit(c) }; holder.onRepliesClick.subscribe { c -> onRepliesClick.emit(c) };
holder.onAuthorClick.subscribe { c -> onAuthorClick.emit(c) };
holder.onDelete.subscribe(::onDelete); holder.onDelete.subscribe(::onDelete);
return@InsertedViewAdapterWithLoader holder; return@InsertedViewAdapterWithLoader holder;
} }

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M612,668L668,612L520,464L520,280L440,280L440,496L612,668ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM480,800Q613,800 706.5,706.5Q800,613 800,480Q800,347 706.5,253.5Q613,160 480,160Q347,160 253.5,253.5Q160,347 160,480Q160,613 253.5,706.5Q347,800 480,800Z"/>
</vector>

View File

@ -26,7 +26,7 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:textSize="28dp" android:textSize="22dp"
android:layout_marginTop="-2dp" android:layout_marginTop="-2dp"
android:fontFamily="@font/inter_light" android:fontFamily="@font/inter_light"
android:text="Grayjay" android:text="Grayjay"

View File

@ -542,6 +542,12 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<com.futo.platformplayer.views.overlays.WebviewOverlay
android:id="@+id/videodetail_container_webview"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.futo.platformplayer.views.overlays.QueueEditorOverlay <com.futo.platformplayer.views.overlays.QueueEditorOverlay
android:id="@+id/videodetail_container_queue" android:id="@+id/videodetail_container_queue"
android:visibility="gone" android:visibility="gone"

View File

@ -226,10 +226,18 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"> app:layout_constraintRight_toRightOf="parent">
<ImageButton
android:id="@+id/button_add_to_watch_later"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginEnd="5dp"
android:background="@drawable/edit_text_background"
app:srcCompat="@drawable/ic_clock_white" />
<ImageButton <ImageButton
android:id="@+id/button_add_to_queue" android:id="@+id/button_add_to_queue"
android:layout_width="wrap_content" android:layout_width="30dp"
android:layout_height="wrap_content" android:layout_height="30dp"
android:layout_marginEnd="1dp" android:layout_marginEnd="1dp"
android:background="@drawable/edit_text_background" android:background="@drawable/edit_text_background"
android:contentDescription="@string/add_to_queue" android:contentDescription="@string/add_to_queue"
@ -242,20 +250,18 @@
<LinearLayout <LinearLayout
android:id="@+id/button_add_to" android:id="@+id/button_add_to"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="30dp"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
android:background="@drawable/edit_text_background" android:background="@drawable/edit_text_background"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="4dp"> android:padding="4dp">
<ImageButton <ImageView
android:layout_width="wrap_content" android:layout_width="20dp"
android:layout_height="wrap_content" android:layout_height="16dp"
android:layout_marginStart="4dp" android:paddingTop="1dp"
android:layout_marginEnd="4dp" android:src="@drawable/ic_settings" />
android:contentDescription="@string/options"
app:srcCompat="@drawable/ic_add_white_8dp" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -262,45 +262,53 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingEnd="6dp"> android:paddingEnd="6dp">
<ImageButton
android:id="@+id/button_add_to_watch_later"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginEnd="5dp"
android:background="@drawable/edit_text_background"
app:srcCompat="@drawable/ic_clock_white" />
<ImageButton <ImageButton
android:id="@+id/button_add_to_queue" android:id="@+id/button_add_to_queue"
android:layout_width="wrap_content" android:layout_width="30dp"
android:layout_height="wrap_content" android:layout_height="30dp"
android:layout_marginEnd="1dp" android:layout_marginEnd="1dp"
android:paddingTop="7dp" android:background="@drawable/edit_text_background"
android:contentDescription="@string/add_to_queue"
android:paddingStart="6dp" android:paddingStart="6dp"
android:paddingTop="7dp"
android:paddingEnd="5dp" android:paddingEnd="5dp"
android:paddingBottom="3dp" android:paddingBottom="3dp"
app:srcCompat="@drawable/ic_queue_16dp" app:srcCompat="@drawable/ic_queue_16dp" />
android:background="@drawable/edit_text_background"
android:contentDescription="@string/add_to_queue" />
<LinearLayout <LinearLayout
android:id="@+id/button_add_to" android:id="@+id/button_add_to"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="30dp"
android:orientation="horizontal"
android:background="@drawable/edit_text_background"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
android:background="@drawable/edit_text_background"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="4dp"> android:padding="4dp">
<ImageButton
android:layout_width="wrap_content" <ImageView
android:layout_height="wrap_content" android:layout_width="20dp"
android:layout_marginEnd="4dp" android:layout_height="16dp"
app:srcCompat="@drawable/ic_add_white_8dp" android:paddingTop="1dp"
android:layout_marginStart="4dp" android:src="@drawable/ic_settings" />
android:contentDescription="@string/options" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/options" android:layout_marginEnd="4dp"
android:background="@color/transparent" android:background="@color/transparent"
android:textSize="12dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_light" android:fontFamily="@font/inter_light"
android:layout_marginEnd="4dp"/> android:text="@string/options"
android:textColor="@color/white"
android:textSize="12dp" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -150,14 +150,11 @@
android:padding="5dp" android:padding="5dp"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"> app:layout_constraintBottom_toBottomOf="parent">
<ImageButton <ImageView
android:layout_width="wrap_content" android:layout_width="20dp"
android:layout_height="wrap_content" android:layout_height="16dp"
android:layout_marginEnd="4dp" android:paddingTop="1dp"
app:srcCompat="@drawable/ic_add_white_8dp" android:src="@drawable/ic_settings" />
android:background="@color/transparent"
android:layout_marginStart="4dp"
android:contentDescription="@string/options" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -180,13 +177,28 @@
android:paddingBottom="2dp" android:paddingBottom="2dp"
android:paddingLeft="10dp" android:paddingLeft="10dp"
android:paddingRight="7dp" android:paddingRight="7dp"
android:layout_marginLeft="7dp" android:layout_marginLeft="5dp"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:background="@drawable/edit_text_background" android:background="@drawable/edit_text_background"
android:contentDescription="@string/add_to_queue" android:contentDescription="@string/add_to_queue"
app:layout_constraintLeft_toRightOf="@id/button_add_to" app:layout_constraintLeft_toRightOf="@id/button_add_to"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintBottom_toBottomOf="parent" />
<ImageButton
android:id="@+id/button_add_to_watch_later"
android:layout_width="wrap_content"
android:layout_height="27dp"
android:src="@drawable/ic_clock_white"
android:paddingTop="7dp"
android:paddingBottom="6dp"
android:paddingLeft="9dp"
android:paddingRight="9dp"
android:layout_marginLeft="5dp"
android:scaleType="fitCenter"
android:background="@drawable/edit_text_background"
app:layout_constraintLeft_toRightOf="@id/button_add_to_queue"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView <TextView
android:id="@+id/text_video_name" android:id="@+id/text_video_name"
android:layout_width="fill_parent" android:layout_width="fill_parent"

View File

@ -206,6 +206,7 @@
</LinearLayout> </LinearLayout>
<ImageButton <ImageButton
android:id="@+id/button_add_to_queue" android:id="@+id/button_add_to_queue"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -215,13 +216,28 @@
android:paddingBottom="2dp" android:paddingBottom="2dp"
android:paddingLeft="10dp" android:paddingLeft="10dp"
android:paddingRight="7dp" android:paddingRight="7dp"
android:layout_marginLeft="7dp" android:layout_marginLeft="5dp"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:background="@drawable/edit_text_background" android:background="@drawable/edit_text_background"
android:contentDescription="@string/add_to_queue" android:contentDescription="@string/add_to_queue"
app:layout_constraintLeft_toRightOf="@id/button_add_to" app:layout_constraintLeft_toRightOf="@id/button_add_to"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintBottom_toBottomOf="parent" />
<ImageButton
android:id="@+id/button_add_to_watch_later"
android:layout_width="wrap_content"
android:layout_height="27dp"
android:src="@drawable/ic_clock_white"
android:paddingTop="7dp"
android:paddingBottom="6dp"
android:paddingLeft="9dp"
android:paddingRight="9dp"
android:layout_marginLeft="5dp"
android:scaleType="fitCenter"
android:background="@drawable/edit_text_background"
app:layout_constraintLeft_toRightOf="@id/button_add_to_queue"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView <TextView
android:id="@+id/text_video_name" android:id="@+id/text_video_name"
android:layout_width="fill_parent" android:layout_width="fill_parent"

View File

@ -16,6 +16,9 @@
android:textColor="@color/white" android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
android:maxLines="1"
android:ellipsize="end"
android:maxWidth="300dp"
tools:text="Queue" /> tools:text="Queue" />
<TextView <TextView
@ -24,16 +27,21 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/text_name" app:layout_constraintLeft_toRightOf="@id/text_name"
app:layout_constraintRight_toLeftOf="@id/button_container"
android:fontFamily="@font/inter_regular" android:fontFamily="@font/inter_regular"
android:textColor="#ACACAC" android:textColor="#ACACAC"
android:textSize="13dp" android:textSize="13dp"
android:layout_marginLeft="15dp" android:layout_marginLeft="15dp"
android:layout_marginBottom="7dp" android:layout_marginBottom="7dp"
android:layout_marginRight="45dp"
android:maxLines="1"
android:ellipsize="end"
tools:text="3 videos" /> tools:text="3 videos" />
<LinearLayout <LinearLayout
android:id="@+id/button_container"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.futo.platformplayer.views.overlays.OverlayTopbar
android:id="@+id/topbar"
android:layout_width="match_parent"
android:layout_height="40dp"
app:title="Web"
app:metadata=""
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/topbar"
app:layout_constraintBottom_toBottomOf="parent">
</WebView>
</androidx.constraintlayout.widget.ConstraintLayout>