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.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import com.futo.platformplayer.api.http.ManagedHttpClient
@ -369,6 +371,33 @@ class UISlideOverlays {
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 {
val items = arrayListOf<View>();
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
@ -407,6 +436,13 @@ class UISlideOverlays {
));
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) {
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();
if(contentType != null)
headers["Content-Type"] = contentType;
headers["Content-Length"] = content.length.toString();
httpContext.respondCode(200, headers, content);
}

View file

@ -1,14 +1,16 @@
package com.futo.platformplayer.api.http.server.handlers
import com.futo.platformplayer.api.http.server.HttpContext
import com.futo.platformplayer.api.http.server.HttpHeaders
import com.futo.platformplayer.logging.Logger
import java.io.ByteArrayOutputStream
import java.io.File
import java.nio.file.Files
import java.text.SimpleDateFormat
import java.util.*
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) {
val requestHeaders = httpContext.headers;
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("\"", "\\\"")}\""
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"]
var start: Long
val start: Long
val end: Long
if (range != null && range.startsWith("bytes=")) {
val parts = range.substring(6).split("-")
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()}"
} else {
start = 0
@ -51,18 +47,19 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
var totalBytesSent = 0
val contentLength = end - start + 1
Logger.i(TAG, "Sending $contentLength bytes (start: $start, end: $end, shouldGzip: $shouldGzip)")
responseHeaders["Content-Length"] = contentLength.toString()
Logger.i(TAG, "Sending $contentLength bytes (start: $start, end: $end)")
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 {
val buffer = ByteArray(8192)
inputStream.skip(start)
var current = start
val outputStream = if (shouldGzip) GZIPOutputStream(responseStream) else responseStream
val outputStream = responseStream
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);
if (bytesRead < 0) {
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)
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()
if (start >= end) {
current += bytesToSend.toLong()
if (current >= end) {
Logger.i(TAG, "Expected amount of bytes sent")
break
}
}
Logger.i(TAG, "Finished sending file (segment)")
if (shouldGzip) (outputStream as GZIPOutputStream).finish()
outputStream.flush()
} catch (e: Exception) {
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.HttpHeaders
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) {
var content: String? = null;
@ -34,8 +35,8 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
proxyHeaders.put("Referer", targetUrl);
val useMethod = if (method == "inherit") context.method else method;
//Logger.i(TAG, "Proxied Request ${useMethod}: ${targetUrl}");
//Logger.i(TAG, "Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
Logger.i(TAG, "Proxied Request ${useMethod}: ${targetUrl}");
Logger.i(TAG, "Headers:" + proxyHeaders.map { "${it.key}: ${it.value}" }.joinToString("\n"));
val resp = when (useMethod) {
"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);
};
//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()) });
for(newHeader in headers)
headersFiltered.put(newHeader.key, newHeader.value);
@ -92,4 +93,8 @@ class HttpProxyHandler(method: String, path: String, val targetUrl: String): Htt
_ignoreRequestHeaders.add("referer");
return this;
}
companion object {
private const val TAG = "HttpProxyHandler"
}
}

View file

@ -486,7 +486,7 @@ class StateCasting {
}
if (subtitleSource != null) {
_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
).withTag("cast");
_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.PreviewVideoViewHolder
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import kotlin.math.floor
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;
override val visibleThreshold: Int get() = if (feedStyle == FeedStyle.PREVIEW) { 5 } else { 10 };
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) {
@ -70,26 +72,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
adapter.onChannelClicked.subscribe(this) { fragment.navigate<ChannelFragment>(it) };
adapter.onAddToClicked.subscribe(this) { content ->
//TODO: Reconstruct search video from detail if search is null
_overlayContainer.let {
if(content is IPlatformVideo)
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);
})
);
if(content is IPlatformVideo) {
showVideoOptionsOverlay(content)
}
};
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);
}
};
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() {
@ -108,6 +136,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
adapter.onChannelClicked.remove(this);
adapter.onAddToClicked.remove(this);
adapter.onAddToQueueClicked.remove(this);
adapter.onLongPress.remove(this);
}
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) {
if(content is IPlatformVideo) {
StatePlayer.instance.clearQueue();
if (Settings.instance.playback.shouldResumePreview(time))
fragment.navigate<VideoDetailFragment>(content.withTimestamp(time)).maximizeVideoDetail();
else
fragment.navigate<VideoDetailFragment>(content).maximizeVideoDetail();
if (StatePlayer.instance.hasQueue) {
StatePlayer.instance.addToQueue(content)
} else {
if (Settings.instance.playback.shouldResumePreview(time))
fragment.navigate<VideoDetailFragment>(content.withTimestamp(time)).maximizeVideoDetail();
else
fragment.navigate<VideoDetailFragment>(content).maximizeVideoDetail();
}
} else if (content is IPlatformPlaylist) {
fragment.navigate<PlaylistFragment>(content);
} else if (content is IPlatformPost) {

View file

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

View file

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

View file

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

View file

@ -17,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.R
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.assume
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
@ -54,6 +55,14 @@ class PlaylistsFragment : MainFragment() {
_view?.onShown(parameter, isBack);
}
override fun onBackPressed(): Boolean {
if (_view?.onBackPressed() == true) {
return true;
}
return super.onBackPressed()
}
@SuppressLint("ViewConstructor")
class PlaylistsView : LinearLayout {
private val _fragment: PlaylistsFragment;
@ -64,6 +73,7 @@ class PlaylistsFragment : MainFragment() {
private var _adapterWatchLater: VideoListHorizontalAdapter;
private var _adapterPlaylist: PlaylistsAdapter;
private var _layoutWatchlist: ConstraintLayout;
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
constructor(fragment: PlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) {
_fragment = fragment;
@ -92,41 +102,24 @@ class PlaylistsFragment : MainFragment() {
recyclerPlaylists.adapter = _adapterPlaylist;
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.onPlay.subscribe { p ->
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);
_layoutWatchlist = findViewById(R.id.layout_watchlist);
@ -142,12 +135,28 @@ class PlaylistsFragment : MainFragment() {
fun onShown(parameter: Any?, isBack: Boolean) {
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();
updateWatchLater();
}
fun onBackPressed(): Boolean {
val slideUpOverlay = _slideUpOverlay;
if (slideUpOverlay != null) {
if (slideUpOverlay.isVisible) {
slideUpOverlay.hide();
return true;
}
return false;
}
return false;
}
private fun updateWatchLater() {
val watchList = StatePlaylists.instance.getWatchLater();
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) {
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.subscriptions.previewFeedItems);
}

View file

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

View file

@ -32,6 +32,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
val onChannelClicked = Event1<PlatformAuthorLink>();
val onAddToClicked = Event1<IPlatformContent>();
val onAddToQueueClicked = Event1<IPlatformContent>();
val onLongPress = Event1<IPlatformContent>();
private var _taskLoadContent = TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>(
StateApp.instance.scopeGetter, { (viewHolder, video) ->
@ -93,6 +94,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit);
this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::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 {
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 onLongPress = Event1<IPlatformVideo>();
val onChannelClicked = Event1<PlatformAuthorLink>();
val onAddToClicked = Event1<IPlatformVideo>();
val onAddToQueueClicked = Event1<IPlatformVideo>();
@ -119,7 +120,13 @@ open class PreviewVideoView : LinearLayout {
this._exoPlayer = exoPlayer
setOnClickListener { onOpenClicked() };
setOnLongClickListener {
onLongPress()
true
};
setOnClickListener {
onOpenClicked()
};
_imageChannel.setOnClickListener { currentVideo?.let { onChannelClicked.emit(it.author) } };
_textChannelName.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) {
_taskLoadProfile.cancel();

View file

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

View file

@ -134,6 +134,10 @@ class SlideUpMenuOverlay : RelativeLayout {
}
fun show(){
if (isVisible) {
return;
}
isVisible = true;
_container?.post {
_container?.visibility = View.VISIBLE;
@ -146,8 +150,8 @@ class SlideUpMenuOverlay : RelativeLayout {
_viewBackground.alpha = 0f;
val animations = arrayListOf<Animator>();
animations.add(ObjectAnimator.ofFloat(_viewBackground, "alpha", 0.0f, 1.0f).setDuration(500));
animations.add(ObjectAnimator.ofFloat(_viewOverlayContainer, "translationY", _viewOverlayContainer.measuredHeight.toFloat(), 0.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(ANIMATION_DURATION_MS));
val animatorSet = AnimatorSet();
animatorSet.playTogether(animations);
@ -159,11 +163,15 @@ class SlideUpMenuOverlay : RelativeLayout {
}
fun hide(animate: Boolean = true){
if (!isVisible) {
return
}
isVisible = false;
if (_animated && animate) {
val animations = arrayListOf<Animator>();
animations.add(ObjectAnimator.ofFloat(_viewBackground, "alpha", 1.0f, 0.0f).setDuration(500));
animations.add(ObjectAnimator.ofFloat(_viewOverlayContainer, "translationY", 0.0f, _viewOverlayContainer.measuredHeight.toFloat()).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(ANIMATION_DURATION_MS));
val animatorSet = AnimatorSet();
animatorSet.doOnEnd {
@ -180,4 +188,8 @@ class SlideUpMenuOverlay : RelativeLayout {
_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="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="new_playlist">New playlist</string>
<string name="add_to_new_playlist">Add to new playlist</string>
<string-array name="home_screen_array">
<item>Recommendations</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)
echo $VERSION > $DOCUMENT_ROOT/version.txt
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
echo "Purging Cloudflare cache for zone $CLOUDFLARE_ZONE_ID..."