mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +00:00
Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay
This commit is contained in:
commit
a4ff47d863
19 changed files with 442 additions and 51 deletions
|
@ -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), "",
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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 = "";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
5
app/src/main/res/drawable/background_primary_border.xml
Normal file
5
app/src/main/res/drawable/background_primary_border.xml
Normal 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>
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
Loading…
Add table
Reference in a new issue