diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index 01f2f48b12fe2868525b280e6b2b5a1bd982f701..01ef456a5119b136f97966253f47a11a3f6c0dd2 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -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), "", { diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index c37f882a787e45fde140d6065a73e0fb4d0d1218..b1f74e13a5d6991e5f2a5d6182940aa369089584 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -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(); } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/comments/PolycentricPlatformComment.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/comments/PolycentricPlatformComment.kt index 90a65e00094ec12acf9ebedfe7d19e8e6fa8d155..c45bdfebd981f83e4aad37c68e1343f269338f41 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/comments/PolycentricPlatformComment.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/comments/PolycentricPlatformComment.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt index f369f4814b25d26600b4f77d98e7333e833825ed..580bd5085c3cec5a5cf97fa3a88442f472de6779 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt @@ -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(); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt index c81ac8deafc16a83c57487722cd104034d15d07f..b2f548b7575ddfcf1655f4e4ab7b525709f4eef4 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt @@ -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 = ""; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt index 487832333ec0270d9e2a25cbce0587fbba056591..ddb21815b60003af498106164127caa9cb193f65 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt @@ -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); } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 82d644b5778e1753249a04bce5c5c273a4397670..78f14b56b0a4e73937a0e35e45576128175f9c0f 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -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); } diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt b/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt index 984c6b4f2f4583e512a7a6a5eea393640b3efdb7..b81b08c0ffc6e30d863af9784b59fc99957f1692 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt @@ -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; diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt index f1fc07dac3eabf6b255f398dad1dbe59171c921b..6b40bbb22098578893fbdf6017b05e5df2e94557 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt @@ -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); } diff --git a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt index 109538339a37f2c939acd9d9093345981d8b5746..7f7d2429953096c7905d0c9eb78b3f33d51be06a 100644 --- a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt @@ -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 diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/RepliesOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/RepliesOverlay.kt index 4d2a7fb9810804ddfc43178722b226cc9983c7c3..19c5dd226dc898209909f25ce4e926b009a279ed 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/RepliesOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/RepliesOverlay.kt @@ -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" + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt b/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt index e02205b7e4dcbfd5381c4d1f63f07d8c830b4ad4..57732acb5c0f8e926f97aded71619430a63d5154 100644 --- a/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt +++ b/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt @@ -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); diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt index a84dcef69f7bf01091839169f30b3df3ce6be2e4..df3a24fb7657a485b29d75256b329457a5614719 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt @@ -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() + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt index d60a3f8e6ab0f4153ad60bbcd13ff54a3a6d8517..3179e2829de0d0ba5ba32fe13e14f4d037aaad53 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt @@ -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"); diff --git a/app/src/main/res/drawable/background_primary_border.xml b/app/src/main/res/drawable/background_primary_border.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b155c58889e5dbb291615af26f487bc92604258 --- /dev/null +++ b/app/src/main/res/drawable/background_primary_border.xml @@ -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> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_comments.xml b/app/src/main/res/layout/fragment_comments.xml index 0e49ff194e0e6ad63014ba117741a70bb07e6b5f..29fe4c0444e8fb87cc10dad5e3a94278984b3b0f 100644 --- a/app/src/main/res/layout/fragment_comments.xml +++ b/app/src/main/res/layout/fragment_comments.xml @@ -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" diff --git a/app/src/main/res/layout/overlay_loader.xml b/app/src/main/res/layout/overlay_loader.xml index c16a6bda29f7aaf0255bed9fbdfbf56f45983494..dafc34c6ab2019c8e4f17f1332c6aceb534a5f2e 100644 --- a/app/src/main/res/layout/overlay_loader.xml +++ b/app/src/main/res/layout/overlay_loader.xml @@ -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" diff --git a/app/src/main/res/layout/overlay_replies.xml b/app/src/main/res/layout/overlay_replies.xml index c683b38a3a0cd2e502ecbab3c9b0b7a8e1620178..51bb4b4c770bd23a53b177c44f7e33482fb3e70f 100644 --- a/app/src/main/res/layout/overlay_replies.xml +++ b/app/src/main/res/layout/overlay_replies.xml @@ -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> \ No newline at end of file diff --git a/app/src/main/res/layout/view_gesture_controls.xml b/app/src/main/res/layout/view_gesture_controls.xml index a358a60aec4013c0b73a5d3f5a566cc0e2accdec..ea175d3148eaecaebc6648893099e9eb6a13813b 100644 --- a/app/src/main/res/layout/view_gesture_controls.xml +++ b/app/src/main/res/layout/view_gesture_controls.xml @@ -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> \ No newline at end of file