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

This commit is contained in:
Kelvin 2024-12-10 22:35:46 +01:00
commit 7cdded8fd7
11 changed files with 259 additions and 115 deletions

View file

@ -52,7 +52,7 @@
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:exported="true"
android:theme="@style/Theme.FutoVideo.NoActionBar"
android:launchMode="singleTask"
android:launchMode="singleInstance"
android:resizeableActivity="true"
android:supportsPictureInPicture="true">

View file

@ -6,6 +6,7 @@ import android.graphics.Color
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
@ -57,11 +58,21 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
_editComment = findViewById(R.id.edit_comment);
_textCharacterCount = findViewById(R.id.character_count);
_textCharacterCountMax = findViewById(R.id.character_count_max);
setCanceledOnTouchOutside(false)
setOnKeyListener { _, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
handleCloseAttempt()
true
} else {
false
}
}
_editComment.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) = Unit
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, c: Int) {
val count = s?.length ?: 0;
_textCharacterCount.text = count.toString();
if (count > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
@ -79,10 +90,13 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
_buttonCancel.setOnClickListener {
clearFocus();
dismiss();
handleCloseAttempt()
};
setOnCancelListener {
handleCloseAttempt()
}
_buttonCreate.setOnClickListener {
clearFocus();
@ -134,6 +148,22 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
focus();
}
private fun handleCloseAttempt() {
if (_editComment.text.isEmpty()) {
clearFocus()
dismiss()
} else {
UIDialogs.showConfirmationDialog(
context,
context.resources.getString(R.string.not_empty_close),
action = {
clearFocus()
dismiss()
}
)
}
}
private fun focus() {
_editComment.requestFocus();
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);

View file

@ -8,6 +8,7 @@ import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.Spinner
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager
@ -25,11 +26,20 @@ class CreatorsFragment : MainFragment() {
private var _overlayContainer: FrameLayout? = null;
private var _containerSearch: FrameLayout? = null;
private var _editSearch: EditText? = null;
private var _buttonClearSearch: ImageButton? = null
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_creators, container, false);
_containerSearch = view.findViewById(R.id.container_search);
_editSearch = view.findViewById(R.id.edit_search);
val editSearch: EditText = view.findViewById(R.id.edit_search);
val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search)
_editSearch = editSearch
_buttonClearSearch = buttonClearSearch
buttonClearSearch.setOnClickListener {
editSearch.text.clear()
editSearch.requestFocus()
_buttonClearSearch?.visibility = View.INVISIBLE;
}
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
@ -51,7 +61,12 @@ class CreatorsFragment : MainFragment() {
_spinnerSortBy = spinnerSortBy;
_editSearch?.addTextChangedListener {
adapter.query = it.toString();
adapter.query = it.toString()
if (it?.isEmpty() == true) {
_buttonClearSearch?.visibility = View.INVISIBLE
} else {
_buttonClearSearch?.visibility = View.VISIBLE
}
}
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_subscriptions);

View file

@ -1799,8 +1799,13 @@ class VideoDetailView : ConstraintLayout {
private fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean){
Logger.i(TAG, "onSourceChanged(videoSource=$videoSource, audioSource=$audioSource, resume=$resume)")
if((videoSource == null || videoSource is LocalVideoSource) && (audioSource == null || audioSource is LocalAudioSource))
UIDialogs.toast(context, context.getString(R.string.offline_playback), false);
if((videoSource == null || videoSource is LocalVideoSource) && (audioSource == null || audioSource is LocalAudioSource)) {
Logger.i(TAG, "Time since last offline playback toast: " + (System.currentTimeMillis() - _lastOfflinePlaybackToastTime).toString())
if (System.currentTimeMillis() - _lastOfflinePlaybackToastTime > 5000) {
UIDialogs.toast(context, context.getString(R.string.offline_playback), false);
_lastOfflinePlaybackToastTime = System.currentTimeMillis()
}
}
//If LiveStream, set to end
if(videoSource is IDashManifestSource || videoSource is IHLSManifestSource) {
if (video?.isLive == true) {
@ -2379,8 +2384,8 @@ class VideoDetailView : ConstraintLayout {
}
fun isLandscapeVideo(): Boolean? {
var videoSourceWidth = _player.exoPlayer?.player?.videoSize?.width
var videoSourceHeight = _player.exoPlayer?.player?.videoSize?.height
val videoSourceWidth = _player.exoPlayer?.player?.videoSize?.width
val videoSourceHeight = _player.exoPlayer?.player?.videoSize?.height
return if (videoSourceWidth == null || videoSourceHeight == null || videoSourceWidth == 0 || videoSourceHeight == 0){
null
@ -3030,8 +3035,6 @@ class VideoDetailView : ConstraintLayout {
const val TAG_MORE = "MORE";
private val _buttonPinStore = FragmentedStorage.get<StringArrayStorage>("videoPinnedButtons");
private var _lastOfflinePlaybackToastTime: Long = 0
}
}

View file

@ -12,70 +12,109 @@ import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.timestampRegex
import com.futo.platformplayer.views.behavior.NonScrollingTextView
import com.futo.platformplayer.views.behavior.NonScrollingTextView.Companion
import kotlinx.coroutines.runBlocking
class PlatformLinkMovementMethod : LinkMovementMethod {
private val _context: Context;
class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMethod() {
constructor(context: Context) : super() {
_context = context;
}
private var pressedLinks: Array<URLSpan>? = null
private var linkPressed = false
private var downX = 0f
private var downY = 0f
private val touchSlop = 20
override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean {
val action = event.action;
Logger.i(TAG, "onTouchEvent (action = $action)")
if (action == MotionEvent.ACTION_UP) {
val x = event.x.toInt() - widget.totalPaddingLeft + widget.scrollX;
val y = event.y.toInt() - widget.totalPaddingTop + widget.scrollY;
val action = event.actionMasked
val layout = widget.layout;
val line = layout.getLineForVertical(y);
val off = layout.getOffsetForHorizontal(line, x.toFloat());
val links = buffer.getSpans(off, off, URLSpan::class.java);
when (action) {
MotionEvent.ACTION_DOWN -> {
val links = findLinksAtTouchPosition(widget, buffer, event)
if (links.isNotEmpty()) {
pressedLinks = links
linkPressed = true
downX = event.x
downY = event.y
widget.parent?.requestDisallowInterceptTouchEvent(true)
return true
} else {
linkPressed = false
pressedLinks = null
}
}
if (links.isNotEmpty()) {
runBlocking {
for (link in links) {
Logger.i(TAG) { "Link clicked '${link.url}'." };
MotionEvent.ACTION_MOVE -> {
if (linkPressed) {
val dx = event.x - downX
val dy = event.y - downY
if (Math.abs(dx) > touchSlop || Math.abs(dy) > touchSlop) {
linkPressed = false
pressedLinks = null
widget.parent?.requestDisallowInterceptTouchEvent(false)
return false
}
return true
}
}
if (_context is MainActivity) {
if (_context.handleUrl(link.url)) {
continue;
}
MotionEvent.ACTION_UP -> {
if (linkPressed && pressedLinks != null) {
val dx = event.x - downX
val dy = event.y - downY
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop) {
runBlocking {
for (link in pressedLinks!!) {
Logger.i(TAG) { "Link clicked '${link.url}'." }
if (timestampRegex.matches(link.url)) {
val tokens = link.url.split(':');
if (_context is MainActivity) {
if (_context.handleUrl(link.url)) continue
if (timestampRegex.matches(link.url)) {
val tokens = link.url.split(':')
var time_s = -1L
when (tokens.size) {
2 -> time_s = tokens[0].toLong() * 60 + tokens[1].toLong()
3 -> time_s = tokens[0].toLong() * 3600 +
tokens[1].toLong() * 60 +
tokens[2].toLong()
}
var time_s = -1L;
if (tokens.size == 2) {
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
} else if (tokens.size == 3) {
time_s =
tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
}
if (time_s != -1L) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
continue;
if (time_s != -1L) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000)
continue
}
}
}
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
}
}
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
pressedLinks = null
linkPressed = false
return true
} else {
pressedLinks = null
linkPressed = false
}
}
}
return true;
MotionEvent.ACTION_CANCEL -> {
linkPressed = false
pressedLinks = null
}
}
return super.onTouchEvent(widget, buffer, event);
return super.onTouchEvent(widget, buffer, event)
}
private fun findLinksAtTouchPosition(widget: TextView, buffer: Spannable, event: MotionEvent): Array<URLSpan> {
val x = (event.x - widget.totalPaddingLeft + widget.scrollX).toInt()
val y = (event.y - widget.totalPaddingTop + widget.scrollY).toInt()
val layout = widget.layout ?: return emptyArray()
val line = layout.getLineForVertical(y)
val off = layout.getOffsetForHorizontal(line, x.toFloat())
return buffer.getSpans(off, off, URLSpan::class.java)
}
companion object {
val TAG = "PlatformLinkMovementMethod";
const val TAG = "PlatformLinkMovementMethod"
}
}
}

View file

@ -112,18 +112,23 @@ class StateSync {
Logger.i(TAG, "Sync key pair initialized (public key = ${publicKey})")
_thread = Thread {
val serverSocket = ServerSocket(PORT)
_serverSocket = serverSocket
try {
val serverSocket = ServerSocket(PORT)
_serverSocket = serverSocket
Log.i(TAG, "Running on port ${PORT} (TCP)")
Log.i(TAG, "Running on port ${PORT} (TCP)")
while (_started) {
val socket = serverSocket.accept()
val session = createSocketSession(socket, true) { session, socketSession ->
while (_started) {
val socket = serverSocket.accept()
val session = createSocketSession(socket, true) { session, socketSession ->
}
session.startAsResponder()
}
session.startAsResponder()
} catch (e: Throwable) {
Logger.e(TAG, "Failed to bind server socket to port ${PORT}", e)
UIDialogs.toast("Failed to start sync, port in use")
}
}.apply { start() }

View file

@ -16,73 +16,113 @@ import com.futo.platformplayer.timestampRegex
import kotlinx.coroutines.runBlocking
class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
private var _lastTouchedLinks: Array<URLSpan>? = null
private var downX = 0f
private var downY = 0f
private var linkPressed = false
private val touchSlop = 20
constructor(context: Context) : super(context) {}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}
override fun scrollTo(x: Int, y: Int) {
//do nothing
// do nothing
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
val action = event?.action
Logger.i(TAG, "onTouchEvent (action = $action)");
val action = event?.actionMasked
if (event == null) return super.onTouchEvent(event)
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
val x = event.x.toInt()
val y = event.y.toInt()
when (action) {
MotionEvent.ACTION_DOWN -> {
val x = event.x.toInt()
val y = event.y.toInt()
val layout: Layout? = this.layout
if (layout != null) {
val line = layout.getLineForVertical(y)
val offset = layout.getOffsetForHorizontal(line, x.toFloat())
val text = this.text
if (text is Spannable) {
val layout: Layout? = this.layout
if (layout != null && this.text is Spannable) {
val offset = layout.getOffsetForHorizontal(layout.getLineForVertical(y), x.toFloat())
val text = this.text as Spannable
val links = text.getSpans(offset, offset, URLSpan::class.java)
if (links.isNotEmpty()) {
runBlocking {
for (link in links) {
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." };
val c = context;
if (c is MainActivity) {
if (c.handleUrl(link.url)) {
continue;
}
if (timestampRegex.matches(link.url)) {
val tokens = link.url.split(':');
var time_s = -1L;
if (tokens.size == 2) {
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
} else if (tokens.size == 3) {
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
}
if (time_s != -1L) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
continue;
}
}
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
}
}
}
parent?.requestDisallowInterceptTouchEvent(true)
_lastTouchedLinks = links
downX = event.x
downY = event.y
linkPressed = true
return true
} else {
linkPressed = false
_lastTouchedLinks = null
}
}
}
MotionEvent.ACTION_MOVE -> {
if (linkPressed) {
val dx = event.x - downX
val dy = event.y - downY
if (Math.abs(dx) > touchSlop || Math.abs(dy) > touchSlop) {
linkPressed = false
_lastTouchedLinks = null
parent?.requestDisallowInterceptTouchEvent(false)
return false
}
return true
}
}
MotionEvent.ACTION_UP -> {
if (linkPressed && _lastTouchedLinks != null) {
val dx = event.x - downX
val dy = event.y - downY
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop) {
runBlocking {
for (link in _lastTouchedLinks!!) {
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." }
val c = context
if (c is MainActivity) {
if (c.handleUrl(link.url)) continue
if (timestampRegex.matches(link.url)) {
val tokens = link.url.split(':')
var time_s = -1L
when (tokens.size) {
2 -> time_s = tokens[0].toLong() * 60 + tokens[1].toLong()
3 -> time_s = tokens[0].toLong() * 3600 +
tokens[1].toLong() * 60 +
tokens[2].toLong()
}
if (time_s != -1L) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000)
continue
}
}
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
} else {
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
}
}
}
_lastTouchedLinks = null
linkPressed = false
return true
} else {
linkPressed = false
_lastTouchedLinks = null
}
}
}
MotionEvent.ACTION_CANCEL -> {
linkPressed = false
_lastTouchedLinks = null
}
}
super.onTouchEvent(event)
return false
return super.onTouchEvent(event)
}
companion object {
private const val TAG = "NonScrollingTextView"
}
}
}

View file

@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.comments.LazyComment
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.structures.IAsyncPager
@ -267,9 +268,13 @@ class CommentsList : ConstraintLayout {
}
fun replaceComment(c: PolycentricPlatformComment, newComment: PolycentricPlatformComment) {
val index = _comments.indexOf(c);
_comments[index] = newComment;
_adapterComments.notifyItemChanged(_adapterComments.childToParentPosition(index));
val index = _comments.indexOfFirst { it == c || (it is LazyComment && it.getUnderlyingComment() == c) };
if (index >= 0) {
_comments[index] = newComment;
_adapterComments.notifyItemChanged(_adapterComments.childToParentPosition(index));
} else {
Logger.w(TAG, "Parent comment not found")
}
}
companion object {

View file

@ -592,6 +592,11 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
@OptIn(UnstableApi::class)
fun setFullScreen(fullScreen: Boolean) {
// prevent fullscreen before the video has loaded to make sure we know whether it's a vertical or horizontal video
if(exoPlayer?.player?.videoSize?.height ?: 0 == 0 && fullScreen){
return
}
updateRotateLock()
if (isFullScreen == fullScreen) {

View file

@ -199,6 +199,7 @@
<string name="previous">Previous</string>
<string name="next">Next</string>
<string name="comment">Comment</string>
<string name="not_empty_close">Comment is not empty, close anyway?</string>
<string name="str_import">Import</string>
<string name="my_playlist_name">My Playlist Name</string>
<string name="do_you_want_to_import_this_store">Do you want to import this store?</string>

View file

@ -15,6 +15,7 @@ touch $DOCUMENT_ROOT/maintenance.file
# Swap over the content
echo "Deploying content..."
cp ./app/build/outputs/bundle/playstoreRelease/app-playstore-release.aab $DOCUMENT_ROOT/app-playstore-release.aab
aws s3 cp ./app/build/outputs/bundle/playstoreRelease/app-playstore-release.aab s3://artifacts-grayjay-app/app-playstore-release.aab
# Notify Cloudflare to wipe the CDN cache