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

This commit is contained in:
Kelvin 2024-01-16 16:46:40 +01:00
commit a4ff47d863
19 changed files with 442 additions and 51 deletions

View file

@ -1,6 +1,5 @@
package com.futo.platformplayer
import android.app.Activity
import android.app.NotificationManager
import android.content.ContentResolver
import android.content.Context
@ -68,7 +67,7 @@ class UISlideOverlays {
return menu;
}
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) {
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>();
val originalNotif = subscription.doNotifications;
@ -77,15 +76,13 @@ class UISlideOverlays {
val originalVideo = subscription.doFetchVideos;
val originalPosts = subscription.doFetchPosts;
val menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, listOf());
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
val capabilities = plugin.getChannelCapabilities();
withContext(Dispatchers.Main) {
var menu: SlideUpMenuOverlay? = null;
items.addAll(listOf(
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
@ -119,7 +116,7 @@ class UISlideOverlays {
}, false)*/
).filterNotNull());
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
menu.setItems(items);
if(subscription.doNotifications)
menu.selectOption(null, "notifications", true, true);
@ -174,6 +171,8 @@ class UISlideOverlays {
menu.show();
}
}
return menu;
}
fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {
@ -718,6 +717,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, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
{

View file

@ -810,11 +810,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if(_fragBotBarMenu.onBackPressed())
return;
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED &&
_fragVideoDetail.onBackPressed())
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed())
return;
if(!fragCurrent.onBackPressed())
closeSegment();
}

View file

@ -19,8 +19,9 @@ class PolycentricPlatformComment : IPlatformComment {
val eventPointer: Pointer;
val reference: Reference;
val parentReference: Reference?;
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, replyCount: Int? = null) {
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, parentReference: Reference?, replyCount: Int? = null) {
this.contextUrl = contextUrl;
this.author = author;
this.message = msg;
@ -29,6 +30,7 @@ class PolycentricPlatformComment : IPlatformComment {
this.replyCount = replyCount;
this.eventPointer = eventPointer;
this.reference = eventPointer.toReference();
this.parentReference = parentReference;
}
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
@ -36,10 +38,11 @@ class PolycentricPlatformComment : IPlatformComment {
}
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, replyCount);
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, parentReference, replyCount);
}
companion object {
private const val TAG = "PolycentricPlatformComment"
val MAX_COMMENT_SIZE = 2000
}
}

View file

@ -123,7 +123,8 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
msg = comment,
rating = RatingLikeDislikes(0, 0),
date = OffsetDateTime.now(),
eventPointer = eventPointer
eventPointer = eventPointer,
parentReference = ref
));
dismiss();

View file

@ -117,6 +117,7 @@ class CommentsFragment : MainFragment() {
val holder = CommentWithReferenceViewHolder(viewGroup, _cache);
holder.onDelete.subscribe(::onDelete);
holder.onRepliesClick.subscribe(::onRepliesClick);
holder.onClick.subscribe(::onClick);
return@InsertedViewAdapterWithLoader holder;
}
);
@ -200,6 +201,17 @@ class CommentsFragment : MainFragment() {
return false
}
private fun onClick(c: IPlatformComment) {
if (c !is PolycentricPlatformComment) {
return
}
val parentRef = c.parentReference
if (parentRef != null && _repliesOverlay.handleParentClick(c.contextUrl, parentRef)) {
setRepliesOverlayVisible(true, true)
}
}
private fun onRepliesClick(c: IPlatformComment) {
val replyCount = c.replyCount ?: 0;
var metadata = "";

View file

@ -169,14 +169,14 @@ class VideoDetailFragment : MainFragment {
_view!!.transitionToStart();
}
fun maximizeVideoDetail(instant: Boolean = false) {
if(_maximizeProgress > 0.9f && state != State.MAXIMIZED) {
if((_maximizeProgress > 0.9f || instant) && state != State.MAXIMIZED) {
state = State.MAXIMIZED;
onMaximized.emit();
}
_view?.let {
if(!instant)
if(!instant) {
it.transitionToEnd();
else {
} else {
it.progress = 1f;
onTransitioning.emit(true);
}

View file

@ -373,7 +373,7 @@ class VideoDetailView : ConstraintLayout {
_buttonSubscribe.onSubscribed.subscribe {
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
};
_container_content_liveChat.onRaidNow.subscribe {
@ -2359,7 +2359,7 @@ class VideoDetailView : ConstraintLayout {
}
else if(isOverlayed) {
_playerProgress.layoutParams = _playerProgress.layoutParams.apply {
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -6f, resources.displayMetrics).toInt();
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -2f, resources.displayMetrics).toInt();
};
_playerProgress.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
}

View file

@ -48,6 +48,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import userpackage.Protocol
import userpackage.Protocol.Reference
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
@ -287,7 +288,8 @@ class StatePolycentric {
rating = RatingLikeDislikes(0, 0),
date = if (ev.unixMilliseconds != null) Instant.ofEpochMilli(ev.unixMilliseconds!!).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
replyCount = 0,
eventPointer = se.toPointer()
eventPointer = se.toPointer(),
parentReference = se.event.references.getOrNull(0)
))
}
@ -328,6 +330,77 @@ class StatePolycentric {
return LikesDislikesReplies(likes, dislikes, replyCount)
}
suspend fun getComment(contextUrl: String, reference: Reference): PolycentricPlatformComment {
ensureEnabled()
if (reference.referenceType != 2L) {
throw Exception("Not a pointer")
}
val pointer = Protocol.Pointer.parseFrom(reference.reference)
val events = ApiMethods.getEvents(PolycentricCache.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder()
.addRangesForProcesses(Protocol.RangesForProcess.newBuilder()
.setProcess(pointer.process)
.addRanges(Protocol.Range.newBuilder()
.setLow(pointer.logicalClock)
.setHigh(pointer.logicalClock)
.build())
.build())
.build())
val sev = SignedEvent.fromProto(events.getEvents(0))
val ev = sev.event
if (ev.contentType != ContentType.POST.value) {
throw Exception("This is not a comment")
}
val post = Protocol.Post.parseFrom(ev.content);
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
val dp_25 = 25.dp(StateApp.instance.context.resources)
val profileEvents = ApiMethods.getQueryLatest(
PolycentricCache.SERVER,
ev.system.toProto(),
listOf(
ContentType.AVATAR.value,
ContentType.USERNAME.value
)
).eventsList.map { e -> SignedEvent.fromProto(e) }.groupBy { e -> e.event.contentType }
.map { (_, events) -> events.maxBy { x -> x.event.unixMilliseconds ?: 0 } };
val nameEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.USERNAME.value };
val avatarEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.AVATAR.value };
val imageBundle = if (avatarEvent != null) {
val lwwElementValue = avatarEvent.event.lwwElement?.value;
if (lwwElementValue != null) {
Protocol.ImageBundle.parseFrom(lwwElementValue)
} else {
null
}
} else {
null
}
val ldr = getLikesDislikesReplies(reference)
return PolycentricPlatformComment(
contextUrl = contextUrl,
author = PlatformAuthorLink(
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
url = systemLinkUrl,
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
subscribers = null
),
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
rating = RatingLikeDislikes(ldr.likes, ldr.dislikes),
date = if (ev.unixMilliseconds != null) Instant.ofEpochMilli(ev.unixMilliseconds!!).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
replyCount = ldr.replyCount.toInt(),
eventPointer = sev.toPointer(),
parentReference = sev.event.references.getOrNull(0)
)
}
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
if (!enabled) {
return EmptyPager()
@ -453,7 +526,8 @@ class StatePolycentric {
rating = RatingLikeDislikes(likes, dislikes),
date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
replyCount = replies.toInt(),
eventPointer = sev.toPointer()
eventPointer = sev.toPointer(),
parentReference = sev.event.references.getOrNull(0)
);
} catch (e: Throwable) {
return@mapNotNull null;

View file

@ -55,6 +55,7 @@ class CommentWithReferenceViewHolder : ViewHolder {
var onRepliesClick = Event1<IPlatformComment>();
var onDelete = Event1<IPlatformComment>();
var onClick = Event1<IPlatformComment>();
var comment: IPlatformComment? = null
private set;
@ -108,6 +109,11 @@ class CommentWithReferenceViewHolder : ViewHolder {
onDelete.emit(c);
}
_layoutComment.setOnClickListener {
val c = comment ?: return@setOnClickListener;
onClick.emit(c);
}
_textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context);
}

View file

@ -85,6 +85,11 @@ class GestureControlView : LinearLayout {
private val _layoutControlsZoom: FrameLayout
private val _textZoom: TextView
private var _isZooming = false
private var _isPanning = false
private var _isZoomPanEnabled = false
private var _surfaceView: View? = null
private var _layoutIndicatorFill: FrameLayout;
private var _layoutIndicatorFit: FrameLayout;
private val _gestureController: GestureDetectorCompat;
@ -113,20 +118,16 @@ class GestureControlView : LinearLayout {
_textZoom = findViewById(R.id.text_zoom)
_progressBrightness = findViewById(R.id.progress_brightness);
_layoutControlsFullscreen = findViewById(R.id.layout_controls_fullscreen);
_layoutIndicatorFill = findViewById(R.id.layout_indicator_fill);
_layoutIndicatorFit = findViewById(R.id.layout_indicator_fit);
_scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
if (!_isFullScreen || !Settings.instance.gestureControls.zoom) {
if (!_isZoomPanEnabled || !_isFullScreen || !Settings.instance.gestureControls.zoom) {
return false
}
var newScaleFactor = (_scaleFactor * detector.scaleFactor).coerceAtLeast(1.0f).coerceAtMost(5.0f)
//Make original zoom sticky
if (newScaleFactor - 1.0f < 0.01f) {
newScaleFactor = 1.0f
}
val newScaleFactor = (_scaleFactor * detector.scaleFactor).coerceAtLeast(1.0f).coerceAtMost(10.0f)
val scaleFactorChange = newScaleFactor / _scaleFactor
_scaleFactor = newScaleFactor
onZoom.emit(_scaleFactor)
@ -149,6 +150,9 @@ class GestureControlView : LinearLayout {
_layoutControlsZoom.visibility = View.VISIBLE
_textZoom.text = "${String.format("%.1f", _scaleFactor)}x"
_isZooming = true
updateSnappingVisibility()
return true
}
})
@ -164,7 +168,7 @@ class GestureControlView : LinearLayout {
Logger.i(TAG, "p0.pointerCount: " + p0.pointerCount)
if (p1.pointerCount == 1) {
if (!_isPanning && p1.pointerCount == 1) {
val minDistance = Math.min(width, height)
if (_isFullScreen && _adjustingBrightness) {
val adjustAmount = (distanceY * 2) / minDistance;
@ -201,8 +205,10 @@ class GestureControlView : LinearLayout {
}
}
}
} else if (_isFullScreen && !_isZooming && Settings.instance.gestureControls.pan) {
} else if (_isZoomPanEnabled && _isFullScreen && !_isZooming && Settings.instance.gestureControls.pan) {
_isPanning = true
stopAllGestures()
updateSnappingVisibility()
pan(_translationX - distanceX, _translationY - distanceY)
}
@ -244,6 +250,39 @@ class GestureControlView : LinearLayout {
isClickable = true
}
fun updateSnappingVisibility() {
if (willSnapFill()) {
_layoutIndicatorFill.visibility = View.VISIBLE
_layoutIndicatorFit.visibility = View.GONE
} else if (willSnapFit()) {
_layoutIndicatorFill.visibility = View.GONE
_layoutIndicatorFit.visibility = View.VISIBLE
_surfaceView?.let {
val lp = _layoutIndicatorFit.layoutParams
lp.width = it.width
lp.height = it.height
_layoutIndicatorFit.layoutParams = lp
}
} else {
_layoutIndicatorFill.visibility = View.GONE
_layoutIndicatorFit.visibility = View.GONE
}
}
fun setZoomPanEnabled(view: View) {
_isZoomPanEnabled = true
_surfaceView = view
}
fun resetZoomPan() {
_scaleFactor = 1.0f
onZoom.emit(_scaleFactor)
_translationX = 0f
_translationY = 0f
onPan.emit(_translationX, _translationY)
}
private fun pan(translationX: Float, translationY: Float) {
val xc = width / 2.0f
val yc = height / 2.0f
@ -305,9 +344,29 @@ class GestureControlView : LinearLayout {
stopAdjustingFullscreenDown();
}
if (_isZooming && ev.action == MotionEvent.ACTION_UP) {
if ((_isPanning || _isZooming) && ev.action == MotionEvent.ACTION_UP) {
val surfaceView = _surfaceView
if (surfaceView != null && willSnapFill()) {
_scaleFactor = calculateZoomScaleFactor()
onZoom.emit(_scaleFactor)
_translationX = 0f
_translationY = 0f
onPan.emit(_translationX, _translationY)
} else if (willSnapFit()) {
_scaleFactor = 1f
onZoom.emit(_scaleFactor)
_translationX = 0f
_translationY = 0f
onPan.emit(_translationX, _translationY)
}
_layoutControlsZoom.visibility = View.GONE
_layoutIndicatorFill.visibility = View.GONE
_layoutIndicatorFit.visibility = View.GONE
_isZooming = false
_isPanning = false
}
startHideJobIfNecessary();
@ -317,6 +376,35 @@ class GestureControlView : LinearLayout {
return true;
}
private fun calculateZoomScaleFactor(): Float {
val w = _surfaceView?.width?.toFloat() ?: return 1.0f;
val h = _surfaceView?.height?.toFloat() ?: return 1.0f;
if (w == 0.0f || h == 0.0f) {
return 1.0f;
}
return Math.max(width / w, height / h)
}
private val _snapTranslationTolerance = 0.04f;
private val _snapZoomTolerance = 0.04f;
private fun willSnapFill(): Boolean {
val surfaceView = _surfaceView
if (surfaceView != null) {
val zoomScaleFactor = calculateZoomScaleFactor()
if (Math.abs(_scaleFactor - zoomScaleFactor) < _snapZoomTolerance && Math.abs(_translationX) / width < _snapTranslationTolerance && Math.abs(_translationY) / height < _snapTranslationTolerance) {
return true
}
}
return false
}
private fun willSnapFit(): Boolean {
return Math.abs(_scaleFactor - 1.0f) < _snapZoomTolerance && Math.abs(_translationX) / width < _snapTranslationTolerance && Math.abs(_translationY) / height < _snapTranslationTolerance
}
fun cancelHideJob() {
_jobHideControls?.cancel();
_jobHideControls = null;
@ -646,11 +734,7 @@ class GestureControlView : LinearLayout {
}
fun setFullscreen(isFullScreen: Boolean) {
_scaleFactor = 1.0f
onZoom.emit(_scaleFactor)
_translationX = 0f
_translationY = 0f
onPan.emit(_translationX, _translationY)
resetZoomPan()
if (isFullScreen) {
val c = context

View file

@ -1,6 +1,7 @@
package com.futo.platformplayer.views.overlays
import android.content.Context
import android.net.Uri
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
@ -8,11 +9,15 @@ import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fixHtmlLinks
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.toHumanNowDiffString
@ -20,6 +25,13 @@ import com.futo.platformplayer.views.behavior.NonScrollingTextView
import com.futo.platformplayer.views.comments.AddCommentView
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.segments.CommentsList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import userpackage.Protocol
class RepliesOverlay : LinearLayout {
@ -34,7 +46,11 @@ class RepliesOverlay : LinearLayout {
private val _creatorThumbnail: CreatorThumbnail;
private val _layoutParentComment: ConstraintLayout;
private var _readonly = false;
private var _loading = true;
private var _parentComment: IPlatformComment? = null;
private var _onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null;
private val _loaderOverlay: LoaderOverlay
private val _client = ManagedHttpClient()
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.overlay_replies, this)
@ -46,6 +62,8 @@ class RepliesOverlay : LinearLayout {
_textAuthor = findViewById(R.id.text_author)
_creatorThumbnail = findViewById(R.id.image_thumbnail)
_layoutParentComment = findViewById(R.id.layout_parent_comment)
_loaderOverlay = findViewById(R.id.loader_overlay)
setLoading(false);
_addCommentView.onCommentAdded.subscribe {
_commentsList.addComment(it);
@ -72,11 +90,21 @@ class RepliesOverlay : LinearLayout {
}
};
_layoutParentComment.setOnClickListener {
val p = _parentComment
if (p !is PolycentricPlatformComment) {
return@setOnClickListener
}
val ref = p.parentReference ?: return@setOnClickListener
handleParentClick(p.contextUrl, ref)
}
_topbar.onClose.subscribe(this, onClose::emit);
_topbar.setInfo(context.getString(R.string.Replies), "");
}
fun load(readonly: Boolean, metadata: String, contextUrl: String?, ref: Protocol.Reference?, parentComment: IPlatformComment? = null, loader: suspend () -> IPager<IPlatformComment>, onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null) {
fun load(readonly: Boolean, metadata: String, contextUrl: String?, ref: Protocol.Reference?, parentComment: IPlatformComment? = null, loader: suspend () -> IPager<IPlatformComment>, onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null, onParentClick: ((comment: IPlatformComment) -> Unit)? = null) {
_readonly = readonly;
if (readonly) {
_addCommentView.visibility = View.GONE;
@ -109,6 +137,136 @@ class RepliesOverlay : LinearLayout {
_topbar.setInfo(context.getString(R.string.Replies), metadata);
_commentsList.load(readonly, loader);
_onCommentAdded = onCommentAdded;
_parentComment = parentComment;
}
fun handleParentClick(contextUrl: String, ref: Protocol.Reference): Boolean {
val ctx = context
if (ctx !is MainActivity) {
return false
}
return when (ref.referenceType) {
2L -> {
setLoading(true)
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try {
val parentComment = StatePolycentric.instance.getComment(contextUrl, ref)
val replyCount = parentComment.replyCount ?: 0;
var metadata = "";
if (replyCount > 0) {
metadata += "$replyCount " + context.getString(R.string.replies);
}
withContext(Dispatchers.Main) {
setLoading(false)
load(false, metadata, parentComment.contextUrl, parentComment.reference, parentComment,
{ StatePolycentric.instance.getCommentPager(contextUrl, ref) })
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
setLoading(false)
}
Logger.e(TAG, "Failed to load parent comment.", e)
UIDialogs.toast("Failed to load comment")
}
}
true
}
3L -> {
StateApp.instance.scopeOrNull?.launch {
try {
val url = referenceToUrl(_client, ref) ?: return@launch
withContext(Dispatchers.Main) {
ctx.handleUrl(url)
onClose.emit()
}
} catch (e: Throwable) {
Logger.i(TAG, "Failed to open ref.", e)
}
}
false
}
else -> false
}
}
private fun referenceToUrl(client: ManagedHttpClient, parentRef: Protocol.Reference): String? {
val refBytes = parentRef.reference?.toByteArray() ?: return null
val ref = refBytes.decodeToString()
try {
Uri.parse(ref)
return ref
} catch (e: Throwable) {
try {
return oldReferenceToUrl(client, ref)
} catch (f: Throwable) {
Logger.i(TAG, "Failed to handle URL.", f)
}
}
return null
}
private fun oldReferenceToUrl(client: ManagedHttpClient, reference: String): String? {
return when {
reference.startsWith("video_episode:") -> {
val response = client.get("https://content.api.nebula.app/video_episodes/$reference")
if (!response.isOk) {
throw Exception("Failed to resolve nebula video (${response.code}).")
}
val respString = response.body?.string()
val jsonElement = respString?.let { Json.parseToJsonElement(it) }
return jsonElement?.jsonObject?.get("share_url")?.jsonPrimitive?.content
}
reference.length == 11 -> "https://www.youtube.com/watch?v=$reference"
reference.length == 40 -> {
val response = client.post("https://api.na-backend.odysee.com/api/v1/proxy?m=claim_search", hashMapOf(
"Content-Type" to "application/json"
))
if (!response.isOk) {
throw Exception("Failed to resolve claim (${response.code}).")
}
val jsonElement = response.body?.string()?.let { Json.parseToJsonElement(it) }
val canonicalUrl = jsonElement?.jsonObject?.get("result")
?.jsonObject?.get("items")
?.jsonArray?.get(0)
?.jsonObject?.get("canonical_url")
?.jsonPrimitive?.content
canonicalUrl ?: throw Exception("Failed to get canonical URL.")
}
reference.startsWith("v") && (reference.length == 7 || reference.length == 6) -> "https://rumble.com/$reference"
Regex("^\\d+\$").matches(reference) -> "https://www.twitch.tv/videos/$reference"
else -> null
}
}
private fun setLoading(loading: Boolean) {
if (_loading == loading) {
return;
}
_loading = loading;
if (!loading) {
_loaderOverlay.hide()
} else {
_loaderOverlay.show()
}
}
fun cleanup() {
@ -116,4 +274,8 @@ class RepliesOverlay : LinearLayout {
_onCommentAdded = null;
_commentsList.cancel();
}
companion object {
private const val TAG = "RepliesOverlay"
}
}

View file

@ -4,7 +4,6 @@ import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.Gravity
import android.view.KeyCharacterMap.UnavailableException
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
@ -12,10 +11,8 @@ import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.R
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
@ -25,6 +22,8 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.adapters.CommentViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
@ -87,8 +86,6 @@ class CommentsList : ConstraintLayout {
var onRepliesClick = Event1<IPlatformComment>();
var onCommentsLoaded = Event1<Int>();
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
LayoutInflater.from(context).inflate(R.layout.view_comments_list, this, true);

View file

@ -272,6 +272,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_videoView.scaleY = it
}
gestureControl.setZoomPanEnabled(_videoView.videoSurfaceView!!)
if(!isInEditMode) {
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
val player = StatePlayer.instance.getPlayerOrCreate(context);
@ -600,6 +602,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
override fun onVideoSizeChanged(videoSize: VideoSize) {
gestureControl.resetZoomPan()
_lastSourceFit = null;
if(isFullScreen)
fillHeight();
@ -763,4 +766,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
fun setGestureSoundFactor(soundFactor: Float) {
gestureControl.setSoundFactor(soundFactor);
}
override fun onSurfaceSizeChanged(width: Int, height: Int) {
gestureControl.resetZoomPan()
}
}

View file

@ -104,6 +104,11 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
super.onPlaybackSuppressionReasonChanged(playbackSuppressionReason)
}
override fun onSurfaceSizeChanged(width: Int, height: Int) {
super.onSurfaceSizeChanged(width, height)
this@FutoVideoPlayerBase.onSurfaceSizeChanged(width, height);
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying);
this@FutoVideoPlayerBase.onIsPlayingChanged(isPlaying);
@ -592,6 +597,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
exoPlayer?.setVolume(volume);
}
protected open fun onSurfaceSizeChanged(width: Int, height: Int) {
}
@Suppress("DEPRECATION")
protected open fun onPlayerError(error: PlaybackException) {
Logger.i(TAG, "onPlayerError error=$error error.errorCode=${error.errorCode} connectivityLoss");

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:color="#992D63ED" android:width="5dp" />
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:id="@+id/layout_header"
@ -65,7 +65,8 @@
android:id="@+id/replies_overlay"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:clickable="true" />
<LinearLayout android:id="@+id/layout_not_logged_in"
android:layout_width="match_parent"

View file

@ -8,7 +8,8 @@
android:orientation="vertical"
android:id="@+id/container"
android:background="#77000000"
android:elevation="4dp">
android:elevation="4dp"
android:clickable="true">
<ImageView
android:id="@+id/loader"
android:layout_width="80dp"

View file

@ -50,7 +50,7 @@
android:textSize="14sp"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintTop_toTopOf="@id/image_thumbnail"
android:text="ShortCircuit" />
tools:text="ShortCircuit" />
<TextView
android:id="@+id/text_metadata"
@ -66,7 +66,7 @@
app:layout_constraintLeft_toRightOf="@id/text_author"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/text_author"
android:text=" • 3 years ago" />
tools:text=" • 3 years ago" />
<com.futo.platformplayer.views.behavior.NonScrollingTextView
android:id="@+id/text_body"
@ -84,7 +84,7 @@
app:layout_constraintTop_toBottomOf="@id/text_metadata"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintRight_toRightOf="parent"
android:text="@string/lorem_ipsum" />
tools:text="@string/lorem_ipsum" />
</androidx.constraintlayout.widget.ConstraintLayout>
@ -107,4 +107,11 @@
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="12dp" />
<com.futo.platformplayer.views.overlays.LoaderOverlay
android:id="@+id/loader_overlay"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -177,4 +177,22 @@
android:textSize="16dp"/>
</FrameLayout>
<FrameLayout
android:id="@+id/layout_indicator_fill"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_primary_border"
android:visibility="gone" />
<FrameLayout
android:id="@+id/layout_indicator_fit"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@drawable/background_primary_border"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>