Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay

This commit is contained in:
Kelvin 2023-11-15 20:02:36 +01:00
commit 1393c489c1
19 changed files with 271 additions and 104 deletions

View file

@ -5,6 +5,8 @@ import android.graphics.Color
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
@ -369,6 +371,33 @@ class UISlideOverlays {
return overlay; return overlay;
} }
fun showCreatePlaylistOverlay(container: ViewGroup, onCreate: (String) -> Unit): SlideUpMenuOverlay {
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
addPlaylistOverlay.onOK.subscribe {
val text = nameInput.text;
if (text.isBlank()) {
return@subscribe;
}
addPlaylistOverlay.hide();
nameInput.deactivate();
nameInput.clear();
onCreate(text)
};
addPlaylistOverlay.onCancel.subscribe {
nameInput.deactivate();
nameInput.clear();
};
addPlaylistOverlay.show();
nameInput.activate();
return addPlaylistOverlay
}
fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, vararg actions: SlideUpMenuItem): SlideUpMenuOverlay { fun showVideoOptionsOverlay(video: IPlatformVideo, container: ViewGroup, vararg actions: SlideUpMenuItem): SlideUpMenuOverlay {
val items = arrayListOf<View>(); val items = arrayListOf<View>();
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist(); val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
@ -407,6 +436,13 @@ class UISlideOverlays {
)); ));
val playlistItems = arrayListOf<SlideUpMenuItem>(); val playlistItems = arrayListOf<SlideUpMenuItem>();
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
showCreatePlaylistOverlay(container) {
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
};
}, false))
for (playlist in allPlaylists) { for (playlist in allPlaylists) {
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, "${container.context.getString(R.string.add_to)} " + playlist.name + "", "${playlist.videos.size} " + container.context.getString(R.string.videos), "", playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, "${container.context.getString(R.string.add_to)} " + playlist.name + "", "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
{ {

View file

@ -7,7 +7,6 @@ class HttpConstantHandler(method: String, path: String, val content: String, val
val headers = this.headers.clone(); val headers = this.headers.clone();
if(contentType != null) if(contentType != null)
headers["Content-Type"] = contentType; headers["Content-Type"] = contentType;
headers["Content-Length"] = content.length.toString();
httpContext.respondCode(200, headers, content); httpContext.respondCode(200, headers, content);
} }

View file

@ -1,14 +1,16 @@
package com.futo.platformplayer.api.http.server.handlers package com.futo.platformplayer.api.http.server.handlers
import com.futo.platformplayer.api.http.server.HttpContext import com.futo.platformplayer.api.http.server.HttpContext
import com.futo.platformplayer.api.http.server.HttpHeaders
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.nio.file.Files import java.nio.file.Files
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.zip.GZIPOutputStream import java.util.zip.GZIPOutputStream
class HttpFileHandler(method: String, path: String, private val contentType: String, private val filePath: String, private val closeAfterRequest: Boolean = false): HttpHandler(method, path) { class HttpFileHandler(method: String, path: String, private val contentType: String, private val filePath: String): HttpHandler(method, path) {
override fun handle(httpContext: HttpContext) { override fun handle(httpContext: HttpContext) {
val requestHeaders = httpContext.headers; val requestHeaders = httpContext.headers;
val responseHeaders = this.headers.clone(); val responseHeaders = this.headers.clone();
@ -30,19 +32,13 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
responseHeaders["Content-Disposition"] = "attachment; filename=\"${file.name.replace("\"", "\\\"")}\"" responseHeaders["Content-Disposition"] = "attachment; filename=\"${file.name.replace("\"", "\\\"")}\""
val acceptEncoding = requestHeaders["Accept-Encoding"]
val shouldGzip = acceptEncoding != null && acceptEncoding.split(',').any { it.trim().equals("gzip", ignoreCase = true) || it == "*" }
if (shouldGzip) {
responseHeaders["Content-Encoding"] = "gzip"
}
val range = requestHeaders["Range"] val range = requestHeaders["Range"]
var start: Long val start: Long
val end: Long val end: Long
if (range != null && range.startsWith("bytes=")) { if (range != null && range.startsWith("bytes=")) {
val parts = range.substring(6).split("-") val parts = range.substring(6).split("-")
start = parts[0].toLong() start = parts[0].toLong()
end = parts.getOrNull(1)?.toLong() ?: (file.length() - 1) end = parts.getOrNull(1)?.toLongOrNull() ?: (file.length() - 1)
responseHeaders["Content-Range"] = "bytes $start-$end/${file.length()}" responseHeaders["Content-Range"] = "bytes $start-$end/${file.length()}"
} else { } else {
start = 0 start = 0
@ -51,18 +47,19 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
var totalBytesSent = 0 var totalBytesSent = 0
val contentLength = end - start + 1 val contentLength = end - start + 1
Logger.i(TAG, "Sending $contentLength bytes (start: $start, end: $end, shouldGzip: $shouldGzip)")
responseHeaders["Content-Length"] = contentLength.toString() responseHeaders["Content-Length"] = contentLength.toString()
Logger.i(TAG, "Sending $contentLength bytes (start: $start, end: $end)")
file.inputStream().use { inputStream -> file.inputStream().use { inputStream ->
httpContext.respond(if (range == null) 200 else 206, responseHeaders) { responseStream -> httpContext.respond(if (range != null) 206 else 200, responseHeaders) { responseStream ->
try { try {
val buffer = ByteArray(8192) val buffer = ByteArray(8192)
inputStream.skip(start) inputStream.skip(start)
var current = start
val outputStream = if (shouldGzip) GZIPOutputStream(responseStream) else responseStream val outputStream = responseStream
while (true) { while (true) {
val expectedBytesRead = (end - start + 1).coerceAtMost(buffer.size.toLong()); val expectedBytesRead = (end - current + 1).coerceAtMost(buffer.size.toLong());
val bytesRead = inputStream.read(buffer); val bytesRead = inputStream.read(buffer);
if (bytesRead < 0) { if (bytesRead < 0) {
Logger.i(TAG, "End of file reached") Logger.i(TAG, "End of file reached")
@ -73,27 +70,21 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
outputStream.write(buffer, 0, bytesToSend) outputStream.write(buffer, 0, bytesToSend)
totalBytesSent += bytesToSend totalBytesSent += bytesToSend
Logger.v(TAG, "Sent bytes $start-${start + bytesToSend}, totalBytesSent=$totalBytesSent") Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
start += bytesToSend.toLong() current += bytesToSend.toLong()
if (start >= end) { if (current >= end) {
Logger.i(TAG, "Expected amount of bytes sent") Logger.i(TAG, "Expected amount of bytes sent")
break break
} }
} }
Logger.i(TAG, "Finished sending file (segment)") Logger.i(TAG, "Finished sending file (segment)")
if (shouldGzip) (outputStream as GZIPOutputStream).finish()
outputStream.flush() outputStream.flush()
} catch (e: Exception) { } catch (e: Exception) {
httpContext.respondCode(500, headers) httpContext.respondCode(500, headers)
} }
} }
if (closeAfterRequest) {
httpContext.keepAlive = false;
}
} }
} }

View file

@ -4,6 +4,7 @@ import android.net.Uri
import com.futo.platformplayer.api.http.server.HttpContext import com.futo.platformplayer.api.http.server.HttpContext
import com.futo.platformplayer.api.http.server.HttpHeaders import com.futo.platformplayer.api.http.server.HttpHeaders
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.logging.Logger
class HttpProxyHandler(method: String, path: String, val targetUrl: String): HttpHandler(method, path) { class HttpProxyHandler(method: String, path: String, val targetUrl: String): HttpHandler(method, path) {
var content: String? = null; var content: String? = null;
@ -34,8 +35,8 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
proxyHeaders.put("Referer", targetUrl); proxyHeaders.put("Referer", targetUrl);
val useMethod = if (method == "inherit") context.method else method; val useMethod = if (method == "inherit") context.method else method;
//Logger.i(TAG, "Proxied Request ${useMethod}: ${targetUrl}"); Logger.i(TAG, "Proxied Request ${useMethod}: ${targetUrl}");
//Logger.i(TAG, "Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n")); Logger.i(TAG, "Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
val resp = when (useMethod) { val resp = when (useMethod) {
"GET" -> _client.get(targetUrl, proxyHeaders); "GET" -> _client.get(targetUrl, proxyHeaders);
@ -44,7 +45,7 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
else -> _client.requestMethod(useMethod, targetUrl, proxyHeaders); else -> _client.requestMethod(useMethod, targetUrl, proxyHeaders);
}; };
//Logger.i(TAG, "Proxied Response [${resp.code}]"); Logger.i(TAG, "Proxied Response [${resp.code}]");
val headersFiltered = HttpHeaders(resp.getHeadersFlat().filter { !_ignoreRequestHeaders.contains(it.key.lowercase()) }); val headersFiltered = HttpHeaders(resp.getHeadersFlat().filter { !_ignoreRequestHeaders.contains(it.key.lowercase()) });
for(newHeader in headers) for(newHeader in headers)
headersFiltered.put(newHeader.key, newHeader.value); headersFiltered.put(newHeader.key, newHeader.value);
@ -92,4 +93,8 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
_ignoreRequestHeaders.add("referer"); _ignoreRequestHeaders.add("referer");
return this; return this;
} }
companion object {
private const val TAG = "HttpProxyHandler"
}
} }

View file

@ -486,7 +486,7 @@ class StateCasting {
} }
if (subtitleSource != null) { if (subtitleSource != null) {
_castServer.addHandler( _castServer.addHandler(
HttpFileHandler("GET", subtitlePath, subtitleSource.format ?: "text/vtt", subtitleSource.filePath, true) HttpFileHandler("GET", subtitlePath, subtitleSource.format ?: "text/vtt", subtitleSource.filePath)
.withHeader("Access-Control-Allow-Origin", "*"), true .withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("cast"); ).withTag("cast");
_castServer.addHandler( _castServer.addHandler(

View file

@ -27,6 +27,7 @@ import com.futo.platformplayer.views.adapters.InsertedViewHolder
import com.futo.platformplayer.views.adapters.PreviewNestedVideoViewHolder import com.futo.platformplayer.views.adapters.PreviewNestedVideoViewHolder
import com.futo.platformplayer.views.adapters.PreviewVideoViewHolder import com.futo.platformplayer.views.adapters.PreviewVideoViewHolder
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import kotlin.math.floor import kotlin.math.floor
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment { abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
@ -37,6 +38,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
private var _previewsEnabled: Boolean = true; private var _previewsEnabled: Boolean = true;
override val visibleThreshold: Int get() = if (feedStyle == FeedStyle.PREVIEW) { 5 } else { 10 }; override val visibleThreshold: Int get() = if (feedStyle == FeedStyle.PREVIEW) { 5 } else { 10 };
protected lateinit var headerView: LinearLayout; protected lateinit var headerView: LinearLayout;
private var _videoOptionsOverlay: SlideUpMenuOverlay? = null;
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) { constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
@ -70,26 +72,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
adapter.onChannelClicked.subscribe(this) { fragment.navigate<ChannelFragment>(it) }; adapter.onChannelClicked.subscribe(this) { fragment.navigate<ChannelFragment>(it) };
adapter.onAddToClicked.subscribe(this) { content -> adapter.onAddToClicked.subscribe(this) { content ->
//TODO: Reconstruct search video from detail if search is null //TODO: Reconstruct search video from detail if search is null
_overlayContainer.let { if(content is IPlatformVideo) {
if(content is IPlatformVideo) showVideoOptionsOverlay(content)
UISlideOverlays.showVideoOptionsOverlay(content, it, SlideUpMenuItem(context, R.drawable.ic_visibility_off, context.getString(R.string.hide), context.getString(R.string.hide_from_home), "hide",
{ StateMeta.instance.addHiddenVideo(content.url);
if (fragment is HomeFragment) {
val removeIndex = recyclerData.results.indexOf(content);
if (removeIndex >= 0) {
recyclerData.results.removeAt(removeIndex);
recyclerData.adapter.notifyItemRemoved(recyclerData.adapter.childToParentPosition(removeIndex));
}
}
}),
SlideUpMenuItem(context, R.drawable.ic_playlist, context.getString(R.string.play_feed_as_queue), context.getString(R.string.play_entire_feed), "playFeed",
{
val newQueue = listOf(content) + recyclerData.results
.filterIsInstance<IPlatformVideo>()
.filter { it != content };
StatePlayer.instance.setQueue(newQueue, StatePlayer.TYPE_QUEUE, "Feed Queue", true, false);
})
);
} }
}; };
adapter.onAddToQueueClicked.subscribe(this) { adapter.onAddToQueueClicked.subscribe(this) {
@ -99,6 +83,50 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
UIDialogs.toast(context, context.getString(R.string.queued) + " [$name]", false); UIDialogs.toast(context, context.getString(R.string.queued) + " [$name]", false);
} }
}; };
adapter.onLongPress.subscribe(this) {
if (it is IPlatformVideo) {
showVideoOptionsOverlay(it)
}
};
}
fun onBackPressed(): Boolean {
val videoOptionsOverlay = _videoOptionsOverlay
if (videoOptionsOverlay != null) {
if (videoOptionsOverlay.isVisible) {
videoOptionsOverlay.hide();
_videoOptionsOverlay = null
return true;
}
_videoOptionsOverlay = null
return false
}
return false
}
private fun showVideoOptionsOverlay(content: IPlatformVideo) {
_overlayContainer.let {
_videoOptionsOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it, SlideUpMenuItem(context, R.drawable.ic_visibility_off, context.getString(R.string.hide), context.getString(R.string.hide_from_home), "hide",
{ StateMeta.instance.addHiddenVideo(content.url);
if (fragment is HomeFragment) {
val removeIndex = recyclerData.results.indexOf(content);
if (removeIndex >= 0) {
recyclerData.results.removeAt(removeIndex);
recyclerData.adapter.notifyItemRemoved(recyclerData.adapter.childToParentPosition(removeIndex));
}
}
}),
SlideUpMenuItem(context, R.drawable.ic_playlist, context.getString(R.string.play_feed_as_queue), context.getString(R.string.play_entire_feed), "playFeed",
{
val newQueue = listOf(content) + recyclerData.results
.filterIsInstance<IPlatformVideo>()
.filter { it != content };
StatePlayer.instance.setQueue(newQueue, StatePlayer.TYPE_QUEUE, "Feed Queue", true, false);
})
);
}
} }
private fun detachAdapterEvents() { private fun detachAdapterEvents() {
@ -108,6 +136,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.onLongPress.remove(this);
} }
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) { override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
@ -137,11 +166,14 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
protected open fun onContentClicked(content: IPlatformContent, time: Long) { protected open fun onContentClicked(content: IPlatformContent, time: Long) {
if(content is IPlatformVideo) { if(content is IPlatformVideo) {
StatePlayer.instance.clearQueue(); if (StatePlayer.instance.hasQueue) {
if (Settings.instance.playback.shouldResumePreview(time)) StatePlayer.instance.addToQueue(content)
fragment.navigate<VideoDetailFragment>(content.withTimestamp(time)).maximizeVideoDetail(); } else {
else if (Settings.instance.playback.shouldResumePreview(time))
fragment.navigate<VideoDetailFragment>(content).maximizeVideoDetail(); fragment.navigate<VideoDetailFragment>(content.withTimestamp(time)).maximizeVideoDetail();
else
fragment.navigate<VideoDetailFragment>(content).maximizeVideoDetail();
}
} else if (content is IPlatformPlaylist) { } else if (content is IPlatformPlaylist) {
fragment.navigate<PlaylistFragment>(content); fragment.navigate<PlaylistFragment>(content);
} else if (content is IPlatformPost) { } else if (content is IPlatformPost) {

View file

@ -62,6 +62,13 @@ class ContentSearchResultsFragment : MainFragment() {
_view = null; _view = null;
} }
override fun onBackPressed(): Boolean {
if (_view?.onBackPressed() == true)
return true
return super.onBackPressed()
}
fun setPreviewsEnabled(previewsEnabled: Boolean) { fun setPreviewsEnabled(previewsEnabled: Boolean) {
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.search.previewFeedItems); _view?.setPreviewsEnabled(previewsEnabled && Settings.instance.search.previewFeedItems);
} }

View file

@ -66,6 +66,13 @@ class HomeFragment : MainFragment() {
return view; return view;
} }
override fun onBackPressed(): Boolean {
if (_view?.onBackPressed() == true)
return true
return super.onBackPressed()
}
override fun onDestroyMainView() { override fun onDestroyMainView() {
super.onDestroyMainView(); super.onDestroyMainView();

View file

@ -44,6 +44,13 @@ class PlaylistSearchResultsFragment : MainFragment() {
return view; return view;
} }
override fun onBackPressed(): Boolean {
if (_view?.onBackPressed() == true)
return true
return super.onBackPressed()
}
override fun onDestroyMainView() { override fun onDestroyMainView() {
super.onDestroyMainView(); super.onDestroyMainView();
_view?.cleanup(); _view?.cleanup();

View file

@ -17,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView
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 com.futo.platformplayer.R import com.futo.platformplayer.R
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.assume
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
@ -54,6 +55,14 @@ class PlaylistsFragment : MainFragment() {
_view?.onShown(parameter, isBack); _view?.onShown(parameter, isBack);
} }
override fun onBackPressed(): Boolean {
if (_view?.onBackPressed() == true) {
return true;
}
return super.onBackPressed()
}
@SuppressLint("ViewConstructor") @SuppressLint("ViewConstructor")
class PlaylistsView : LinearLayout { class PlaylistsView : LinearLayout {
private val _fragment: PlaylistsFragment; private val _fragment: PlaylistsFragment;
@ -64,6 +73,7 @@ class PlaylistsFragment : MainFragment() {
private var _adapterWatchLater: VideoListHorizontalAdapter; private var _adapterWatchLater: VideoListHorizontalAdapter;
private var _adapterPlaylist: PlaylistsAdapter; private var _adapterPlaylist: PlaylistsAdapter;
private var _layoutWatchlist: ConstraintLayout; private var _layoutWatchlist: ConstraintLayout;
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
constructor(fragment: PlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) { constructor(fragment: PlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) {
_fragment = fragment; _fragment = fragment;
@ -92,41 +102,24 @@ class PlaylistsFragment : MainFragment() {
recyclerPlaylists.adapter = _adapterPlaylist; recyclerPlaylists.adapter = _adapterPlaylist;
recyclerPlaylists.layoutManager = LinearLayoutManager(context); recyclerPlaylists.layoutManager = LinearLayoutManager(context);
val nameInput = SlideUpMenuTextInput(context, context.getString(R.string.name));
val addPlaylistOverlay = SlideUpMenuOverlay(context, findViewById<FrameLayout>(R.id.overlay_create_playlist), context.getString(R.string.create_new_playlist), context.getString(R.string.ok), false, nameInput);
val buttonCreatePlaylist = findViewById<ImageButton>(R.id.button_create_playlist);
buttonCreatePlaylist.setOnClickListener {
_slideUpOverlay = UISlideOverlays.showCreatePlaylistOverlay(findViewById<FrameLayout>(R.id.overlay_create_playlist)) {
val playlist = Playlist(it, arrayListOf());
playlists.add(0, playlist);
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
_adapterPlaylist.notifyItemInserted(0);
};
};
_adapterPlaylist.onClick.subscribe { p -> _fragment.navigate<PlaylistFragment>(p); }; _adapterPlaylist.onClick.subscribe { p -> _fragment.navigate<PlaylistFragment>(p); };
_adapterPlaylist.onPlay.subscribe { p -> _adapterPlaylist.onPlay.subscribe { p ->
StatePlayer.instance.setPlaylist(p, 0, true); StatePlayer.instance.setPlaylist(p, 0, true);
}; };
addPlaylistOverlay.onOK.subscribe {
val text = nameInput.text;
if (text.isBlank()) {
return@subscribe;
}
val playlist = Playlist(text, arrayListOf());
playlists.add(0, playlist);
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
_adapterPlaylist.notifyItemInserted(0);
addPlaylistOverlay.hide();
nameInput.deactivate();
nameInput.clear();
};
addPlaylistOverlay.onCancel.subscribe {
nameInput.deactivate();
nameInput.clear();
};
val buttonCreatePlaylist = findViewById<ImageButton>(R.id.button_create_playlist);
buttonCreatePlaylist.setOnClickListener {
addPlaylistOverlay.show();
nameInput.activate();
};
_appBar = findViewById(R.id.app_bar); _appBar = findViewById(R.id.app_bar);
_layoutWatchlist = findViewById(R.id.layout_watchlist); _layoutWatchlist = findViewById(R.id.layout_watchlist);
@ -142,12 +135,28 @@ class PlaylistsFragment : MainFragment() {
fun onShown(parameter: Any?, isBack: Boolean) { fun onShown(parameter: Any?, isBack: Boolean) {
playlists.clear() playlists.clear()
playlists.addAll(StatePlaylists.instance.getPlaylists().sortedByDescending { maxOf(it.datePlayed, it.dateUpdate, it.dateCreation) }); playlists.addAll(
StatePlaylists.instance.getPlaylists()
.sortedByDescending { maxOf(it.datePlayed, it.dateUpdate, it.dateCreation) });
_adapterPlaylist.notifyDataSetChanged(); _adapterPlaylist.notifyDataSetChanged();
updateWatchLater(); updateWatchLater();
} }
fun onBackPressed(): Boolean {
val slideUpOverlay = _slideUpOverlay;
if (slideUpOverlay != null) {
if (slideUpOverlay.isVisible) {
slideUpOverlay.hide();
return true;
}
return false;
}
return false;
}
private fun updateWatchLater() { private fun updateWatchLater() {
val watchList = StatePlaylists.instance.getWatchLater(); val watchList = StatePlaylists.instance.getWatchLater();
if (watchList.isNotEmpty()) { if (watchList.isNotEmpty()) {

View file

@ -80,6 +80,13 @@ class SubscriptionsFeedFragment : MainFragment() {
} }
} }
override fun onBackPressed(): Boolean {
if (_view?.onBackPressed() == true)
return true
return super.onBackPressed()
}
fun setPreviewsEnabled(previewsEnabled: Boolean) { fun setPreviewsEnabled(previewsEnabled: Boolean) {
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.subscriptions.previewFeedItems); _view?.setPreviewsEnabled(previewsEnabled && Settings.instance.subscriptions.previewFeedItems);
} }

View file

@ -54,6 +54,12 @@ class StatePlayer {
var queueShuffle: Boolean = false var queueShuffle: Boolean = false
private set; private set;
val hasQueue: Boolean get() {
synchronized(_queue) {
return _queue.isNotEmpty()
}
}
val queueName: String get() = _queueName ?: _queueType; val queueName: String get() = _queueName ?: _queueType;
//Events //Events

View file

@ -32,6 +32,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 onLongPress = Event1<IPlatformContent>();
private var _taskLoadContent = TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>( private var _taskLoadContent = TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>(
StateApp.instance.scopeGetter, { (viewHolder, video) -> StateApp.instance.scopeGetter, { (viewHolder, video) ->
@ -93,6 +94,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.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 {
this.onPlaylistClicked.subscribe { this@PreviewContentListAdapter.onContentClicked.emit(it, 0L) }; this.onPlaylistClicked.subscribe { this@PreviewContentListAdapter.onContentClicked.emit(it, 0L) };

View file

@ -68,6 +68,7 @@ open class PreviewVideoView : LinearLayout {
}; };
val onVideoClicked = Event2<IPlatformVideo, Long>(); val onVideoClicked = Event2<IPlatformVideo, Long>();
val onLongPress = Event1<IPlatformVideo>();
val onChannelClicked = Event1<PlatformAuthorLink>(); val onChannelClicked = Event1<PlatformAuthorLink>();
val onAddToClicked = Event1<IPlatformVideo>(); val onAddToClicked = Event1<IPlatformVideo>();
val onAddToQueueClicked = Event1<IPlatformVideo>(); val onAddToQueueClicked = Event1<IPlatformVideo>();
@ -119,7 +120,13 @@ open class PreviewVideoView : LinearLayout {
this._exoPlayer = exoPlayer this._exoPlayer = exoPlayer
setOnClickListener { onOpenClicked() }; setOnLongClickListener {
onLongPress()
true
};
setOnClickListener {
onOpenClicked()
};
_imageChannel.setOnClickListener { currentVideo?.let { onChannelClicked.emit(it.author) } }; _imageChannel.setOnClickListener { currentVideo?.let { onChannelClicked.emit(it.author) } };
_textChannelName.setOnClickListener { currentVideo?.let { onChannelClicked.emit(it.author) } }; _textChannelName.setOnClickListener { currentVideo?.let { onChannelClicked.emit(it.author) } };
_textVideoMetadata.setOnClickListener { currentVideo?.let { onChannelClicked.emit(it.author) } }; _textVideoMetadata.setOnClickListener { currentVideo?.let { onChannelClicked.emit(it.author) } };
@ -145,6 +152,12 @@ open class PreviewVideoView : LinearLayout {
} }
} }
protected open fun onLongPress() {
currentVideo?.let {
onLongPress.emit(it);
}
}
open fun bind(content: IPlatformContent) { open fun bind(content: IPlatformContent) {
_taskLoadProfile.cancel(); _taskLoadProfile.cancel();

View file

@ -17,6 +17,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 onLongPress = Event1<IPlatformVideo>();
//val context: Context; //val context: Context;
val currentVideo: IPlatformVideo? get() = view.currentVideo; val currentVideo: IPlatformVideo? get() = view.currentVideo;
@ -30,6 +31,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.onLongPress.subscribe(onLongPress::emit);
} }

View file

@ -6,6 +6,7 @@ import android.animation.ObjectAnimator
import android.content.Context import android.content.Context
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector import android.view.GestureDetector
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
@ -57,8 +58,10 @@ class GestureControlView : LinearLayout {
private var _isFullScreen = false; private var _isFullScreen = false;
private var _animatorBrightness: ObjectAnimator? = null; private var _animatorBrightness: ObjectAnimator? = null;
private val _layoutControlsFullscreen: FrameLayout; private val _layoutControlsFullscreen: FrameLayout;
private var _adjustingFullscreen: Boolean = false; private var _adjustingFullscreenUp: Boolean = false;
private var _fullScreenFactor = 1.0f; private var _adjustingFullscreenDown: Boolean = false;
private var _fullScreenFactorUp = 1.0f;
private var _fullScreenFactorDown = 1.0f;
val onSeek = Event1<Long>(); val onSeek = Event1<Long>();
val onBrightnessAdjusted = Event1<Float>(); val onBrightnessAdjusted = Event1<Float>();
@ -100,10 +103,14 @@ class GestureControlView : LinearLayout {
_soundFactor = (_soundFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f); _soundFactor = (_soundFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
_progressSound.progress = _soundFactor; _progressSound.progress = _soundFactor;
onSoundAdjusted.emit(_soundFactor); onSoundAdjusted.emit(_soundFactor);
} else if (_adjustingFullscreen) { } else if (_adjustingFullscreenUp) {
val adjustAmount = (distanceY * 2) / height; val adjustAmount = (distanceY * 2) / height;
_fullScreenFactor = (_fullScreenFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f); _fullScreenFactorUp = (_fullScreenFactorUp + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
_layoutControlsFullscreen.transitionAlpha = _fullScreenFactor; _layoutControlsFullscreen.alpha = _fullScreenFactorUp;
} else if (_adjustingFullscreenDown) {
val adjustAmount = (-distanceY * 2) / height;
_fullScreenFactorDown = (_fullScreenFactorDown + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
_layoutControlsFullscreen.alpha = _fullScreenFactorDown;
} else { } else {
val rx = p0.x / width; val rx = p0.x / width;
val ry = p0.y / height; val ry = p0.y / height;
@ -114,7 +121,11 @@ class GestureControlView : LinearLayout {
} else if (_isFullScreen && rx > 0.6) { } else if (_isFullScreen && rx > 0.6) {
startAdjustingSound(); startAdjustingSound();
} else if (rx >= 0.4 && rx <= 0.6) { } else if (rx >= 0.4 && rx <= 0.6) {
startAdjustingFullscreen(); if (_isFullScreen) {
startAdjustingFullscreenDown();
} else {
startAdjustingFullscreenUp();
}
} }
} }
} }
@ -180,11 +191,18 @@ class GestureControlView : LinearLayout {
stopAdjustingBrightness(); stopAdjustingBrightness();
} }
if (_adjustingFullscreen && ev.action == MotionEvent.ACTION_UP) { if (_adjustingFullscreenUp && ev.action == MotionEvent.ACTION_UP) {
if (_fullScreenFactor > 0.5) { if (_fullScreenFactorUp > 0.5) {
onToggleFullscreen.emit(); onToggleFullscreen.emit();
} }
stopAdjustingFullscreen(); stopAdjustingFullscreenUp();
}
if (_adjustingFullscreenDown && ev.action == MotionEvent.ACTION_UP) {
if (_fullScreenFactorDown > 0.5) {
onToggleFullscreen.emit();
}
stopAdjustingFullscreenDown();
} }
startHideJobIfNecessary(); startHideJobIfNecessary();
@ -469,15 +487,27 @@ class GestureControlView : LinearLayout {
_animatorSound?.start(); _animatorSound?.start();
} }
private fun startAdjustingFullscreen() { private fun startAdjustingFullscreenUp() {
_adjustingFullscreen = true; _adjustingFullscreenUp = true;
_fullScreenFactor = 0f; _fullScreenFactorUp = 0f;
_layoutControlsFullscreen.transitionAlpha = 0f; _layoutControlsFullscreen.alpha = 0f;
_layoutControlsFullscreen.visibility = View.VISIBLE; _layoutControlsFullscreen.visibility = View.VISIBLE;
} }
private fun stopAdjustingFullscreen() { private fun stopAdjustingFullscreenUp() {
_adjustingFullscreen = false; _adjustingFullscreenUp = false;
_layoutControlsFullscreen.visibility = View.GONE;
}
private fun startAdjustingFullscreenDown() {
_adjustingFullscreenDown = true;
_fullScreenFactorDown = 0f;
_layoutControlsFullscreen.alpha = 0f;
_layoutControlsFullscreen.visibility = View.VISIBLE;
}
private fun stopAdjustingFullscreenDown() {
_adjustingFullscreenDown = false;
_layoutControlsFullscreen.visibility = View.GONE; _layoutControlsFullscreen.visibility = View.GONE;
} }
@ -511,7 +541,7 @@ class GestureControlView : LinearLayout {
//onSoundAdjusted.emit(1.0f); //onSoundAdjusted.emit(1.0f);
stopAdjustingBrightness(); stopAdjustingBrightness();
stopAdjustingSound(); stopAdjustingSound();
stopAdjustingFullscreen(); stopAdjustingFullscreenUp();
} }
_isFullScreen = isFullScreen; _isFullScreen = isFullScreen;

View file

@ -134,6 +134,10 @@ class SlideUpMenuOverlay : RelativeLayout {
} }
fun show(){ fun show(){
if (isVisible) {
return;
}
isVisible = true; isVisible = true;
_container?.post { _container?.post {
_container?.visibility = View.VISIBLE; _container?.visibility = View.VISIBLE;
@ -146,8 +150,8 @@ class SlideUpMenuOverlay : RelativeLayout {
_viewBackground.alpha = 0f; _viewBackground.alpha = 0f;
val animations = arrayListOf<Animator>(); val animations = arrayListOf<Animator>();
animations.add(ObjectAnimator.ofFloat(_viewBackground, "alpha", 0.0f, 1.0f).setDuration(500)); animations.add(ObjectAnimator.ofFloat(_viewBackground, "alpha", 0.0f, 1.0f).setDuration(ANIMATION_DURATION_MS));
animations.add(ObjectAnimator.ofFloat(_viewOverlayContainer, "translationY", _viewOverlayContainer.measuredHeight.toFloat(), 0.0f).setDuration(500)); animations.add(ObjectAnimator.ofFloat(_viewOverlayContainer, "translationY", _viewOverlayContainer.measuredHeight.toFloat(), 0.0f).setDuration(ANIMATION_DURATION_MS));
val animatorSet = AnimatorSet(); val animatorSet = AnimatorSet();
animatorSet.playTogether(animations); animatorSet.playTogether(animations);
@ -159,11 +163,15 @@ class SlideUpMenuOverlay : RelativeLayout {
} }
fun hide(animate: Boolean = true){ fun hide(animate: Boolean = true){
if (!isVisible) {
return
}
isVisible = false; isVisible = false;
if (_animated && animate) { if (_animated && animate) {
val animations = arrayListOf<Animator>(); val animations = arrayListOf<Animator>();
animations.add(ObjectAnimator.ofFloat(_viewBackground, "alpha", 1.0f, 0.0f).setDuration(500)); animations.add(ObjectAnimator.ofFloat(_viewBackground, "alpha", 1.0f, 0.0f).setDuration(ANIMATION_DURATION_MS));
animations.add(ObjectAnimator.ofFloat(_viewOverlayContainer, "translationY", 0.0f, _viewOverlayContainer.measuredHeight.toFloat()).setDuration(500)); animations.add(ObjectAnimator.ofFloat(_viewOverlayContainer, "translationY", 0.0f, _viewOverlayContainer.measuredHeight.toFloat()).setDuration(ANIMATION_DURATION_MS));
val animatorSet = AnimatorSet(); val animatorSet = AnimatorSet();
animatorSet.doOnEnd { animatorSet.doOnEnd {
@ -180,4 +188,8 @@ class SlideUpMenuOverlay : RelativeLayout {
_container?.visibility = View.GONE; _container?.visibility = View.GONE;
} }
} }
companion object {
private const val ANIMATION_DURATION_MS = 350L
}
} }

View file

@ -659,6 +659,8 @@
<string name="stopped_after_requestcount_to_avoid_rate_limit_click_load_more_to_load_more">Stopped after {requestCount} to avoid rate limit, click load more to load more.</string> <string name="stopped_after_requestcount_to_avoid_rate_limit_click_load_more_to_load_more">Stopped after {requestCount} to avoid rate limit, click load more to load more.</string>
<string name="this_creator_has_not_setup_any_monetization_features">This creator has not setup any monetization features</string> <string name="this_creator_has_not_setup_any_monetization_features">This creator has not setup any monetization features</string>
<string name="plus_tax">" + Tax"</string> <string name="plus_tax">" + Tax"</string>
<string name="new_playlist">New playlist</string>
<string name="add_to_new_playlist">Add to new playlist</string>
<string-array name="home_screen_array"> <string-array name="home_screen_array">
<item>Recommendations</item> <item>Recommendations</item>
<item>Subscriptions</item> <item>Subscriptions</item>

View file

@ -25,7 +25,7 @@ cp ./app/build/outputs/apk/stable/release/app-stable-arm64-v8a-release.apk $DOCU
VERSION=$(git describe --tags) VERSION=$(git describe --tags)
echo $VERSION > $DOCUMENT_ROOT/version.txt echo $VERSION > $DOCUMENT_ROOT/version.txt
mkdir -p $DOCUMENT_ROOT/changelogs mkdir -p $DOCUMENT_ROOT/changelogs
git tag -l $VERSION -n1000 | awk '{$1=""; print $0}' | sed -e 's/^[ \t]*//' > $DOCUMENT_ROOT/changelogs/$VERSION git tag -l --format='%(contents)' $VERSION > $DOCUMENT_ROOT/changelogs/$VERSION
# Notify Cloudflare to wipe the CDN cache # Notify Cloudflare to wipe the CDN cache
echo "Purging Cloudflare cache for zone $CLOUDFLARE_ZONE_ID..." echo "Purging Cloudflare cache for zone $CLOUDFLARE_ZONE_ID..."