mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-28 02:33:03 +02:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53f2be2b4c | |||
| 11d4ec383e | |||
| 493d77b43b | |||
| 1edc8aabf8 | |||
| 91060faac9 | |||
| 17027ba364 | |||
| 8569eaa5db | |||
| d32d817e0a | |||
| a0f4cc760c | |||
| 5247997ea5 | |||
| 453030d561 | |||
| e080702a52 | |||
| 3909343adc | |||
| dc76934d0e | |||
| 6cf47d592a | |||
| a058bdbfef | |||
| 3d863b9c89 | |||
| 04c0679930 | |||
| 45ded8d384 | |||
| f64efdc964 |
+3
-3
@@ -154,16 +154,16 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.dagger:dagger:2.48'
|
||||
//implementation 'com.google.dagger:dagger:2.48'
|
||||
implementation 'androidx.test:monitor:1.7.2'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
||||
//annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
||||
|
||||
//Core
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
|
||||
//Images
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
||||
|
||||
@@ -603,6 +603,11 @@ class Settings : FragmentedStorageFileJson() {
|
||||
else -> 2.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.shorts_pregenerate, FieldForm.TOGGLE, R.string.shorts_pregenerate_description, 28)
|
||||
var shortsPregenerate: Boolean = false;
|
||||
}
|
||||
|
||||
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.matchesDomain
|
||||
import com.futo.platformplayer.others.LoginWebViewClient
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
@@ -74,6 +75,7 @@ class LoginActivity : AppCompatActivity() {
|
||||
finish();
|
||||
};
|
||||
var isFirstLoad = true;
|
||||
val loginWarnings = authConfig.loginWarnings?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.Warning>();
|
||||
webViewClient.onPageLoaded.subscribe { view, url ->
|
||||
_textUrl.setText(url ?: "");
|
||||
|
||||
@@ -86,6 +88,19 @@ class LoginActivity : AppCompatActivity() {
|
||||
//TODO: Find most reliable way to wait for page js to finish
|
||||
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
|
||||
}
|
||||
|
||||
if(loginWarnings.size > 0) {
|
||||
synchronized(loginWarnings) {
|
||||
val warning = loginWarnings.find { it.url.matches(it.getRegex()) };
|
||||
if(warning != null) {
|
||||
if(warning.once == true)
|
||||
loginWarnings.remove(warning);
|
||||
UIDialogs.showDialog(this@LoginActivity, R.drawable.ic_warning_yellow, warning.text ?: "", warning.details ?: "", null, 0,
|
||||
UIDialogs.Action("Understood", {
|
||||
}, UIDialogs.ActionStyle.PRIMARY));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_webView.settings.domStorageEnabled = true;
|
||||
|
||||
|
||||
@@ -365,14 +365,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
_fragVideoDetail.onMinimize.subscribe {
|
||||
updateSegmentPaddings();
|
||||
};
|
||||
_fragVideoDetail.onTransitioning.subscribe {
|
||||
if (it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED)
|
||||
_fragContainerOverlay.elevation =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics);
|
||||
else
|
||||
_fragContainerOverlay.elevation =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
||||
}
|
||||
|
||||
_fragVideoDetail.onCloseEvent.subscribe {
|
||||
_fragMainHome.setPreviewsEnabled(true);
|
||||
@@ -1143,8 +1135,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
if (_fragContainerVideoDetail.visibility != View.VISIBLE)
|
||||
_fragContainerVideoDetail.visibility = View.VISIBLE;
|
||||
when (segment.state) {
|
||||
VideoDetailFragment.State.MINIMIZED -> segment.maximizeVideoDetail()
|
||||
VideoDetailFragment.State.CLOSED -> segment.maximizeVideoDetail()
|
||||
VideoDetailFragment.State.MINIMIZED -> segment.maximizeVideoDetail(false)
|
||||
VideoDetailFragment.State.CLOSED -> segment.maximizeVideoDetail(false)
|
||||
else -> {}
|
||||
}
|
||||
segment.onShown(parameter, isBack);
|
||||
@@ -1269,7 +1261,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun updateSegmentPaddings() {
|
||||
var paddingBottom = 0f;
|
||||
if (fragCurrent.hasBottomBar)
|
||||
@@ -1280,9 +1271,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
.toInt()
|
||||
);
|
||||
|
||||
if (_fragVideoDetail.state == VideoDetailFragment.State.MINIMIZED)
|
||||
paddingBottom += HEIGHT_VIDEO_MINIMIZED_DP;
|
||||
|
||||
_fragContainerMain.setPadding(
|
||||
0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom, resources.displayMetrics)
|
||||
.toInt()
|
||||
|
||||
+28
-3
@@ -1,6 +1,10 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.Dictionary
|
||||
|
||||
@Serializable
|
||||
class SourcePluginAuthConfig(
|
||||
val loginUrl: String,
|
||||
val completionUrl: String? = null,
|
||||
@@ -11,5 +15,26 @@ class SourcePluginAuthConfig(
|
||||
val userAgent: String? = null,
|
||||
val loginButton: String? = null,
|
||||
val domainHeadersToFind: Map<String, List<String>>? = null,
|
||||
val loginWarning: String? = null
|
||||
) { }
|
||||
val loginWarning: String? = null,
|
||||
val loginWarnings: List<Warning>? = null
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
class Warning(
|
||||
val url: String,
|
||||
val text: String?,
|
||||
val details: String? = null,
|
||||
val once: Boolean? = true
|
||||
) {
|
||||
@Contextual
|
||||
private var _regex: Regex? = null;
|
||||
|
||||
fun getRegex(): Regex {
|
||||
return _regex ?: url.let {
|
||||
val reg = Regex(it);
|
||||
_regex = reg;
|
||||
return reg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -17,6 +17,7 @@ import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.invokeV8Async
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.others.Language
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
@@ -57,12 +58,24 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||
hasGenerate = _obj.has("generate");
|
||||
}
|
||||
|
||||
private var _pregenerate: V8Deferred<String?>? = null;
|
||||
fun pregenerateAsync(scope: CoroutineScope): V8Deferred<String?>? {
|
||||
_pregenerate = generateAsync(scope);
|
||||
return _pregenerate;
|
||||
}
|
||||
|
||||
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
|
||||
if(!hasGenerate)
|
||||
return V8Deferred(CompletableDeferred(manifest));
|
||||
if(_obj.isClosed)
|
||||
throw IllegalStateException("Source object already closed");
|
||||
|
||||
val pregenerated = _pregenerate;
|
||||
if(pregenerated != null) {
|
||||
Logger.w("JSDashManifestRawAudioSource", "Returning pre-generated audio");
|
||||
return pregenerated;
|
||||
}
|
||||
|
||||
val plugin = _plugin.getUnderlyingPlugin();
|
||||
|
||||
var result: V8Deferred<V8ValueString>? = null;
|
||||
|
||||
+12
@@ -18,6 +18,7 @@ import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.invokeV8
|
||||
import com.futo.platformplayer.invokeV8Async
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -65,11 +66,22 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
||||
hasGenerate = _obj.has("generate");
|
||||
}
|
||||
|
||||
private var _pregenerate: V8Deferred<String?>? = null;
|
||||
fun pregenerateAsync(scope: CoroutineScope): V8Deferred<String?>? {
|
||||
_pregenerate = generateAsync(scope);
|
||||
return _pregenerate;
|
||||
}
|
||||
|
||||
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
|
||||
if(!hasGenerate)
|
||||
return V8Deferred(CompletableDeferred(manifest));
|
||||
if(_obj.isClosed)
|
||||
throw IllegalStateException("Source object already closed");
|
||||
val pregenerated = _pregenerate;
|
||||
if(pregenerated != null) {
|
||||
Logger.w("JSDashManifestRawSource", "Returning pre-generated video");
|
||||
return pregenerated;
|
||||
}
|
||||
|
||||
val plugin = _plugin.getUnderlyingPlugin();
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ class MultiDistributionContentPager<T : IPlatformContent> : MultiPager<T> {
|
||||
private val dist : HashMap<IPager<T>, Float>;
|
||||
private val distConsumed : HashMap<IPager<T>, Float>;
|
||||
|
||||
constructor(pagers : Map<IPager<T>, Float>) : super(pagers.keys.toMutableList()) {
|
||||
constructor(pagers : Map<IPager<T>, Float>, pageSize: Int = 9) : super(pagers.keys.toMutableList(), false, pageSize) {
|
||||
val distTotal = pagers.values.sum();
|
||||
dist = HashMap();
|
||||
|
||||
|
||||
@@ -719,7 +719,7 @@ class VideoDownload {
|
||||
|
||||
Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString());
|
||||
|
||||
var written = 0;
|
||||
var written: Long = 0;
|
||||
var indexCounter = 0;
|
||||
onProgress(foundCues.count().toLong(), 0, 0);
|
||||
for(cue in foundCues) {
|
||||
@@ -744,7 +744,7 @@ class VideoDownload {
|
||||
|
||||
indexCounter++;
|
||||
}
|
||||
sourceLength = written.toLong();
|
||||
sourceLength = written;
|
||||
|
||||
Logger.i(TAG, "$name downloadSource Finished");
|
||||
}
|
||||
|
||||
@@ -194,7 +194,11 @@ class PackageBridge : V8Package {
|
||||
|
||||
val stackTrace = Thread.currentThread().stackTrace;
|
||||
val callerMethod = stackTrace.findLast {
|
||||
it.className == JSClient::class.java.name
|
||||
it.className == JSClient::class.java.name &&
|
||||
it.methodName != "isBusy" &&
|
||||
it.methodName != "busy" &&
|
||||
it.methodName != "getCopy" &&
|
||||
it.methodName != "isBusyWith"
|
||||
}?.methodName ?: "";
|
||||
val session = StateApp.instance.sessionId;
|
||||
val pluginId = _plugin.config.id;
|
||||
|
||||
+2
-2
@@ -199,7 +199,7 @@ class ChannelFragment : MainFragment() {
|
||||
when (v) {
|
||||
is IPlatformVideo -> {
|
||||
StatePlayer.instance.clearQueue()
|
||||
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail()
|
||||
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail(false)
|
||||
}
|
||||
|
||||
is IPlatformPlaylist -> {
|
||||
@@ -245,7 +245,7 @@ class ChannelFragment : MainFragment() {
|
||||
when (contentType) {
|
||||
ContentType.MEDIA -> {
|
||||
StatePlayer.instance.clearQueue()
|
||||
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail()
|
||||
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail(false)
|
||||
}
|
||||
|
||||
ContentType.URL -> fragment.navigate<BrowserFragment>(url)
|
||||
|
||||
+3
-3
@@ -197,9 +197,9 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
StatePlayer.instance.insertToQueue(content, true);
|
||||
} else {
|
||||
if (Settings.instance.playback.shouldResumePreview(time))
|
||||
fragment.navigate<VideoDetailFragment>(content.withTimestamp(time)).maximizeVideoDetail();
|
||||
fragment.navigate<VideoDetailFragment>(content.withTimestamp(time)).maximizeVideoDetail(false);
|
||||
else
|
||||
fragment.navigate<VideoDetailFragment>(content).maximizeVideoDetail();
|
||||
fragment.navigate<VideoDetailFragment>(content).maximizeVideoDetail(false);
|
||||
}
|
||||
} else if (content is IPlatformPlaylist) {
|
||||
fragment.navigate<RemotePlaylistFragment>(content);
|
||||
@@ -218,7 +218,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
when(contentType) {
|
||||
ContentType.MEDIA -> {
|
||||
StatePlayer.instance.clearQueue()
|
||||
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail()
|
||||
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail(false)
|
||||
}
|
||||
ContentType.PLAYLIST -> fragment.navigate<RemotePlaylistFragment>(url)
|
||||
ContentType.URL -> fragment.navigate<BrowserFragment>(url)
|
||||
|
||||
+1
-1
@@ -174,7 +174,7 @@ class DownloadsFragment : MainFragment() {
|
||||
.asAnyWithTop(findViewById(R.id.downloads_top)) {
|
||||
it.onClick.subscribe {
|
||||
StatePlayer.instance.clearQueue();
|
||||
_frag.navigate<VideoDetailFragment>(it).maximizeVideoDetail();
|
||||
_frag.navigate<VideoDetailFragment>(it).maximizeVideoDetail(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -247,7 +247,7 @@ class HistoryFragment : MainFragment() {
|
||||
val diff = v.video.duration - v.position;
|
||||
val vid: Any = if (diff > 5) { v.video.withTimestamp(v.position) } else { v.video };
|
||||
StatePlayer.instance.clearQueue();
|
||||
_fragment.navigate<VideoDetailFragment>(vid).maximizeVideoDetail();
|
||||
_fragment.navigate<VideoDetailFragment>(vid).maximizeVideoDetail(false);
|
||||
_editSearch.clearFocus();
|
||||
inputMethodManager.hideSoftInputFromWindow(_editSearch.windowToken, 0);
|
||||
|
||||
|
||||
+105
-505
@@ -1,46 +1,27 @@
|
||||
package com.futo.platformplayer.fragment.mainactivity.main
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.text.Spanned
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.SoundEffectConstants
|
||||
import android.view.View
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
|
||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
@@ -54,40 +35,30 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.casting.CastConnectionState
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event3
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.downloads.VideoLocal
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptAgeException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||
import com.futo.platformplayer.fixHtmlLinks
|
||||
import com.futo.platformplayer.getNowDiffSeconds
|
||||
import com.futo.platformplayer.fragment.mainactivity.special.CommentsModalBottomSheet
|
||||
import com.futo.platformplayer.helpers.VideoHelper
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateMeta
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.toHumanBitrate
|
||||
import com.futo.platformplayer.toHumanBytesSize
|
||||
import com.futo.platformplayer.toHumanNowDiffString
|
||||
import com.futo.platformplayer.toHumanNumber
|
||||
import com.futo.platformplayer.views.MonetizationView
|
||||
import com.futo.platformplayer.views.comments.AddCommentView
|
||||
import com.futo.platformplayer.views.buttons.ShortsButton
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.overlays.DescriptionOverlay
|
||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||
import com.futo.platformplayer.views.overlays.SupportOverlay
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuButtonList
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||
@@ -95,20 +66,17 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTitle
|
||||
import com.futo.platformplayer.views.pills.OnLikeDislikeUpdatedArgs
|
||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
import com.futo.platformplayer.views.segments.CommentsList
|
||||
import com.futo.platformplayer.views.video.FutoShortPlayer
|
||||
import com.futo.platformplayer.views.video.FutoVideoPlayerBase
|
||||
import com.futo.platformplayer.views.video.FutoVideoPlayerBase.Companion.PREFERED_AUDIO_CONTAINERS
|
||||
import com.futo.platformplayer.views.video.FutoVideoPlayerBase.Companion.PREFERED_VIDEO_CONTAINERS
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.ContentType
|
||||
import com.futo.polycentric.core.Models
|
||||
import com.futo.polycentric.core.Opinion
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButton
|
||||
//import com.google.android.material.button.MaterialButton
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -116,30 +84,29 @@ import userpackage.Protocol
|
||||
|
||||
@UnstableApi
|
||||
class ShortView : FrameLayout {
|
||||
private lateinit var mainFragment: MainFragment
|
||||
private lateinit var fragment: MainFragment
|
||||
private val player: FutoShortPlayer
|
||||
|
||||
private val channelInfo: LinearLayout
|
||||
private val creatorThumbnail: CreatorThumbnail
|
||||
private val channelName: TextView
|
||||
private val videoTitle: TextView
|
||||
private val videoSubtitle: TextView
|
||||
private val platformIndicator: PlatformIndicator
|
||||
|
||||
//TODO: Replace with non-material button
|
||||
private val backButton: MaterialButton
|
||||
private val backButtonContainer: ConstraintLayout
|
||||
|
||||
private val likeContainer: FrameLayout
|
||||
private val dislikeContainer: FrameLayout
|
||||
private val likeButton: MaterialButton
|
||||
private val likeCount: TextView
|
||||
private val dislikeButton: MaterialButton
|
||||
private val dislikeCount: TextView
|
||||
private val likeButton: ShortsButton
|
||||
//private val likeCount: TextView
|
||||
private val dislikeButton: ShortsButton
|
||||
//private val dislikeCount: TextView
|
||||
|
||||
private val commentsButton: MaterialButton
|
||||
private val shareButton: MaterialButton
|
||||
private val refreshButton: MaterialButton
|
||||
private val refreshButtonContainer: View
|
||||
private val qualityButton: MaterialButton
|
||||
private val commentsButton: ShortsButton
|
||||
private val shareButton: ShortsButton
|
||||
private val refreshButton: ShortsButton
|
||||
private val qualityButton: ShortsButton
|
||||
|
||||
private val playPauseOverlay: FrameLayout
|
||||
private val playPauseIcon: ImageView
|
||||
@@ -173,18 +140,21 @@ class ShortView : FrameLayout {
|
||||
private val onLikeDislikeUpdated = Event1<OnLikeDislikeUpdatedArgs>()
|
||||
private val onVideoUpdated = Event1<IPlatformVideo?>()
|
||||
|
||||
//TODO: Replace with non-material UI? Only true dependency on Material left
|
||||
private val bottomSheet: CommentsModalBottomSheet = CommentsModalBottomSheet()
|
||||
|
||||
var likes: Long = 0
|
||||
set(value) {
|
||||
field = value
|
||||
likeCount.text = value.toString()
|
||||
likeButton.withPrimaryText(value.toString());
|
||||
//likeCount.text = value.toString()
|
||||
}
|
||||
|
||||
var dislikes: Long = 0
|
||||
set(value) {
|
||||
field = value
|
||||
dislikeCount.text = value.toString()
|
||||
dislikeButton.withPrimaryText(value.toString());
|
||||
//dislikeCount.text = value.toString()
|
||||
}
|
||||
|
||||
constructor(inflater: LayoutInflater, fragment: MainFragment, overlayQualityContainer: FrameLayout) : this(inflater.context) {
|
||||
@@ -194,7 +164,7 @@ class ShortView : FrameLayout {
|
||||
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
this.mainFragment = fragment
|
||||
this.fragment = fragment
|
||||
bottomSheet.mainFragment = fragment
|
||||
}
|
||||
|
||||
@@ -217,19 +187,17 @@ class ShortView : FrameLayout {
|
||||
creatorThumbnail = findViewById(R.id.creator_thumbnail)
|
||||
channelName = findViewById(R.id.channel_name)
|
||||
videoTitle = findViewById(R.id.video_title)
|
||||
videoSubtitle = findViewById(R.id.video_subtitle)
|
||||
platformIndicator = findViewById(R.id.short_platform_indicator)
|
||||
backButton = findViewById(R.id.back_button)
|
||||
backButtonContainer = findViewById(R.id.back_button_container)
|
||||
likeContainer = findViewById(R.id.like_container)
|
||||
dislikeContainer = findViewById(R.id.dislike_container)
|
||||
likeButton = findViewById(R.id.like_button)
|
||||
likeCount = findViewById(R.id.like_count)
|
||||
//likeCount = findViewById(R.id.like_count)
|
||||
dislikeButton = findViewById(R.id.dislike_button)
|
||||
dislikeCount = findViewById(R.id.dislike_count)
|
||||
//dislikeCount = findViewById(R.id.dislike_count)
|
||||
commentsButton = findViewById(R.id.comments_button)
|
||||
shareButton = findViewById(R.id.share_button)
|
||||
refreshButton = findViewById(R.id.refresh_button)
|
||||
refreshButtonContainer = findViewById(R.id.refresh_button_container)
|
||||
qualityButton = findViewById(R.id.quality_button)
|
||||
playPauseOverlay = findViewById(R.id.play_pause_overlay)
|
||||
playPauseIcon = findViewById(R.id.play_pause_icon)
|
||||
@@ -258,48 +226,44 @@ class ShortView : FrameLayout {
|
||||
}
|
||||
|
||||
onVideoUpdated.subscribe {
|
||||
Logger.i(TAG, "Shorts videoUpdated [${it?.name}] (isDetail: ${it is IPlatformVideoDetails}, thumbnail: ${it?.author?.thumbnail})");
|
||||
videoTitle.text = it?.name
|
||||
videoSubtitle.text = if(it is IPlatformVideoDetails) it?.description; else "";
|
||||
platformIndicator.setPlatformFromClientID(it?.id?.pluginId)
|
||||
creatorThumbnail.setThumbnail(it?.author?.thumbnail, true)
|
||||
channelName.text = it?.author?.name
|
||||
}
|
||||
|
||||
backButton.setOnClickListener {
|
||||
playSoundEffect(SoundEffectConstants.CLICK)
|
||||
mainFragment.closeSegment()
|
||||
fragment.closeSegment()
|
||||
}
|
||||
|
||||
channelInfo.setOnClickListener {
|
||||
playSoundEffect(SoundEffectConstants.CLICK)
|
||||
mainFragment.navigate<ChannelFragment>(video?.author)
|
||||
fragment.navigate<ChannelFragment>(video?.author)
|
||||
}
|
||||
|
||||
videoTitle.setOnClickListener {
|
||||
playSoundEffect(SoundEffectConstants.CLICK)
|
||||
if (!bottomSheet.isAdded) {
|
||||
bottomSheet.show(mainFragment.childFragmentManager, CommentsModalBottomSheet.TAG)
|
||||
bottomSheet.show(fragment.childFragmentManager, CommentsModalBottomSheet.TAG)
|
||||
}
|
||||
}
|
||||
|
||||
commentsButton.setOnClickListener {
|
||||
playSoundEffect(SoundEffectConstants.CLICK)
|
||||
commentsButton.onClick.subscribe {
|
||||
if (!bottomSheet.isAdded) {
|
||||
bottomSheet.show(mainFragment.childFragmentManager, CommentsModalBottomSheet.TAG)
|
||||
bottomSheet.show(fragment.childFragmentManager, CommentsModalBottomSheet.TAG)
|
||||
}
|
||||
}
|
||||
|
||||
shareButton.setOnClickListener {
|
||||
playSoundEffect(SoundEffectConstants.CLICK)
|
||||
shareButton.onClick.subscribe {
|
||||
val url = video?.shareUrl ?: video?.url
|
||||
mainFragment.startActivity(Intent.createChooser(Intent().apply {
|
||||
fragment.startActivity(Intent.createChooser(Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, url)
|
||||
type = "text/plain"
|
||||
}, null))
|
||||
}
|
||||
|
||||
refreshButton.setOnClickListener {
|
||||
playSoundEffect(SoundEffectConstants.CLICK)
|
||||
refreshButton.onClick.subscribe {
|
||||
onResetTriggered.emit()
|
||||
}
|
||||
|
||||
@@ -308,14 +272,12 @@ class ShortView : FrameLayout {
|
||||
false
|
||||
}
|
||||
|
||||
qualityButton.setOnClickListener {
|
||||
playSoundEffect(SoundEffectConstants.CLICK)
|
||||
qualityButton.onClick.subscribe {
|
||||
showVideoSettings()
|
||||
}
|
||||
|
||||
likeButton.setOnClickListener {
|
||||
playSoundEffect(SoundEffectConstants.CLICK)
|
||||
val checked = !likeButton.isChecked
|
||||
likeButton.onClick.subscribe {
|
||||
val checked = likeButton.iconId == R.drawable.ic_thumb_up_s // !likeButton.isChecked
|
||||
StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) {
|
||||
if (checked) {
|
||||
likes++
|
||||
@@ -323,24 +285,27 @@ class ShortView : FrameLayout {
|
||||
likes--
|
||||
}
|
||||
|
||||
likeButton.isChecked = checked
|
||||
if(checked)
|
||||
likeButton.withIcon(R.drawable.ic_thumb_up_s_filled) //.isChecked = checked
|
||||
else
|
||||
likeButton.withIcon(R.drawable.ic_thumb_up_s)
|
||||
|
||||
if (dislikeButton.isChecked && checked) {
|
||||
dislikeButton.isChecked = false
|
||||
if (dislikeButton.iconId == R.drawable.ic_thumb_down_s_filled && checked) {
|
||||
//dislikeButton.isChecked = false
|
||||
dislikeButton.withIcon(R.drawable.ic_thumb_down_s)
|
||||
dislikes--
|
||||
}
|
||||
|
||||
onLikeDislikeUpdated.emit(
|
||||
OnLikeDislikeUpdatedArgs(
|
||||
it, likes, likeButton.isChecked, dislikes, dislikeButton.isChecked
|
||||
it, likes, checked, dislikes, !checked
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dislikeButton.setOnClickListener {
|
||||
playSoundEffect(SoundEffectConstants.CLICK)
|
||||
val checked = !dislikeButton.isChecked
|
||||
dislikeButton.onClick.subscribe {
|
||||
val checked = dislikeButton.iconId == R.drawable.ic_thumb_down_s //!dislikeButton.isChecked
|
||||
StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) {
|
||||
if (checked) {
|
||||
dislikes++
|
||||
@@ -348,16 +313,21 @@ class ShortView : FrameLayout {
|
||||
dislikes--
|
||||
}
|
||||
|
||||
dislikeButton.isChecked = checked
|
||||
//dislikeButton.isChecked = checked
|
||||
if(checked)
|
||||
dislikeButton.withIcon(R.drawable.ic_thumb_down_s_filled) //.isChecked = checked
|
||||
else
|
||||
dislikeButton.withIcon(R.drawable.ic_thumb_down_s)
|
||||
|
||||
if (likeButton.isChecked && checked) {
|
||||
likeButton.isChecked = false
|
||||
if (likeButton.iconId == R.drawable.ic_thumb_up_s_filled && checked) {
|
||||
//likeButton.isChecked = false
|
||||
likeButton.withIcon(R.drawable.ic_thumb_up_s);
|
||||
likes--
|
||||
}
|
||||
|
||||
onLikeDislikeUpdated.emit(
|
||||
OnLikeDislikeUpdatedArgs(
|
||||
it, likes, likeButton.isChecked, dislikes, dislikeButton.isChecked
|
||||
it, likes, !checked, dislikes, checked
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -366,11 +336,11 @@ class ShortView : FrameLayout {
|
||||
onLikesLoaded.subscribe(tag) { rating, liked, disliked ->
|
||||
likes = rating.likes
|
||||
dislikes = rating.dislikes
|
||||
likeButton.isChecked = liked
|
||||
dislikeButton.isChecked = disliked
|
||||
//likeButton.isChecked = liked
|
||||
//dislikeButton.isChecked = disliked
|
||||
|
||||
dislikeContainer.visibility = VISIBLE
|
||||
likeContainer.visibility = VISIBLE
|
||||
dislikeButton.visibility = VISIBLE
|
||||
likeButton.visibility = VISIBLE
|
||||
}
|
||||
|
||||
player.onPlaybackStateChanged.subscribe {
|
||||
@@ -565,7 +535,7 @@ class ShortView : FrameLayout {
|
||||
var toSet: ISubtitleSource? = subtitleSource
|
||||
if (_lastSubtitleSource == subtitleSource) toSet = null
|
||||
|
||||
mainFragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
player.swapSubtitles(toSet)
|
||||
} catch (e: Throwable) {
|
||||
@@ -625,7 +595,7 @@ class ShortView : FrameLayout {
|
||||
|
||||
@Suppress("unused")
|
||||
fun setMainFragment(fragment: MainFragment, overlayQualityContainer: FrameLayout) {
|
||||
this.mainFragment = fragment
|
||||
this.fragment = fragment
|
||||
this.bottomSheet.mainFragment = fragment
|
||||
this.overlayQualityContainer = overlayQualityContainer
|
||||
}
|
||||
@@ -636,10 +606,10 @@ class ShortView : FrameLayout {
|
||||
}
|
||||
this.video = video
|
||||
|
||||
refreshButtonContainer.visibility = if (isChannelShortsMode) {
|
||||
refreshButton.visibility = if (isChannelShortsMode) {
|
||||
GONE
|
||||
} else {
|
||||
VISIBLE
|
||||
GONE //TODO: Revert?
|
||||
}
|
||||
backButtonContainer.visibility = if (isChannelShortsMode) {
|
||||
VISIBLE
|
||||
@@ -695,8 +665,8 @@ class ShortView : FrameLayout {
|
||||
}
|
||||
|
||||
private fun loadLikes(video: IPlatformVideo) {
|
||||
likeContainer.visibility = GONE
|
||||
dislikeContainer.visibility = GONE
|
||||
likeButton.visibility = GONE
|
||||
dislikeButton.visibility = GONE
|
||||
|
||||
loadLikesTask?.cancel()
|
||||
loadLikesTask =
|
||||
@@ -735,13 +705,13 @@ class ShortView : FrameLayout {
|
||||
args.processHandle.opinion(ref, Opinion.neutral)
|
||||
}
|
||||
|
||||
mainFragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
Logger.i(CommentsModalBottomSheet.TAG, "Started backfill")
|
||||
Logger.i(TAG, "Started backfill")
|
||||
args.processHandle.fullyBackfillServersAnnounceExceptions()
|
||||
Logger.i(CommentsModalBottomSheet.TAG, "Finished backfill")
|
||||
Logger.i(TAG, "Finished backfill")
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(CommentsModalBottomSheet.TAG, "Failed to backfill servers", e)
|
||||
Logger.e(TAG, "Failed to backfill servers", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -763,20 +733,41 @@ class ShortView : FrameLayout {
|
||||
|
||||
setLoading(true)
|
||||
|
||||
Logger.i(TAG, "Shorts loadVideo [${url}]");
|
||||
val timeLoadVideoStart = System.currentTimeMillis();
|
||||
loadVideoTask = TaskHandler<String, IPlatformVideoDetails>(
|
||||
StateApp.instance.scopeGetter, {
|
||||
val result = StatePlatform.instance.getContentDetails(it).await()
|
||||
if (result !is IPlatformVideoDetails) throw IllegalStateException("Expected media content, found ${result.contentType}")
|
||||
return@TaskHandler result
|
||||
}).success { result ->
|
||||
videoDetails = result
|
||||
video = result
|
||||
val timeLoadVideo = System.currentTimeMillis() - timeLoadVideoStart;
|
||||
Logger.i(TAG, "Shorts loadVideo [${url}] took ${timeLoadVideo}ms");
|
||||
videoDetails = result
|
||||
video = result
|
||||
|
||||
bottomSheet.video = result
|
||||
if(Settings.instance.playback.shortsPregenerate)
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
if(result != null) {
|
||||
val prefVid = VideoHelper.selectBestVideoSource(result.video, Settings.instance.playback.getCurrentPreferredQualityPixelCount(), PREFERED_VIDEO_CONTAINERS);
|
||||
val prefAud = VideoHelper.selectBestAudioSource(result.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context));
|
||||
|
||||
setLoading(false)
|
||||
if(prefVid != null && prefVid is JSDashManifestRawSource) {
|
||||
Logger.i(TAG, "Shorts pregenerating video (${result.name})");
|
||||
prefVid.pregenerateAsync(fragment.lifecycleScope);
|
||||
}
|
||||
if(prefAud != null && prefAud is JSDashManifestRawAudioSource) {
|
||||
Logger.i(TAG, "Shorts pregenerating audio (${result.name})");
|
||||
prefAud.pregenerateAsync(fragment.lifecycleScope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (playWhenReady) playVideo()
|
||||
bottomSheet.video = result
|
||||
|
||||
setLoading(false)
|
||||
|
||||
if (playWhenReady) playVideo()
|
||||
}.exception<NoPlatformClientException> {
|
||||
Logger.w(TAG, "exception<NoPlatformClientException>", it)
|
||||
UIDialogs.showDialog(
|
||||
@@ -799,7 +790,7 @@ class ShortView : FrameLayout {
|
||||
UIDialogs.showSingleButtonDialog(context, R.drawable.ic_schedule, "Video is available in ${it.availableWhen}.", "Close") { }
|
||||
}.exception<ScriptImplementationException> {
|
||||
Logger.w(TAG, "exception<ScriptImplementationException>", it)
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, { loadVideo(url) }, null, mainFragment)
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, { loadVideo(url) }, null, fragment)
|
||||
}.exception<ScriptAgeException> {
|
||||
Logger.w(TAG, "exception<ScriptAgeException>", it)
|
||||
UIDialogs.showDialog(
|
||||
@@ -812,10 +803,10 @@ class ShortView : FrameLayout {
|
||||
)
|
||||
}.exception<ScriptException> {
|
||||
Logger.w(TAG, "exception<ScriptException>", it)
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, { loadVideo(url) }, null, mainFragment)
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, { loadVideo(url) }, null, fragment)
|
||||
}.exception<Throwable> {
|
||||
Logger.w(ChannelFragment.TAG, "Failed to load video.", it)
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, { loadVideo(url) }, null, mainFragment)
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, { loadVideo(url) }, null, fragment)
|
||||
}
|
||||
|
||||
loadVideoTask?.run(url)
|
||||
@@ -849,6 +840,7 @@ class ShortView : FrameLayout {
|
||||
}
|
||||
|
||||
val thumbnail = videoDetails.thumbnails.getHQThumbnail()
|
||||
/*
|
||||
if (videoSource == null && !thumbnail.isNullOrBlank()) Glide.with(context).asBitmap()
|
||||
.load(thumbnail).into(object : CustomTarget<Bitmap>() {
|
||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||
@@ -860,8 +852,9 @@ class ShortView : FrameLayout {
|
||||
}
|
||||
})
|
||||
else player.setArtwork(null)
|
||||
*/
|
||||
|
||||
mainFragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0)
|
||||
if (subtitleSource != null) player.swapSubtitles(subtitleSource)
|
||||
@@ -887,397 +880,4 @@ class ShortView : FrameLayout {
|
||||
const val TAG = "VideoDetailView"
|
||||
}
|
||||
|
||||
class CommentsModalBottomSheet : BottomSheetDialogFragment() {
|
||||
var mainFragment: MainFragment? = null
|
||||
|
||||
private lateinit var containerContent: FrameLayout
|
||||
private lateinit var containerContentMain: LinearLayout
|
||||
private lateinit var containerContentReplies: RepliesOverlay
|
||||
private lateinit var containerContentDescription: DescriptionOverlay
|
||||
private lateinit var containerContentSupport: SupportOverlay
|
||||
|
||||
private lateinit var title: TextView
|
||||
private lateinit var subTitle: TextView
|
||||
private lateinit var channelName: TextView
|
||||
private lateinit var channelMeta: TextView
|
||||
private lateinit var creatorThumbnail: CreatorThumbnail
|
||||
private lateinit var channelButton: LinearLayout
|
||||
private lateinit var monetization: MonetizationView
|
||||
private lateinit var platform: PlatformIndicator
|
||||
private lateinit var textLikes: TextView
|
||||
private lateinit var textDislikes: TextView
|
||||
private lateinit var layoutRating: LinearLayout
|
||||
private lateinit var imageDislikeIcon: ImageView
|
||||
private lateinit var imageLikeIcon: ImageView
|
||||
|
||||
private lateinit var description: TextView
|
||||
private lateinit var descriptionContainer: LinearLayout
|
||||
private lateinit var descriptionViewMore: TextView
|
||||
|
||||
private lateinit var commentsList: CommentsList
|
||||
private lateinit var addCommentView: AddCommentView
|
||||
|
||||
private var polycentricProfile: PolycentricProfile? = null
|
||||
|
||||
private lateinit var buttonPolycentric: Button
|
||||
private lateinit var buttonPlatform: Button
|
||||
|
||||
private var tabIndex: Int? = null
|
||||
|
||||
private var contentOverlayView: View? = null
|
||||
|
||||
lateinit var video: IPlatformVideoDetails
|
||||
|
||||
private lateinit var behavior: BottomSheetBehavior<FrameLayout>
|
||||
|
||||
private val _taskLoadPolycentricProfile =
|
||||
TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) }).success { setPolycentricProfile(it, animate = true) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load claims.", it)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(
|
||||
savedInstanceState: Bundle?,
|
||||
): Dialog {
|
||||
val bottomSheetDialog =
|
||||
BottomSheetDialog(requireContext(), R.style.Custom_BottomSheetDialog_Theme)
|
||||
bottomSheetDialog.setContentView(R.layout.modal_comments)
|
||||
|
||||
behavior = bottomSheetDialog.behavior
|
||||
|
||||
// TODO figure out how to not need all of these non null assertions
|
||||
containerContent = bottomSheetDialog.findViewById(R.id.content_container)!!
|
||||
containerContentMain = bottomSheetDialog.findViewById(R.id.videodetail_container_main)!!
|
||||
containerContentReplies =
|
||||
bottomSheetDialog.findViewById(R.id.videodetail_container_replies)!!
|
||||
containerContentDescription =
|
||||
bottomSheetDialog.findViewById(R.id.videodetail_container_description)!!
|
||||
containerContentSupport =
|
||||
bottomSheetDialog.findViewById(R.id.videodetail_container_support)!!
|
||||
|
||||
title = bottomSheetDialog.findViewById(R.id.videodetail_title)!!
|
||||
subTitle = bottomSheetDialog.findViewById(R.id.videodetail_meta)!!
|
||||
channelName = bottomSheetDialog.findViewById(R.id.videodetail_channel_name)!!
|
||||
channelMeta = bottomSheetDialog.findViewById(R.id.videodetail_channel_meta)!!
|
||||
creatorThumbnail = bottomSheetDialog.findViewById(R.id.creator_thumbnail)!!
|
||||
channelButton = bottomSheetDialog.findViewById(R.id.videodetail_channel_button)!!
|
||||
monetization = bottomSheetDialog.findViewById(R.id.monetization)!!
|
||||
platform = bottomSheetDialog.findViewById(R.id.videodetail_platform)!!
|
||||
layoutRating = bottomSheetDialog.findViewById(R.id.layout_rating)!!
|
||||
textDislikes = bottomSheetDialog.findViewById(R.id.text_dislikes)!!
|
||||
textLikes = bottomSheetDialog.findViewById(R.id.text_likes)!!
|
||||
imageLikeIcon = bottomSheetDialog.findViewById(R.id.image_like_icon)!!
|
||||
imageDislikeIcon = bottomSheetDialog.findViewById(R.id.image_dislike_icon)!!
|
||||
|
||||
description = bottomSheetDialog.findViewById(R.id.videodetail_description)!!
|
||||
descriptionContainer =
|
||||
bottomSheetDialog.findViewById(R.id.videodetail_description_container)!!
|
||||
descriptionViewMore =
|
||||
bottomSheetDialog.findViewById(R.id.videodetail_description_view_more)!!
|
||||
|
||||
addCommentView = bottomSheetDialog.findViewById(R.id.add_comment_view)!!
|
||||
commentsList = bottomSheetDialog.findViewById(R.id.comments_list)!!
|
||||
buttonPolycentric = bottomSheetDialog.findViewById(R.id.button_polycentric)!!
|
||||
buttonPlatform = bottomSheetDialog.findViewById(R.id.button_platform)!!
|
||||
|
||||
commentsList.onAuthorClick.subscribe { c ->
|
||||
if (c !is PolycentricPlatformComment) {
|
||||
return@subscribe
|
||||
}
|
||||
val id = c.author.id.value
|
||||
|
||||
Logger.i(TAG, "onAuthorClick: $id")
|
||||
if (id != null && id.startsWith("polycentric://")) {
|
||||
val navUrl = "https://harbor.social/" + id.substring("polycentric://".length)
|
||||
mainFragment!!.startActivity(Intent(Intent.ACTION_VIEW, navUrl.toUri()))
|
||||
}
|
||||
}
|
||||
commentsList.onRepliesClick.subscribe { c ->
|
||||
val replyCount = c.replyCount ?: 0
|
||||
var metadata = ""
|
||||
if (replyCount > 0) {
|
||||
metadata += "$replyCount " + requireContext().getString(R.string.replies)
|
||||
}
|
||||
|
||||
if (c is PolycentricPlatformComment) {
|
||||
var parentComment: PolycentricPlatformComment = c
|
||||
containerContentReplies.load(tabIndex!! != 0, metadata, c.contextUrl, c.reference, c, { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) }, {
|
||||
val newComment = parentComment.cloneWithUpdatedReplyCount(
|
||||
(parentComment.replyCount ?: 0) + 1
|
||||
)
|
||||
commentsList.replaceComment(parentComment, newComment)
|
||||
parentComment = newComment
|
||||
})
|
||||
} else {
|
||||
containerContentReplies.load(tabIndex!! != 0, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) })
|
||||
}
|
||||
animateOpenOverlayView(containerContentReplies)
|
||||
}
|
||||
|
||||
if (StatePolycentric.instance.enabled) {
|
||||
buttonPolycentric.setOnClickListener {
|
||||
setTabIndex(0)
|
||||
StateMeta.instance.setLastCommentSection(0)
|
||||
}
|
||||
} else {
|
||||
buttonPolycentric.visibility = GONE
|
||||
}
|
||||
|
||||
buttonPlatform.setOnClickListener {
|
||||
setTabIndex(1)
|
||||
StateMeta.instance.setLastCommentSection(1)
|
||||
}
|
||||
|
||||
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||
addCommentView.setContext(video.url, ref)
|
||||
|
||||
if (Settings.instance.comments.recommendationsDefault && !Settings.instance.comments.hideRecommendations) {
|
||||
setTabIndex(2, true)
|
||||
} else {
|
||||
when (Settings.instance.comments.defaultCommentSection) {
|
||||
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(0, true) else setTabIndex(1, true)
|
||||
1 -> setTabIndex(1, true)
|
||||
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
|
||||
}
|
||||
}
|
||||
|
||||
containerContentDescription.onClose.subscribe { animateCloseOverlayView() }
|
||||
containerContentReplies.onClose.subscribe { animateCloseOverlayView() }
|
||||
|
||||
descriptionViewMore.setOnClickListener {
|
||||
animateOpenOverlayView(containerContentDescription)
|
||||
}
|
||||
|
||||
updateDescriptionUI(video.description.fixHtmlLinks())
|
||||
|
||||
val dp5 =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics)
|
||||
val dp2 =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics)
|
||||
|
||||
//UI
|
||||
title.text = video.name
|
||||
channelName.text = video.author.name
|
||||
if (video.author.subscribers != null) {
|
||||
channelMeta.text = if ((video.author.subscribers
|
||||
?: 0) > 0
|
||||
) video.author.subscribers!!.toHumanNumber() + " " + requireContext().getString(R.string.subscribers) else ""
|
||||
(channelName.layoutParams as MarginLayoutParams).setMargins(
|
||||
0, (dp5 * -1).toInt(), 0, 0
|
||||
)
|
||||
} else {
|
||||
channelMeta.text = ""
|
||||
(channelName.layoutParams as MarginLayoutParams).setMargins(0, (dp2).toInt(), 0, 0)
|
||||
}
|
||||
|
||||
video.author.let {
|
||||
if (it is PlatformAuthorMembershipLink && !it.membershipUrl.isNullOrEmpty()) monetization.setPlatformMembership(video.id.pluginId, it.membershipUrl)
|
||||
else monetization.setPlatformMembership(null, null)
|
||||
}
|
||||
|
||||
val subTitleSegments: ArrayList<String> = ArrayList()
|
||||
if (video.viewCount > 0) subTitleSegments.add("${video.viewCount.toHumanNumber()} ${if (video.isLive) requireContext().getString(R.string.watching_now) else requireContext().getString(R.string.views)}")
|
||||
if (video.datetime != null) {
|
||||
val diff = video.datetime?.getNowDiffSeconds() ?: 0
|
||||
val ago = video.datetime?.toHumanNowDiffString(true)
|
||||
if (diff >= 0) subTitleSegments.add("$ago ago")
|
||||
else subTitleSegments.add("available in $ago")
|
||||
}
|
||||
|
||||
platform.setPlatformFromClientID(video.id.pluginId)
|
||||
subTitle.text = subTitleSegments.joinToString(" • ")
|
||||
creatorThumbnail.setThumbnail(video.author.thumbnail, false)
|
||||
|
||||
setPolycentricProfile(null, animate = false)
|
||||
_taskLoadPolycentricProfile.run(video.author.id)
|
||||
|
||||
when (video.rating) {
|
||||
is RatingLikeDislikes -> {
|
||||
val r = video.rating as RatingLikeDislikes
|
||||
layoutRating.visibility = VISIBLE
|
||||
|
||||
textLikes.visibility = VISIBLE
|
||||
imageLikeIcon.visibility = VISIBLE
|
||||
textLikes.text = r.likes.toHumanNumber()
|
||||
|
||||
imageDislikeIcon.visibility = VISIBLE
|
||||
textDislikes.visibility = VISIBLE
|
||||
textDislikes.text = r.dislikes.toHumanNumber()
|
||||
}
|
||||
|
||||
is RatingLikes -> {
|
||||
val r = video.rating as RatingLikes
|
||||
layoutRating.visibility = VISIBLE
|
||||
|
||||
textLikes.visibility = VISIBLE
|
||||
imageLikeIcon.visibility = VISIBLE
|
||||
textLikes.text = r.likes.toHumanNumber()
|
||||
|
||||
imageDislikeIcon.visibility = GONE
|
||||
textDislikes.visibility = GONE
|
||||
}
|
||||
|
||||
else -> {
|
||||
layoutRating.visibility = GONE
|
||||
}
|
||||
}
|
||||
|
||||
monetization.onSupportTap.subscribe {
|
||||
containerContentSupport.setPolycentricProfile(polycentricProfile)
|
||||
animateOpenOverlayView(containerContentSupport)
|
||||
}
|
||||
|
||||
monetization.onStoreTap.subscribe {
|
||||
polycentricProfile?.systemState?.store?.let {
|
||||
try {
|
||||
val uri = it.toUri()
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = uri
|
||||
requireContext().startActivity(intent)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to open URI: '${it}'.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
monetization.onUrlTap.subscribe {
|
||||
mainFragment!!.navigate<BrowserFragment>(it)
|
||||
}
|
||||
|
||||
addCommentView.onCommentAdded.subscribe {
|
||||
commentsList.addComment(it)
|
||||
}
|
||||
|
||||
channelButton.setOnClickListener {
|
||||
mainFragment!!.navigate<ChannelFragment>(video.author)
|
||||
}
|
||||
|
||||
return bottomSheetDialog
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
animateCloseOverlayView()
|
||||
}
|
||||
|
||||
private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
|
||||
polycentricProfile = profile
|
||||
|
||||
val dp35 = 35.dp(requireContext().resources)
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }
|
||||
|
||||
if (avatar != null) {
|
||||
creatorThumbnail.setThumbnail(avatar, animate)
|
||||
} else {
|
||||
creatorThumbnail.setThumbnail(video.author.thumbnail, animate)
|
||||
creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto())
|
||||
}
|
||||
|
||||
val username = profile?.systemState?.username
|
||||
if (username != null) {
|
||||
channelName.text = username
|
||||
}
|
||||
|
||||
monetization.setPolycentricProfile(profile)
|
||||
}
|
||||
|
||||
private fun setTabIndex(index: Int?, forceReload: Boolean = false) {
|
||||
Logger.i(TAG, "setTabIndex (index: ${index}, forceReload: ${forceReload})")
|
||||
val changed = tabIndex != index || forceReload
|
||||
if (!changed) {
|
||||
return
|
||||
}
|
||||
|
||||
tabIndex = index
|
||||
buttonPlatform.setTextColor(resources.getColor(if (index == 1) R.color.white else R.color.gray_ac, null))
|
||||
buttonPolycentric.setTextColor(resources.getColor(if (index == 0) R.color.white else R.color.gray_ac, null))
|
||||
|
||||
when (index) {
|
||||
null -> {
|
||||
addCommentView.visibility = GONE
|
||||
commentsList.clear()
|
||||
}
|
||||
|
||||
0 -> {
|
||||
addCommentView.visibility = VISIBLE
|
||||
fetchPolycentricComments()
|
||||
}
|
||||
|
||||
1 -> {
|
||||
addCommentView.visibility = GONE
|
||||
fetchComments()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchComments() {
|
||||
Logger.i(TAG, "fetchComments")
|
||||
video.let {
|
||||
commentsList.load(true) { StatePlatform.instance.getComments(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchPolycentricComments() {
|
||||
Logger.i(TAG, "fetchPolycentricComments")
|
||||
val video = video
|
||||
val idValue = video.id.value
|
||||
if (video.url.isEmpty()) {
|
||||
Logger.w(TAG, "Failed to fetch polycentric comments because url was null")
|
||||
commentsList.clear()
|
||||
return
|
||||
}
|
||||
|
||||
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||
val extraBytesRef = idValue?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
||||
commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); }
|
||||
}
|
||||
|
||||
private fun updateDescriptionUI(text: Spanned) {
|
||||
containerContentDescription.load(text)
|
||||
description.text = text
|
||||
|
||||
if (description.text.isNotEmpty()) descriptionContainer.visibility = VISIBLE
|
||||
else descriptionContainer.visibility = GONE
|
||||
}
|
||||
|
||||
private fun animateOpenOverlayView(view: View) {
|
||||
if (contentOverlayView != null) {
|
||||
Logger.e(TAG, "Content overlay already open")
|
||||
return
|
||||
}
|
||||
|
||||
behavior.isDraggable = false
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
|
||||
val animHeight = containerContentMain.height
|
||||
|
||||
view.translationY = animHeight.toFloat()
|
||||
view.visibility = VISIBLE
|
||||
|
||||
view.animate().setDuration(300).translationY(0f).withEndAction {
|
||||
contentOverlayView = view
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun animateCloseOverlayView() {
|
||||
val curView = contentOverlayView
|
||||
if (curView == null) {
|
||||
Logger.e(TAG, "No content overlay open")
|
||||
return
|
||||
}
|
||||
|
||||
behavior.isDraggable = true
|
||||
|
||||
val animHeight = contentOverlayView!!.height
|
||||
|
||||
curView.animate().setDuration(300).translationY(animHeight.toFloat()).withEndAction {
|
||||
curView.visibility = GONE
|
||||
contentOverlayView = null
|
||||
}.start()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "ModalBottomSheet"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-17
@@ -11,6 +11,7 @@ import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
@@ -25,6 +26,9 @@ import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@UnstableApi
|
||||
class ShortsFragment : MainFragment() {
|
||||
@@ -35,6 +39,7 @@ class ShortsFragment : MainFragment() {
|
||||
private var loadPagerTask: TaskHandler<ShortsFragment, IPager<IPlatformVideo>>? = null
|
||||
private var nextPageTask: TaskHandler<ShortsFragment, List<IPlatformVideo>>? = null
|
||||
|
||||
//TODO: Reduce number of pagers (1, or at most 2)
|
||||
private var mainShortsPager: IPager<IPlatformVideo>? = null
|
||||
private val mainShorts: MutableList<IPlatformVideo> = mutableListOf()
|
||||
|
||||
@@ -58,6 +63,7 @@ class ShortsFragment : MainFragment() {
|
||||
private var customViewAdapter: CustomViewAdapter? = null
|
||||
|
||||
// we just completely reset the data structure so we want to tell the adapter that
|
||||
//TODO: Move most of this logic to ShortsView
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
(activity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
|
||||
@@ -118,7 +124,6 @@ class ShortsFragment : MainFragment() {
|
||||
overlayQualityContainer = view.findViewById(R.id.shorts_quality_overview)
|
||||
|
||||
sourcesButton.onClick.subscribe {
|
||||
sourcesButton.playSoundEffect(SoundEffectConstants.CLICK)
|
||||
navigate<SourcesFragment>()
|
||||
}
|
||||
|
||||
@@ -145,7 +150,7 @@ class ShortsFragment : MainFragment() {
|
||||
|
||||
this.customViewAdapter = customViewAdapter
|
||||
|
||||
if (loadPagerTask == null && currentShorts.isEmpty()) {
|
||||
if (loadPagerTask == null) {// && currentShorts.isEmpty()) {
|
||||
loadPager()
|
||||
|
||||
loadPagerTask!!.success {
|
||||
@@ -207,28 +212,29 @@ class ShortsFragment : MainFragment() {
|
||||
}
|
||||
|
||||
private fun nextPage() {
|
||||
nextPageTask?.cancel()
|
||||
|
||||
val nextPageTask =
|
||||
TaskHandler<ShortsFragment, List<IPlatformVideo>>(StateApp.instance.scopeGetter, {
|
||||
currentShortsPager!!.nextPage()
|
||||
|
||||
return@TaskHandler currentShortsPager!!.getResults()
|
||||
}).success { newVideos ->
|
||||
Logger.i(TAG, "ShortsFragment nextPage");
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val time = measureTimeMillis {
|
||||
currentShortsPager!!.nextPage();
|
||||
}
|
||||
val newVideos = currentShortsPager!!.getResults();
|
||||
val prevCount = customViewAdapter!!.itemCount
|
||||
Logger.i(TAG, "Shorts nextPage took ${time}ms, ${prevCount}-${prevCount + newVideos.size}, hasMore: ${currentShortsPager?.hasMorePages()}");
|
||||
currentShorts.addAll(newVideos)
|
||||
if (isChannelShortsMode) {
|
||||
channelShorts.addAll(newVideos)
|
||||
} else {
|
||||
mainShorts.addAll(newVideos)
|
||||
}
|
||||
customViewAdapter!!.notifyItemRangeInserted(prevCount, newVideos.size)
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
customViewAdapter!!.notifyItemRangeInserted(prevCount, newVideos.size)
|
||||
}
|
||||
nextPageTask = null
|
||||
} catch (ex: Throwable) {
|
||||
Logger.e(TAG, "Shorts Failed to call nextPage", ex);
|
||||
}
|
||||
|
||||
nextPageTask.run(this)
|
||||
|
||||
this.nextPageTask = nextPageTask
|
||||
}
|
||||
}
|
||||
|
||||
// we just completely reset the data structure so we want to tell the adapter that
|
||||
@@ -236,12 +242,16 @@ class ShortsFragment : MainFragment() {
|
||||
private fun loadPager() {
|
||||
loadPagerTask?.cancel()
|
||||
|
||||
Logger.i(TAG, "Shorts loadPage");
|
||||
var loadPageStart = System.currentTimeMillis();
|
||||
val loadPagerTask =
|
||||
TaskHandler<ShortsFragment, IPager<IPlatformVideo>>(StateApp.instance.scopeGetter, {
|
||||
val pager = StatePlatform.instance.getShorts()
|
||||
val pager = StatePlatform.instance.getShorts();
|
||||
|
||||
return@TaskHandler pager
|
||||
}).success { pager ->
|
||||
val timeLoadPage = System.currentTimeMillis() - loadPageStart;
|
||||
Logger.i(TAG, "Shorts loadPage took ${timeLoadPage}ms");
|
||||
mainShorts.clear()
|
||||
mainShorts.addAll(pager.getResults())
|
||||
mainShortsPager = pager
|
||||
@@ -259,7 +269,7 @@ class ShortsFragment : MainFragment() {
|
||||
loadPagerTask = null
|
||||
}.exception<Throwable> { err ->
|
||||
val message = "Unable to load shorts $err"
|
||||
Logger.i(TAG, message)
|
||||
Logger.w(TAG, message, err)
|
||||
if (context != null) {
|
||||
UIDialogs.showDialog(
|
||||
requireContext(), R.drawable.ic_sources, message, null, null, 0, UIDialogs.Action(
|
||||
@@ -329,6 +339,7 @@ class ShortsFragment : MainFragment() {
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
|
||||
Logger.i(TAG, "Shorts change (position: ${position}): ${videos[position].name} (${videos[position].id.value})")
|
||||
holder.shortView.changeVideo(videos[position], isChannelShortsMode())
|
||||
|
||||
if (position == itemCount - 1) {
|
||||
|
||||
+41
-1
@@ -25,6 +25,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
@@ -152,11 +153,50 @@ class SourceDetailFragment : MainFragment() {
|
||||
if(field is View)
|
||||
field.isVisible = false;
|
||||
}
|
||||
if(!source.capabilities.hasGetUserHistory) {
|
||||
if(!source.capabilities.hasGetUserHistory || !source.isLoggedIn) {
|
||||
val field = _settingsAppForm.findField("sync");
|
||||
if(field is View)
|
||||
field.isVisible = false;
|
||||
}
|
||||
else {
|
||||
val field = _settingsAppForm.findField("syncHistory");
|
||||
field?.onChanged?.subscribe { field, new, old ->
|
||||
if(old != new && new == true && StatePlatform.instance.isClientEnabled(config.id)) {
|
||||
UIDialogs.showDialog(context, R.drawable.ic_sources, "Would you like to sync now?",
|
||||
"This will attempt to update your history from the platform, when this setting is enabled, it is done during startup.", null, 0,
|
||||
UIDialogs.Action("No", {
|
||||
|
||||
}),
|
||||
UIDialogs.Action("Yes", {
|
||||
UIDialogs.showDialogProgress(context, {
|
||||
it.setText("Importing history..");
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val client = StatePlatform.instance.getClient(config.id);
|
||||
if (client != null && client is JSClient) {
|
||||
val count = StateHistory.instance.syncRemoteHistory(client);
|
||||
withContext(Dispatchers.Main) {
|
||||
it.hide();
|
||||
if(count > 0)
|
||||
UIDialogs.showDialogOk(context, R.drawable.ic_pair_success, "Imported ${count} history items");
|
||||
else
|
||||
UIDialogs.showDialogOk(context, R.drawable.ic_help, "Imported no history items");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
withContext(Dispatchers.Main) {
|
||||
UIDialogs.appToast("Sync History failed due to:\n" + ex.message);
|
||||
it.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, UIDialogs.ActionStyle.PRIMARY));
|
||||
}
|
||||
}
|
||||
}
|
||||
_settingsAppForm.onChanged.clear();
|
||||
_settingsAppForm.onChanged.subscribe { field, value ->
|
||||
_settingsAppChanged = true;
|
||||
|
||||
+53
-109
@@ -34,13 +34,12 @@ import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.PlatformVideoWithTime
|
||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
|
||||
import com.futo.platformplayer.views.containers.CustomMotionLayout
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
//region Fragment
|
||||
@UnstableApi
|
||||
class VideoDetailFragment() : MainFragment() {
|
||||
@@ -51,8 +50,8 @@ class VideoDetailFragment() : MainFragment() {
|
||||
|
||||
private var _isActive: Boolean = false;
|
||||
|
||||
private var _viewDetail : VideoDetailView? = null;
|
||||
private var _view : SingleViewTouchableMotionLayout? = null;
|
||||
private var _viewDetail : VideoDetailView? = null
|
||||
private var _motionLayout: CustomMotionLayout? = null
|
||||
|
||||
var isFullscreen : Boolean = false;
|
||||
/**
|
||||
@@ -61,8 +60,6 @@ class VideoDetailFragment() : MainFragment() {
|
||||
*/
|
||||
var isMinimizingFromFullScreen : Boolean = false;
|
||||
val onFullscreenChanged = Event1<Boolean>();
|
||||
var isTransitioning : Boolean = false
|
||||
private set;
|
||||
var isInPictureInPicture : Boolean = false
|
||||
private set;
|
||||
|
||||
@@ -78,13 +75,8 @@ class VideoDetailFragment() : MainFragment() {
|
||||
val currentUrl get() = _viewDetail?.currentUrl;
|
||||
|
||||
val onMinimize = Event0();
|
||||
val onTransitioning = Event1<Boolean>();
|
||||
val onMaximized = Event0();
|
||||
|
||||
private var _isInitialMaximize = true;
|
||||
|
||||
private val _maximizeProgress get() = _view?.progress ?: 0.0f;
|
||||
|
||||
private var _loadUrlOnCreate: UrlVideoWithTime? = null;
|
||||
private var _leavingPiP = false;
|
||||
|
||||
@@ -295,22 +287,17 @@ class VideoDetailFragment() : MainFragment() {
|
||||
|
||||
fun minimizeVideoDetail() {
|
||||
_viewDetail?.setFullscreen(false);
|
||||
if(_view != null)
|
||||
_view!!.transitionToStart();
|
||||
_motionLayout?.transitionToState(R.id.collapsed)
|
||||
}
|
||||
fun maximizeVideoDetail(instant: Boolean = false) {
|
||||
if((_maximizeProgress > 0.9f || instant) && state != State.MAXIMIZED) {
|
||||
state = State.MAXIMIZED;
|
||||
onMaximized.emit();
|
||||
fun maximizeVideoDetail(instant: Boolean) {
|
||||
state = State.MAXIMIZED
|
||||
onMaximized.emit()
|
||||
if(instant) {
|
||||
_motionLayout?.setTransition(R.id.maximize)
|
||||
_motionLayout?.progress = 1f
|
||||
} else {
|
||||
_motionLayout?.transitionToState(R.id.expanded)
|
||||
}
|
||||
_view?.let {
|
||||
if(!instant) {
|
||||
it.transitionToEnd();
|
||||
} else {
|
||||
it.progress = 1f;
|
||||
onTransitioning.emit(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
fun closeVideoDetails() {
|
||||
Logger.i(TAG, "closeVideoDetails()")
|
||||
@@ -323,83 +310,48 @@ class VideoDetailFragment() : MainFragment() {
|
||||
}
|
||||
|
||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_view = inflater.inflate(R.layout.fragment_video_detail, container, false) as SingleViewTouchableMotionLayout;
|
||||
_viewDetail = _view!!.findViewById<VideoDetailView>(R.id.fragview_videodetail).also {
|
||||
it.applyFragment(this);
|
||||
it.onFullscreenChanged.subscribe(::onFullscreenChanged);
|
||||
it.onVideoChanged.subscribe(::onVideoChanged)
|
||||
it.onMinimize.subscribe {
|
||||
isMinimizingFromFullScreen = true
|
||||
_view!!.transitionToStart();
|
||||
};
|
||||
it.onClose.subscribe {
|
||||
Logger.i(TAG, "onClose")
|
||||
closeVideoDetails();
|
||||
};
|
||||
it.onMaximize.subscribe { maximizeVideoDetail(it) };
|
||||
it.onEnterPictureInPicture.subscribe {
|
||||
Logger.i(TAG, "onEnterPictureInPicture")
|
||||
isInPictureInPicture = true;
|
||||
_viewDetail?.handleEnterPictureInPicture();
|
||||
_viewDetail?.invalidate();
|
||||
};
|
||||
it.onTouchCancel.subscribe {
|
||||
val v = _view ?: return@subscribe;
|
||||
if (v.progress >= 0.5 && v.progress < 1) {
|
||||
maximizeVideoDetail();
|
||||
}
|
||||
if (v.progress < 0.5 && v.progress > 0) {
|
||||
minimizeVideoDetail();
|
||||
}
|
||||
};
|
||||
}
|
||||
_view!!.setTransitionListener(object : MotionLayout.TransitionListener {
|
||||
override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float) {
|
||||
val viewDetail = VideoDetailView(this, inflater);
|
||||
|
||||
_motionLayout = viewDetail.findViewById(R.id.videodetail_root)
|
||||
viewDetail.applyFragment(this);
|
||||
viewDetail.onFullscreenChanged.subscribe(::onFullscreenChanged);
|
||||
viewDetail.onVideoChanged.subscribe(::onVideoChanged)
|
||||
viewDetail.onMinimize.subscribe {
|
||||
isMinimizingFromFullScreen = true
|
||||
_motionLayout?.transitionToState(R.id.collapsed)
|
||||
};
|
||||
viewDetail.onClose.subscribe {
|
||||
Logger.i(TAG, "onClose")
|
||||
closeVideoDetails();
|
||||
};
|
||||
viewDetail.onMaximize.subscribe { maximizeVideoDetail(it) };
|
||||
viewDetail.onEnterPictureInPicture.subscribe {
|
||||
Logger.i(TAG, "onEnterPictureInPicture")
|
||||
isInPictureInPicture = true;
|
||||
_viewDetail?.handleEnterPictureInPicture();
|
||||
_viewDetail?.invalidate();
|
||||
};
|
||||
_motionLayout!!.addTransitionListener(object : MotionLayout.TransitionListener {
|
||||
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
|
||||
_viewDetail?.stopAllGestures()
|
||||
|
||||
if (state != State.MINIMIZED && progress < 0.1) {
|
||||
state = State.MINIMIZED;
|
||||
if (state != State.MINIMIZED && currentId == R.id.collapsed) {
|
||||
state = State.MINIMIZED
|
||||
isMinimizingFromFullScreen = false
|
||||
onMinimize.emit();
|
||||
}
|
||||
else if (state != State.MAXIMIZED && progress > 0.9) {
|
||||
if (_isInitialMaximize) {
|
||||
state = State.CLOSED;
|
||||
_isInitialMaximize = false;
|
||||
}
|
||||
else {
|
||||
state = State.MAXIMIZED;
|
||||
onMaximized.emit();
|
||||
}
|
||||
onMinimize.emit()
|
||||
}
|
||||
|
||||
if (isTransitioning && (progress > 0.95 || progress < 0.05)) {
|
||||
isTransitioning = false;
|
||||
onTransitioning.emit(isTransitioning);
|
||||
|
||||
if(isInPictureInPicture) leavePictureInPictureMode(false); //Workaround to prevent getting stuck in p2p
|
||||
}
|
||||
else if (!isTransitioning && (progress < 0.95 && progress > 0.05)) {
|
||||
isTransitioning = true;
|
||||
onTransitioning.emit(isTransitioning);
|
||||
|
||||
if(isInPictureInPicture) leavePictureInPictureMode(false); //Workaround to prevent getting stuck in p2p
|
||||
if (state != State.MAXIMIZED && currentId == R.id.expanded) {
|
||||
state = State.MAXIMIZED
|
||||
onMaximized.emit()
|
||||
}
|
||||
}
|
||||
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) { }
|
||||
override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) { }
|
||||
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) { }
|
||||
});
|
||||
|
||||
_view?.let {
|
||||
if (it.progress >= 0.5 && it.progress < 1.0)
|
||||
maximizeVideoDetail();
|
||||
if (it.progress < 0.5 && it.progress > 0.0)
|
||||
minimizeVideoDetail();
|
||||
}
|
||||
override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) {}
|
||||
override fun onTransitionTrigger(motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float) {}
|
||||
override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float) {}
|
||||
})
|
||||
|
||||
_loadUrlOnCreate?.let { _viewDetail?.setVideo(it.url, it.timeSeconds, it.playWhenReady) };
|
||||
maximizeVideoDetail();
|
||||
maximizeVideoDetail(false);
|
||||
|
||||
SettingsActivity.settingsActivityClosed.subscribe(this) {
|
||||
updateOrientation()
|
||||
@@ -432,12 +384,13 @@ class VideoDetailFragment() : MainFragment() {
|
||||
}
|
||||
_autoRotateObserver?.startObserving()
|
||||
|
||||
return _view!!;
|
||||
_viewDetail = viewDetail
|
||||
return viewDetail
|
||||
}
|
||||
|
||||
fun onUserLeaveHint() {
|
||||
val viewDetail = _viewDetail;
|
||||
Logger.i(TAG, "onUserLeaveHint preventPictureInPicture=${viewDetail?.preventPictureInPicture} isCasting=${StateCasting.instance.isCasting} isBackgroundPictureInPicture=${Settings.instance.playback.isBackgroundPictureInPicture()} allowBackground=${viewDetail?.allowBackground}");
|
||||
Logger.i(TAG, "onUserLeaveHint preventPictureInPicture=${viewDetail?.preventPictureInPicture} isCasting=${StateCasting.instance.isCasting} isBackgroundPictureInPicture=${Settings.instance.playback.isBackgroundPictureInPicture()} allowBackground=${viewDetail?.isAudioOnlyUserAction}");
|
||||
|
||||
if (viewDetail === null) {
|
||||
return
|
||||
@@ -446,7 +399,7 @@ class VideoDetailFragment() : MainFragment() {
|
||||
if (viewDetail.shouldEnterPictureInPicture) {
|
||||
_leavingPiP = false
|
||||
}
|
||||
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.S && viewDetail.preventPictureInPicture == false && !StateCasting.instance.isCasting && Settings.instance.playback.isBackgroundPictureInPicture() && !viewDetail.allowBackground) {
|
||||
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.S && viewDetail.preventPictureInPicture == false && !StateCasting.instance.isCasting && Settings.instance.playback.isBackgroundPictureInPicture() && !viewDetail.isAudioOnlyUserAction) {
|
||||
val params = _viewDetail?.getPictureInPictureParams();
|
||||
if(params != null) {
|
||||
Logger.i(TAG, "enterPictureInPictureMode")
|
||||
@@ -526,7 +479,7 @@ class VideoDetailFragment() : MainFragment() {
|
||||
|
||||
private fun stopIfRequired() {
|
||||
var shouldStop = true;
|
||||
if (_viewDetail?.allowBackground == true) {
|
||||
if (_viewDetail?.isAudioOnlyUserAction == true) {
|
||||
shouldStop = false;
|
||||
} else if (Settings.instance.playback.isBackgroundPictureInPicture() && !_leavingPiP) {
|
||||
shouldStop = false;
|
||||
@@ -554,21 +507,12 @@ class VideoDetailFragment() : MainFragment() {
|
||||
_portraitOrientationListener?.disableListener()
|
||||
_autoRotateObserver?.stopObserving()
|
||||
|
||||
_viewDetail?.let {
|
||||
_viewDetail = null;
|
||||
it.onDestroy();
|
||||
}
|
||||
_view = null;
|
||||
_viewDetail = null;
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
_viewDetail?.let {
|
||||
_viewDetail = null;
|
||||
it.onDestroy();
|
||||
}
|
||||
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||
|
||||
Logger.i(TAG, "onDestroy");
|
||||
@@ -626,7 +570,7 @@ class VideoDetailFragment() : MainFragment() {
|
||||
}
|
||||
|
||||
updateOrientation();
|
||||
_view?.allowMotion = !fullscreen;
|
||||
_motionLayout?.isInteractionEnabled = !fullscreen
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
+96
-154
@@ -10,7 +10,6 @@ import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
@@ -19,11 +18,10 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import android.text.Spanned
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.util.Rational
|
||||
import android.util.TypedValue
|
||||
import android.view.MotionEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
@@ -34,6 +32,7 @@ import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.C
|
||||
@@ -51,7 +50,6 @@ import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.UISlideOverlays
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.activities.SyncShowPairingCodeActivity.Companion.activity
|
||||
import com.futo.platformplayer.api.media.IPluginSourced
|
||||
import com.futo.platformplayer.api.media.LiveChatManager
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
@@ -82,7 +80,6 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
@@ -137,9 +134,9 @@ import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
import com.futo.platformplayer.views.MonetizationView
|
||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoView
|
||||
import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
|
||||
import com.futo.platformplayer.views.casting.CastView
|
||||
import com.futo.platformplayer.views.comments.AddCommentView
|
||||
import com.futo.platformplayer.views.containers.CustomMotionLayout
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.overlays.ChaptersOverlay
|
||||
import com.futo.platformplayer.views.overlays.DescriptionOverlay
|
||||
@@ -170,6 +167,7 @@ import com.futo.polycentric.core.PolycentricProfile
|
||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -185,7 +183,7 @@ import kotlin.math.abs
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
@UnstableApi
|
||||
class VideoDetailView : ConstraintLayout {
|
||||
class VideoDetailView(fragment: VideoDetailFragment, inflater: LayoutInflater) : FrameLayout(inflater.context) {
|
||||
private val TAG = "VideoDetailView"
|
||||
|
||||
lateinit var fragment: VideoDetailFragment;
|
||||
@@ -214,7 +212,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
private val _timeBar: TimeBar;
|
||||
private var _upNext: UpNextView;
|
||||
|
||||
private val rootView: ConstraintLayout;
|
||||
private val rootView: CustomMotionLayout;
|
||||
|
||||
private val _title: TextView;
|
||||
private val _subTitle: TextView;
|
||||
@@ -243,7 +241,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
private val _commentsList: CommentsList;
|
||||
|
||||
private var _minimizeProgress: Float = 0f;
|
||||
private val _buttonSubscribe: SubscribeButton;
|
||||
|
||||
private val _buttonPins: RoundButtonGroup;
|
||||
@@ -265,7 +262,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
private val _textResume: TextView;
|
||||
private val _layoutResume: LinearLayout;
|
||||
private var _jobHideResume: Job? = null;
|
||||
private val _layoutPlayerContainer: TouchInterceptFrameLayout;
|
||||
private val _layoutPlayerContainer: FrameLayout;
|
||||
private val _layoutChangeBottomSection: LinearLayout;
|
||||
|
||||
//Overlays
|
||||
@@ -326,7 +323,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
val onEnterPictureInPicture = Event0();
|
||||
val onVideoChanged = Event2<Int, Int>()
|
||||
|
||||
var allowBackground: Boolean = false
|
||||
var isAudioOnlyUserAction: Boolean = false
|
||||
private set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
@@ -338,12 +335,11 @@ class VideoDetailView : ConstraintLayout {
|
||||
get() = !preventPictureInPicture &&
|
||||
!StateCasting.instance.isCasting &&
|
||||
Settings.instance.playback.isBackgroundPictureInPicture() &&
|
||||
!allowBackground &&
|
||||
!isAudioOnlyUserAction &&
|
||||
isPlaying
|
||||
|
||||
val onShouldEnterPictureInPictureChanged = Event0();
|
||||
|
||||
val onTouchCancel = Event0();
|
||||
private var _lastPositionSaveTime: Long = -1;
|
||||
|
||||
private val DP_5 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
||||
@@ -361,9 +357,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
Pair(0, 10) //around live, try every 10 seconds
|
||||
);
|
||||
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
|
||||
inflate(context, R.layout.fragview_video_detail, this);
|
||||
init {
|
||||
inflater.inflate(R.layout.fragview_video_detail, this)
|
||||
|
||||
//Declare Views
|
||||
rootView = findViewById(R.id.videodetail_root);
|
||||
@@ -416,8 +411,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_textSkip = findViewById(R.id.text_skip);
|
||||
_layoutResume = findViewById(R.id.layout_resume);
|
||||
_textResume = findViewById(R.id.text_resume);
|
||||
_layoutPlayerContainer = findViewById(R.id.layout_player_container);
|
||||
_layoutPlayerContainer.onClick.subscribe { onMaximize.emit(false); };
|
||||
_layoutPlayerContainer = findViewById(R.id.layout_player_container)
|
||||
|
||||
_layoutRating = findViewById(R.id.layout_rating);
|
||||
_textDislikes = findViewById(R.id.text_dislikes);
|
||||
@@ -612,10 +606,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
updatePlaybackTracking(position);
|
||||
};
|
||||
|
||||
_player.onVideoClicked.subscribe {
|
||||
if(_minimizeProgress < 0.5)
|
||||
onMaximize.emit(false);
|
||||
}
|
||||
_player.onSourceChanged.subscribe(::onSourceChanged);
|
||||
_player.onSourceEnded.subscribe {
|
||||
if (!fragment.isInPictureInPicture) {
|
||||
@@ -764,7 +754,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
MediaControlReceiver.onBackgroundReceived.subscribe(this) {
|
||||
Logger.i(TAG, "MediaControlReceiver.onBackgroundReceived")
|
||||
_player.switchToAudioMode(video);
|
||||
allowBackground = true;
|
||||
isAudioOnlyUserAction = true;
|
||||
StateApp.instance.contextOrNull?.let {
|
||||
try {
|
||||
if (it is MainActivity) {
|
||||
@@ -898,6 +888,48 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentState = R.id.expanded
|
||||
|
||||
rootView.addTransitionListener(object : MotionLayout.TransitionListener {
|
||||
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
|
||||
when (currentId) {
|
||||
R.id.collapsed -> {
|
||||
_player.gestureControl.setOnClickListener {
|
||||
fragment.maximizeVideoDetail(false)
|
||||
}
|
||||
_player.gestureControl.controlsEnabled = false
|
||||
}
|
||||
|
||||
R.id.expanded -> {
|
||||
_layoutResume.alpha = 1f
|
||||
_cast.setButtonAlpha(1f)
|
||||
_player.lockControlsAlpha(false)
|
||||
_player.hideControls(false)
|
||||
|
||||
_player.gestureControl.controlsEnabled = true
|
||||
_player.gestureControl.setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
currentState = currentId
|
||||
if(currentId == R.id.full_screen_gesture) {
|
||||
setFullscreen(true)
|
||||
motionLayout?.transitionToState(R.id.expanded)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTransitionStarted(motionLayout: MotionLayout?, startId: Int, endId: Int) {
|
||||
if (currentState == R.id.expanded) {
|
||||
_layoutResume.alpha = 0f
|
||||
_cast.setButtonAlpha(0f)
|
||||
_player.lockControlsAlpha(true)
|
||||
_player.hideControls(true);
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTransitionTrigger(motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float) { }
|
||||
override fun onTransitionChange(motionLayout: MotionLayout?, startId: Int, endId: Int, progress: Float) {}
|
||||
})
|
||||
}
|
||||
|
||||
val _trackingUpdateTimeLock = Object();
|
||||
@@ -1009,14 +1041,26 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
_slideUpOverlay?.hide();
|
||||
} else null,
|
||||
if (!isLimitedVersion) RoundButton(context, R.drawable.ic_screen_share, if (allowBackground) context.getString(R.string.background_revert) else context.getString(R.string.background), TAG_BACKGROUND) {
|
||||
if (!allowBackground) {
|
||||
if(video is JSVideoDetails && (video as JSVideoDetails).hasVODEvents())
|
||||
RoundButton(context, R.drawable.ic_chat, context.getString(R.string.vod_chat), TAG_VODCHAT) {
|
||||
video?.let {
|
||||
try {
|
||||
loadVODChat(it);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to reopen vod chat", ex);
|
||||
}
|
||||
}
|
||||
_slideUpOverlay?.hide();
|
||||
} else null,
|
||||
if (!isLimitedVersion) RoundButton(context, R.drawable.ic_screen_share, if (isAudioOnlyUserAction) context.getString(R.string.background_revert) else context.getString(R.string.background), TAG_BACKGROUND) {
|
||||
if (!isAudioOnlyUserAction) {
|
||||
_player.switchToAudioMode(video);
|
||||
allowBackground = true;
|
||||
isAudioOnlyUserAction = true;
|
||||
it.text.text = resources.getString(R.string.background_revert);
|
||||
} else {
|
||||
_player.switchToVideoMode();
|
||||
allowBackground = false;
|
||||
isAudioOnlyUserAction = false;
|
||||
it.text.text = resources.getString(R.string.background);
|
||||
}
|
||||
_slideUpOverlay?.hide();
|
||||
@@ -1132,10 +1176,14 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
|
||||
//Lifecycle
|
||||
var isLoginStop = false; //TODO: This is a bit jank, but easiest solution for now without reworking flow. (Alternatively, fix MainActivity getting stopped/disposing video)
|
||||
fun onResume() {
|
||||
Logger.v(TAG, "onResume");
|
||||
_onPauseCalled = false;
|
||||
|
||||
val wasLoginCall = isLoginStop;
|
||||
isLoginStop = false;
|
||||
|
||||
Logger.i(TAG, "_video: ${video?.name ?: "no video"}");
|
||||
Logger.i(TAG, "_didStop: $_didStop");
|
||||
|
||||
@@ -1144,7 +1192,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
val t = (lastPositionMilliseconds / 1000.0f).roundToLong();
|
||||
if(_searchVideo != null)
|
||||
setVideoOverview(_searchVideo!!, true, t);
|
||||
else if(_url != null)
|
||||
else if(_url != null && !wasLoginCall)
|
||||
setVideo(_url!!, t, _playWhenReady);
|
||||
}
|
||||
else if(_didStop) {
|
||||
@@ -1156,11 +1204,12 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
if(_player.isAudioMode) {
|
||||
//Requested behavior to leave it in audio mode. leaving it commented if it causes issues, revert?
|
||||
if(!allowBackground) {
|
||||
if(!isAudioOnlyUserAction) {
|
||||
_player.switchToVideoMode();
|
||||
allowBackground = false;
|
||||
isAudioOnlyUserAction = false;
|
||||
_buttonPins.getButtonByTag(TAG_BACKGROUND)?.text?.text = resources.getString(R.string.background);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
_buttonPins.getButtonByTag(TAG_BACKGROUND)?.text?.text = resources.getString(R.string.video);
|
||||
}
|
||||
}
|
||||
@@ -1178,7 +1227,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
if(StateCasting.instance.isCasting)
|
||||
return;
|
||||
|
||||
if(allowBackground)
|
||||
if(isAudioOnlyUserAction)
|
||||
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
||||
else {
|
||||
when (Settings.instance.playback.backgroundPlay) {
|
||||
@@ -1186,7 +1235,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
1 -> {
|
||||
if(!(video?.isLive ?: false)) {
|
||||
_player.switchToAudioMode(video);
|
||||
allowBackground = true;
|
||||
}
|
||||
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
||||
}
|
||||
@@ -1974,10 +2022,10 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
if (isLimitedVersion && _player.isAudioMode) {
|
||||
_player.switchToVideoMode()
|
||||
allowBackground = false;
|
||||
isAudioOnlyUserAction = false;
|
||||
} else {
|
||||
val thumbnail = video.thumbnails.getHQThumbnail();
|
||||
if ((videoSource == null || _player.isAudioMode) && !thumbnail.isNullOrBlank())
|
||||
if ((videoSource == null) && !thumbnail.isNullOrBlank()) // || _player.isAudioMode
|
||||
Glide.with(context).asBitmap().load(thumbnail)
|
||||
.into(object: CustomTarget<Bitmap>() {
|
||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||
@@ -2189,16 +2237,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
|
||||
if (ev?.actionMasked == MotionEvent.ACTION_CANCEL ||
|
||||
ev?.actionMasked == MotionEvent.ACTION_POINTER_DOWN ||
|
||||
ev?.actionMasked == MotionEvent.ACTION_POINTER_UP) {
|
||||
onTouchCancel.emit();
|
||||
}
|
||||
|
||||
return super.onInterceptTouchEvent(ev);
|
||||
}
|
||||
|
||||
//Actions
|
||||
private fun showVideoSettings() {
|
||||
Logger.i(TAG, "showVideoSettings")
|
||||
@@ -2692,10 +2730,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
Logger.i(TAG, "handleFullScreen(fullscreen=$fullscreen)")
|
||||
|
||||
if(fullscreen) {
|
||||
_container_content.visibility = GONE
|
||||
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
|
||||
|
||||
val lp = _container_content.layoutParams as LayoutParams;
|
||||
val lp = _container_content.layoutParams as ConstraintLayout.LayoutParams;
|
||||
lp.topMargin = 0;
|
||||
_container_content.layoutParams = lp;
|
||||
|
||||
@@ -2706,10 +2741,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
setProgressBarOverlayed(null);
|
||||
}
|
||||
else {
|
||||
_container_content.visibility = VISIBLE
|
||||
_layoutPlayerContainer.setPadding(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, Resources.getSystem().displayMetrics).toInt());
|
||||
|
||||
val lp = _container_content.layoutParams as LayoutParams;
|
||||
val lp = _container_content.layoutParams as ConstraintLayout.LayoutParams;
|
||||
lp.topMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -18.0f, Resources.getSystem().displayMetrics).toInt();
|
||||
_container_content.layoutParams = lp;
|
||||
|
||||
@@ -2938,7 +2970,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
hideAddTo()
|
||||
|
||||
onVideoClicked.subscribe { video, _ ->
|
||||
fragment.navigate<VideoDetailFragment>(video).maximizeVideoDetail()
|
||||
fragment.navigate<VideoDetailFragment>(video).maximizeVideoDetail(false)
|
||||
}
|
||||
|
||||
onChannelClicked.subscribe {
|
||||
@@ -2985,17 +3017,12 @@ class VideoDetailView : ConstraintLayout {
|
||||
_container_content.visibility = GONE
|
||||
|
||||
_player.fillHeight(false)
|
||||
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
fun handleLeavePictureInPicture() {
|
||||
Logger.i(TAG, "handleLeavePictureInPicture")
|
||||
|
||||
if(!_player.isFullScreen) {
|
||||
_container_content.visibility = VISIBLE
|
||||
_player.fitHeight();
|
||||
_layoutPlayerContainer.setPadding(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, Resources.getSystem().displayMetrics).toInt());
|
||||
} else {
|
||||
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
fun getPictureInPictureParams() : PictureInPictureParams {
|
||||
@@ -3100,53 +3127,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
//Animation related setters
|
||||
fun setMinimizeProgress(progress : Float) {
|
||||
_minimizeProgress = progress;
|
||||
_player.lockControlsAlpha(progress < 0.9);
|
||||
_layoutPlayerContainer.shouldInterceptTouches = progress < 0.95;
|
||||
|
||||
if(progress > 0.9) {
|
||||
if(_minimize_controls.visibility != View.GONE)
|
||||
_minimize_controls.visibility = View.GONE;
|
||||
}
|
||||
else if(_minimize_controls.visibility != View.VISIBLE) {
|
||||
_minimize_controls.visibility = View.VISIBLE;
|
||||
}
|
||||
|
||||
//Switching video to fill
|
||||
if(progress > 0.25) {
|
||||
if(!_player.isFullScreen && _player.layoutParams.height != WRAP_CONTENT) {
|
||||
_player.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT);
|
||||
if(!fragment.isInPictureInPicture) {
|
||||
_player.fitHeight();
|
||||
_layoutPlayerContainer.setPadding(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, Resources.getSystem().displayMetrics).toInt());
|
||||
}
|
||||
else {
|
||||
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
_cast.layoutParams = _cast.layoutParams.apply {
|
||||
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, resources.displayMetrics).toInt();
|
||||
};
|
||||
setProgressBarOverlayed(false);
|
||||
_player.hideControls(false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(_player.layoutParams.height == WRAP_CONTENT) {
|
||||
_player.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
|
||||
_player.fillHeight(true)
|
||||
_cast.layoutParams = _cast.layoutParams.apply {
|
||||
(this as MarginLayoutParams).bottomMargin = 0;
|
||||
};
|
||||
setProgressBarOverlayed(true);
|
||||
_player.hideControls(false);
|
||||
|
||||
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
|
||||
_polycentricProfile = profile
|
||||
|
||||
@@ -3170,42 +3150,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
|
||||
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
|
||||
Logger.v(TAG, "setProgressBarOverlayed(isOverlayed: ${isOverlayed ?: "null"})");
|
||||
isOverlayed?.let{ _cast.setProgressBarOverlayed(it) };
|
||||
|
||||
if(isOverlayed == null) {
|
||||
//For now this seems to be the best way to keep it updated?
|
||||
_playerProgress.layoutParams = _playerProgress.layoutParams.apply {
|
||||
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -12f, resources.displayMetrics).toInt();
|
||||
};
|
||||
_playerProgress.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics);
|
||||
}
|
||||
else if(isOverlayed) {
|
||||
_playerProgress.layoutParams = _playerProgress.layoutParams.apply {
|
||||
(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);
|
||||
}
|
||||
else {
|
||||
_playerProgress.layoutParams = _playerProgress.layoutParams.apply {
|
||||
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6f, resources.displayMetrics).toInt();
|
||||
};
|
||||
_playerProgress.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics);
|
||||
}
|
||||
}
|
||||
fun setContentAlpha(alpha: Float) {
|
||||
_container_content.alpha = alpha;
|
||||
}
|
||||
fun setControllerAlpha(alpha: Float) {
|
||||
_layoutResume.alpha = alpha;
|
||||
_player.videoControls.alpha = alpha;
|
||||
_cast.setButtonAlpha(alpha);
|
||||
}
|
||||
fun setMinimizeControlsAlpha(alpha : Float) {
|
||||
_minimize_controls.alpha = alpha;
|
||||
val clickable = alpha > 0.9;
|
||||
if(_minimize_controls.isClickable != clickable)
|
||||
_minimize_controls.isClickable = clickable;
|
||||
Logger.v(TAG, "setProgressBarOverlayed(isOverlayed: ${isOverlayed ?: "null"})")
|
||||
isOverlayed?.let { _cast.setProgressBarOverlayed(it) }
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||
@@ -3217,16 +3163,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fun setVideoMinimize(value : Float) {
|
||||
val padRight = (resources.displayMetrics.widthPixels * 0.70 * value).toInt()
|
||||
_player.setPadding(0, _player.paddingTop, padRight, 0)
|
||||
_cast.setPadding(0, _cast.paddingTop, padRight, 0)
|
||||
}
|
||||
|
||||
fun setTopPadding(value: Float) {
|
||||
_player.setPadding(_player.paddingLeft, value.toInt(), _player.paddingRight, 0)
|
||||
}
|
||||
|
||||
//Tasks
|
||||
private val _taskLoadVideo = if(!isInEditMode) TaskHandler<String, IPlatformVideoDetails>(
|
||||
StateApp.instance.scopeGetter,
|
||||
@@ -3267,8 +3203,13 @@ class VideoDetailView : ConstraintLayout {
|
||||
val id = e.config.let { if(it is SourcePluginConfig) it.id else null };
|
||||
val didLogin = if(id == null)
|
||||
false
|
||||
else StatePlugins.instance.loginPlugin(context, id) {
|
||||
fetchVideo();
|
||||
else {
|
||||
isLoginStop = true;
|
||||
StatePlugins.instance.loginPlugin(context, id) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
fetchVideo();
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!didLogin)
|
||||
UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, "Failed to login");
|
||||
@@ -3446,6 +3387,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
const val TAG_SHARE = "share";
|
||||
const val TAG_OVERLAY = "overlay";
|
||||
const val TAG_LIVECHAT = "livechat";
|
||||
const val TAG_VODCHAT = "vodchat";
|
||||
const val TAG_CHAPTERS = "chapters";
|
||||
const val TAG_OPEN = "open";
|
||||
const val TAG_SEND_TO_DEVICE = "send_to_device";
|
||||
|
||||
+454
@@ -0,0 +1,454 @@
|
||||
package com.futo.platformplayer.fragment.mainactivity.special
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.Spanned
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.FrameLayout.GONE
|
||||
import android.widget.FrameLayout.VISIBLE
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.net.toUri
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.fixHtmlLinks
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
||||
import com.futo.platformplayer.getNowDiffSeconds
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateMeta
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.toHumanNowDiffString
|
||||
import com.futo.platformplayer.toHumanNumber
|
||||
import com.futo.platformplayer.views.MonetizationView
|
||||
import com.futo.platformplayer.views.comments.AddCommentView
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.overlays.DescriptionOverlay
|
||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||
import com.futo.platformplayer.views.overlays.SupportOverlay
|
||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
import com.futo.platformplayer.views.segments.CommentsList
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.Models
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
|
||||
|
||||
class CommentsModalBottomSheet : BottomSheetDialogFragment() {
|
||||
var mainFragment: MainFragment? = null
|
||||
|
||||
private lateinit var containerContent: FrameLayout
|
||||
private lateinit var containerContentMain: LinearLayout
|
||||
private lateinit var containerContentReplies: RepliesOverlay
|
||||
private lateinit var containerContentDescription: DescriptionOverlay
|
||||
private lateinit var containerContentSupport: SupportOverlay
|
||||
|
||||
private lateinit var title: TextView
|
||||
private lateinit var subTitle: TextView
|
||||
private lateinit var channelName: TextView
|
||||
private lateinit var channelMeta: TextView
|
||||
private lateinit var creatorThumbnail: CreatorThumbnail
|
||||
private lateinit var channelButton: LinearLayout
|
||||
private lateinit var monetization: MonetizationView
|
||||
private lateinit var platform: PlatformIndicator
|
||||
private lateinit var textLikes: TextView
|
||||
private lateinit var textDislikes: TextView
|
||||
private lateinit var layoutRating: LinearLayout
|
||||
private lateinit var imageDislikeIcon: ImageView
|
||||
private lateinit var imageLikeIcon: ImageView
|
||||
|
||||
private lateinit var description: TextView
|
||||
private lateinit var descriptionContainer: LinearLayout
|
||||
private lateinit var descriptionViewMore: TextView
|
||||
|
||||
private lateinit var commentsList: CommentsList
|
||||
private lateinit var addCommentView: AddCommentView
|
||||
|
||||
private var polycentricProfile: PolycentricProfile? = null
|
||||
|
||||
private lateinit var buttonPolycentric: Button
|
||||
private lateinit var buttonPlatform: Button
|
||||
|
||||
private var tabIndex: Int? = null
|
||||
|
||||
private var contentOverlayView: View? = null
|
||||
|
||||
lateinit var video: IPlatformVideoDetails
|
||||
|
||||
private lateinit var behavior: BottomSheetBehavior<FrameLayout>
|
||||
|
||||
private val _taskLoadPolycentricProfile =
|
||||
TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(
|
||||
ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) }).success { setPolycentricProfile(it, animate = true) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load claims.", it)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(
|
||||
savedInstanceState: Bundle?,
|
||||
): Dialog {
|
||||
val bottomSheetDialog =
|
||||
BottomSheetDialog(requireContext(), R.style.Custom_BottomSheetDialog_Theme)
|
||||
bottomSheetDialog.setContentView(R.layout.modal_comments)
|
||||
|
||||
behavior = bottomSheetDialog.behavior
|
||||
|
||||
// TODO figure out how to not need all of these non null assertions
|
||||
containerContent = bottomSheetDialog.findViewById(R.id.content_container)!!
|
||||
containerContentMain = bottomSheetDialog.findViewById(R.id.videodetail_container_main)!!
|
||||
containerContentReplies =
|
||||
bottomSheetDialog.findViewById(R.id.videodetail_container_replies)!!
|
||||
containerContentDescription =
|
||||
bottomSheetDialog.findViewById(R.id.videodetail_container_description)!!
|
||||
containerContentSupport =
|
||||
bottomSheetDialog.findViewById(R.id.videodetail_container_support)!!
|
||||
|
||||
title = bottomSheetDialog.findViewById(R.id.videodetail_title)!!
|
||||
subTitle = bottomSheetDialog.findViewById(R.id.videodetail_meta)!!
|
||||
channelName = bottomSheetDialog.findViewById(R.id.videodetail_channel_name)!!
|
||||
channelMeta = bottomSheetDialog.findViewById(R.id.videodetail_channel_meta)!!
|
||||
creatorThumbnail = bottomSheetDialog.findViewById(R.id.creator_thumbnail)!!
|
||||
channelButton = bottomSheetDialog.findViewById(R.id.videodetail_channel_button)!!
|
||||
monetization = bottomSheetDialog.findViewById(R.id.monetization)!!
|
||||
platform = bottomSheetDialog.findViewById(R.id.videodetail_platform)!!
|
||||
layoutRating = bottomSheetDialog.findViewById(R.id.layout_rating)!!
|
||||
textDislikes = bottomSheetDialog.findViewById(R.id.text_dislikes)!!
|
||||
textLikes = bottomSheetDialog.findViewById(R.id.text_likes)!!
|
||||
imageLikeIcon = bottomSheetDialog.findViewById(R.id.image_like_icon)!!
|
||||
imageDislikeIcon = bottomSheetDialog.findViewById(R.id.image_dislike_icon)!!
|
||||
|
||||
description = bottomSheetDialog.findViewById(R.id.videodetail_description)!!
|
||||
descriptionContainer =
|
||||
bottomSheetDialog.findViewById(R.id.videodetail_description_container)!!
|
||||
descriptionViewMore =
|
||||
bottomSheetDialog.findViewById(R.id.videodetail_description_view_more)!!
|
||||
|
||||
addCommentView = bottomSheetDialog.findViewById(R.id.add_comment_view)!!
|
||||
commentsList = bottomSheetDialog.findViewById(R.id.comments_list)!!
|
||||
buttonPolycentric = bottomSheetDialog.findViewById(R.id.button_polycentric)!!
|
||||
buttonPlatform = bottomSheetDialog.findViewById(R.id.button_platform)!!
|
||||
|
||||
commentsList.onAuthorClick.subscribe { c ->
|
||||
if (c !is PolycentricPlatformComment) {
|
||||
return@subscribe
|
||||
}
|
||||
val id = c.author.id.value
|
||||
|
||||
Logger.i(TAG, "onAuthorClick: $id")
|
||||
if (id != null && id.startsWith("polycentric://")) {
|
||||
val navUrl = "https://harbor.social/" + id.substring("polycentric://".length)
|
||||
mainFragment!!.startActivity(Intent(Intent.ACTION_VIEW, navUrl.toUri()))
|
||||
}
|
||||
}
|
||||
commentsList.onRepliesClick.subscribe { c ->
|
||||
val replyCount = c.replyCount ?: 0
|
||||
var metadata = ""
|
||||
if (replyCount > 0) {
|
||||
metadata += "$replyCount " + requireContext().getString(R.string.replies)
|
||||
}
|
||||
|
||||
if (c is PolycentricPlatformComment) {
|
||||
var parentComment: PolycentricPlatformComment = c
|
||||
containerContentReplies.load(tabIndex!! != 0, metadata, c.contextUrl, c.reference, c, { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) }, {
|
||||
val newComment = parentComment.cloneWithUpdatedReplyCount(
|
||||
(parentComment.replyCount ?: 0) + 1
|
||||
)
|
||||
commentsList.replaceComment(parentComment, newComment)
|
||||
parentComment = newComment
|
||||
})
|
||||
} else {
|
||||
containerContentReplies.load(tabIndex!! != 0, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) })
|
||||
}
|
||||
animateOpenOverlayView(containerContentReplies)
|
||||
}
|
||||
|
||||
if (StatePolycentric.instance.enabled) {
|
||||
buttonPolycentric.setOnClickListener {
|
||||
setTabIndex(0)
|
||||
StateMeta.instance.setLastCommentSection(0)
|
||||
}
|
||||
} else {
|
||||
buttonPolycentric.visibility = GONE
|
||||
}
|
||||
|
||||
buttonPlatform.setOnClickListener {
|
||||
setTabIndex(1)
|
||||
StateMeta.instance.setLastCommentSection(1)
|
||||
}
|
||||
|
||||
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||
addCommentView.setContext(video.url, ref)
|
||||
|
||||
if (Settings.instance.comments.recommendationsDefault && !Settings.instance.comments.hideRecommendations) {
|
||||
setTabIndex(2, true)
|
||||
} else {
|
||||
when (Settings.instance.comments.defaultCommentSection) {
|
||||
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(0, true) else setTabIndex(1, true)
|
||||
1 -> setTabIndex(1, true)
|
||||
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
|
||||
}
|
||||
}
|
||||
|
||||
containerContentDescription.onClose.subscribe { animateCloseOverlayView() }
|
||||
containerContentReplies.onClose.subscribe { animateCloseOverlayView() }
|
||||
|
||||
descriptionViewMore.setOnClickListener {
|
||||
animateOpenOverlayView(containerContentDescription)
|
||||
}
|
||||
|
||||
updateDescriptionUI(video.description.fixHtmlLinks())
|
||||
|
||||
val dp5 =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics)
|
||||
val dp2 =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics)
|
||||
|
||||
//UI
|
||||
title.text = video.name
|
||||
channelName.text = video.author.name
|
||||
if (video.author.subscribers != null) {
|
||||
channelMeta.text = if ((video.author.subscribers
|
||||
?: 0) > 0
|
||||
) video.author.subscribers!!.toHumanNumber() + " " + requireContext().getString(R.string.subscribers) else ""
|
||||
(channelName.layoutParams as MarginLayoutParams).setMargins(
|
||||
0, (dp5 * -1).toInt(), 0, 0
|
||||
)
|
||||
} else {
|
||||
channelMeta.text = ""
|
||||
(channelName.layoutParams as MarginLayoutParams).setMargins(0, (dp2).toInt(), 0, 0)
|
||||
}
|
||||
|
||||
video.author.let {
|
||||
if (it is PlatformAuthorMembershipLink && !it.membershipUrl.isNullOrEmpty()) monetization.setPlatformMembership(video.id.pluginId, it.membershipUrl)
|
||||
else monetization.setPlatformMembership(null, null)
|
||||
}
|
||||
|
||||
val subTitleSegments: ArrayList<String> = ArrayList()
|
||||
if (video.viewCount > 0) subTitleSegments.add("${video.viewCount.toHumanNumber()} ${if (video.isLive) requireContext().getString(
|
||||
R.string.watching_now) else requireContext().getString(R.string.views)}")
|
||||
if (video.datetime != null) {
|
||||
val diff = video.datetime?.getNowDiffSeconds() ?: 0
|
||||
val ago = video.datetime?.toHumanNowDiffString(true)
|
||||
if (diff >= 0) subTitleSegments.add("$ago ago")
|
||||
else subTitleSegments.add("available in $ago")
|
||||
}
|
||||
|
||||
platform.setPlatformFromClientID(video.id.pluginId)
|
||||
subTitle.text = subTitleSegments.joinToString(" • ")
|
||||
creatorThumbnail.setThumbnail(video.author.thumbnail, false)
|
||||
|
||||
setPolycentricProfile(null, animate = false)
|
||||
_taskLoadPolycentricProfile.run(video.author.id)
|
||||
|
||||
when (video.rating) {
|
||||
is RatingLikeDislikes -> {
|
||||
val r = video.rating as RatingLikeDislikes
|
||||
layoutRating.visibility = VISIBLE
|
||||
|
||||
textLikes.visibility = VISIBLE
|
||||
imageLikeIcon.visibility = VISIBLE
|
||||
textLikes.text = r.likes.toHumanNumber()
|
||||
|
||||
imageDislikeIcon.visibility = VISIBLE
|
||||
textDislikes.visibility = VISIBLE
|
||||
textDislikes.text = r.dislikes.toHumanNumber()
|
||||
}
|
||||
|
||||
is RatingLikes -> {
|
||||
val r = video.rating as RatingLikes
|
||||
layoutRating.visibility = VISIBLE
|
||||
|
||||
textLikes.visibility = VISIBLE
|
||||
imageLikeIcon.visibility = VISIBLE
|
||||
textLikes.text = r.likes.toHumanNumber()
|
||||
|
||||
imageDislikeIcon.visibility = GONE
|
||||
textDislikes.visibility = GONE
|
||||
}
|
||||
|
||||
else -> {
|
||||
layoutRating.visibility = GONE
|
||||
}
|
||||
}
|
||||
|
||||
monetization.onSupportTap.subscribe {
|
||||
containerContentSupport.setPolycentricProfile(polycentricProfile)
|
||||
animateOpenOverlayView(containerContentSupport)
|
||||
}
|
||||
|
||||
monetization.onStoreTap.subscribe {
|
||||
polycentricProfile?.systemState?.store?.let {
|
||||
try {
|
||||
val uri = it.toUri()
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = uri
|
||||
requireContext().startActivity(intent)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to open URI: '${it}'.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
monetization.onUrlTap.subscribe {
|
||||
mainFragment!!.navigate<BrowserFragment>(it)
|
||||
}
|
||||
|
||||
addCommentView.onCommentAdded.subscribe {
|
||||
commentsList.addComment(it)
|
||||
}
|
||||
|
||||
channelButton.setOnClickListener {
|
||||
mainFragment!!.navigate<ChannelFragment>(video.author)
|
||||
}
|
||||
|
||||
return bottomSheetDialog
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
animateCloseOverlayView()
|
||||
}
|
||||
|
||||
private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
|
||||
polycentricProfile = profile
|
||||
|
||||
val dp35 = 35.dp(requireContext().resources)
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }
|
||||
|
||||
if (avatar != null) {
|
||||
creatorThumbnail.setThumbnail(avatar, animate)
|
||||
} else {
|
||||
creatorThumbnail.setThumbnail(video.author.thumbnail, animate)
|
||||
creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto())
|
||||
}
|
||||
|
||||
val username = profile?.systemState?.username
|
||||
if (username != null) {
|
||||
channelName.text = username
|
||||
}
|
||||
|
||||
monetization.setPolycentricProfile(profile)
|
||||
}
|
||||
|
||||
private fun setTabIndex(index: Int?, forceReload: Boolean = false) {
|
||||
Logger.i(TAG, "setTabIndex (index: ${index}, forceReload: ${forceReload})")
|
||||
val changed = tabIndex != index || forceReload
|
||||
if (!changed) {
|
||||
return
|
||||
}
|
||||
|
||||
tabIndex = index
|
||||
buttonPlatform.setTextColor(resources.getColor(if (index == 1) R.color.white else R.color.gray_ac, null))
|
||||
buttonPolycentric.setTextColor(resources.getColor(if (index == 0) R.color.white else R.color.gray_ac, null))
|
||||
|
||||
when (index) {
|
||||
null -> {
|
||||
addCommentView.visibility = GONE
|
||||
commentsList.clear()
|
||||
}
|
||||
|
||||
0 -> {
|
||||
addCommentView.visibility = VISIBLE
|
||||
fetchPolycentricComments()
|
||||
}
|
||||
|
||||
1 -> {
|
||||
addCommentView.visibility = GONE
|
||||
fetchComments()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchComments() {
|
||||
Logger.i(TAG, "fetchComments")
|
||||
video.let {
|
||||
commentsList.load(true) { StatePlatform.instance.getComments(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchPolycentricComments() {
|
||||
Logger.i(TAG, "fetchPolycentricComments")
|
||||
val video = video
|
||||
val idValue = video.id.value
|
||||
if (video.url.isEmpty()) {
|
||||
Logger.w(TAG, "Failed to fetch polycentric comments because url was null")
|
||||
commentsList.clear()
|
||||
return
|
||||
}
|
||||
|
||||
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||
val extraBytesRef = idValue?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
||||
commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); }
|
||||
}
|
||||
|
||||
private fun updateDescriptionUI(text: Spanned) {
|
||||
containerContentDescription.load(text)
|
||||
description.text = text
|
||||
|
||||
if (description.text.isNotEmpty()) descriptionContainer.visibility = VISIBLE
|
||||
else descriptionContainer.visibility = GONE
|
||||
}
|
||||
|
||||
private fun animateOpenOverlayView(view: View) {
|
||||
if (contentOverlayView != null) {
|
||||
Logger.e(TAG, "Content overlay already open")
|
||||
return
|
||||
}
|
||||
|
||||
behavior.isDraggable = false
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
|
||||
val animHeight = containerContentMain.height
|
||||
|
||||
view.translationY = animHeight.toFloat()
|
||||
view.visibility = VISIBLE
|
||||
|
||||
view.animate().setDuration(300).translationY(0f).withEndAction {
|
||||
contentOverlayView = view
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun animateCloseOverlayView() {
|
||||
val curView = contentOverlayView
|
||||
if (curView == null) {
|
||||
Logger.e(TAG, "No content overlay open")
|
||||
return
|
||||
}
|
||||
|
||||
behavior.isDraggable = true
|
||||
|
||||
val animHeight = contentOverlayView!!.height
|
||||
|
||||
curView.animate().setDuration(300).translationY(animHeight.toFloat()).withEndAction {
|
||||
curView.visibility = GONE
|
||||
contentOverlayView = null
|
||||
}.start()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "ModalBottomSheet"
|
||||
}
|
||||
}
|
||||
@@ -194,17 +194,18 @@ class StateHistory {
|
||||
_remoteHistoryDatesStore.save();
|
||||
}
|
||||
|
||||
fun syncRemoteHistory(plugin: JSClient) {
|
||||
fun syncRemoteHistory(plugin: JSClient): Int {
|
||||
if (plugin.capabilities.hasGetUserHistory &&
|
||||
plugin.isLoggedIn) {
|
||||
Logger.i(TAG, "Syncing remote history for plugin [${plugin.name}]");
|
||||
|
||||
val hist = StatePlatform.instance.getUserHistory(plugin.id);
|
||||
|
||||
syncRemoteHistory(plugin.id, hist, 100, 3);
|
||||
return syncRemoteHistory(plugin.id, hist, 100, 3);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
fun syncRemoteHistory(pluginId: String, videos: IPager<IPlatformContent>, maxVideos: Int, maxPages: Int) {
|
||||
fun syncRemoteHistory(pluginId: String, videos: IPager<IPlatformContent>, maxVideos: Int, maxPages: Int): Int {
|
||||
val lastDate = _remoteHistoryDatesStore.get(pluginId) ?: OffsetDateTime.MIN;
|
||||
val maxVideosCount = if(maxVideos <= 0) 500 else maxVideos;
|
||||
val maxPageCount = if(maxPages <= 0) 3 else maxPages;
|
||||
@@ -272,12 +273,14 @@ class StateHistory {
|
||||
}
|
||||
catch(ex: Throwable){}
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
val plugin = if(pluginId != StateDeveloper.DEV_ID) StatePlugins.instance.getPlugin(pluginId) else null;
|
||||
Logger.e(TAG, "Sync Remote History failed for [${plugin?.config?.name}] due to: " + ex.message)
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -500,7 +500,7 @@ class StatePlatform {
|
||||
.toList()
|
||||
.associateWith { 1f };
|
||||
|
||||
val pager = MultiDistributionContentPager(pages);
|
||||
val pager = MultiDistributionContentPager(pages, 2);
|
||||
pager.initialize();
|
||||
return pager;
|
||||
}
|
||||
|
||||
@@ -179,8 +179,9 @@ class StatePlugins {
|
||||
}
|
||||
|
||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||
StatePlatform.instance.reloadClient(context, id);
|
||||
afterLogin.invoke();
|
||||
StatePlatform.instance.reloadClient(context, id) {
|
||||
afterLogin.invoke();
|
||||
}
|
||||
}
|
||||
};
|
||||
return true;
|
||||
@@ -475,6 +476,7 @@ class StatePlugins {
|
||||
delay(500);
|
||||
|
||||
val client = ManagedHttpClient();
|
||||
client.setTimeout(10000);
|
||||
try {
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress.invoke("Validating script", 0.25);
|
||||
@@ -489,14 +491,14 @@ class StatePlugins {
|
||||
}
|
||||
|
||||
val icon = config.absoluteIconUrl?.let { absIconUrl ->
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress.invoke("Saving plugin", 0.75);
|
||||
}
|
||||
val iconResp = client.get(absIconUrl);
|
||||
if (iconResp.isOk)
|
||||
return@let iconResp.body?.byteStream()?.use { it.readBytes() };
|
||||
return@let null;
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress.invoke("Saving plugin", 0.75);
|
||||
}
|
||||
val installEx = StatePlugins.instance.createPlugin(config, script, icon, true);
|
||||
if (installEx != null)
|
||||
throw installEx;
|
||||
|
||||
@@ -119,6 +119,8 @@ class GestureControlView : LinearLayout {
|
||||
|
||||
var fullScreenGestureEnabled = true
|
||||
|
||||
var controlsEnabled = true
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_gesture_controls, this, true);
|
||||
|
||||
@@ -350,6 +352,10 @@ class GestureControlView : LinearLayout {
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
val ev = event ?: return super.onTouchEvent(event);
|
||||
|
||||
if(!controlsEnabled) {
|
||||
return super.onTouchEvent(ev)
|
||||
}
|
||||
|
||||
if (ev.action == MotionEvent.ACTION_UP && _speedHolding) {
|
||||
_speedHolding = false
|
||||
hideHoldSpeedControls()
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.futo.platformplayer.views.buttons
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
|
||||
class ShortsButton : LinearLayout {
|
||||
private val _root: LinearLayout;
|
||||
private val _icon: ImageView;
|
||||
private val _textPrimary: TextView;
|
||||
val onClick = Event0();
|
||||
|
||||
var iconId: Int? = null;
|
||||
|
||||
constructor(context : Context, text: String, icon: Int, action: ()->Unit) : super(context) {
|
||||
inflate(context, R.layout.view_shorts_button, this);
|
||||
_icon = findViewById(R.id.button_icon);
|
||||
_textPrimary = findViewById(R.id.button_text);
|
||||
_root = findViewById(R.id.root);
|
||||
|
||||
withPrimaryText(text);
|
||||
withIcon(icon);
|
||||
|
||||
_root.apply {
|
||||
isClickable = true;
|
||||
setOnClickListener {
|
||||
if(!isEnabled)
|
||||
return@setOnClickListener;
|
||||
action();
|
||||
onClick.emit();
|
||||
UIDialogs.toast("Clicked button: " + _textPrimary.text);
|
||||
};
|
||||
}
|
||||
}
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
inflate(context, R.layout.view_shorts_button, this);
|
||||
_icon = findViewById(R.id.image_icon);
|
||||
_textPrimary = findViewById(R.id.text_title);
|
||||
_root = findViewById(R.id.root);
|
||||
_root.apply {
|
||||
isClickable = true;
|
||||
setOnClickListener {
|
||||
if(!isEnabled)
|
||||
return@setOnClickListener;
|
||||
onClick.emit();
|
||||
};
|
||||
}
|
||||
|
||||
val attrArr = context.obtainStyledAttributes(attrs, R.styleable.ShortsButton, 0, 0);
|
||||
val attrIconRef = attrArr.getResourceId(R.styleable.ShortsButton_buttonIcon_s, -1);
|
||||
val attrText = attrArr.getText(R.styleable.ShortsButton_buttonText_s) ?: "";
|
||||
attrArr.recycle()
|
||||
|
||||
withIcon(attrIconRef);
|
||||
withPrimaryText(attrText.toString());
|
||||
}
|
||||
|
||||
fun withMargin(bottom: Int, side: Int = 0): ShortsButton {
|
||||
setPadding(side, 0, side, bottom)
|
||||
return this;
|
||||
}
|
||||
fun withPrimaryText(text: String): ShortsButton {
|
||||
_textPrimary.text = text;
|
||||
|
||||
if(text.isNullOrBlank())
|
||||
_textPrimary.visibility = View.GONE;
|
||||
else
|
||||
_textPrimary.visibility = View.VISIBLE;
|
||||
return this;
|
||||
}
|
||||
|
||||
fun withIcon(resourceId: Int): ShortsButton {
|
||||
if (resourceId != -1) {
|
||||
_icon.visibility = View.VISIBLE;
|
||||
_icon.setImageResource(resourceId);
|
||||
} else
|
||||
_icon.visibility = View.GONE;
|
||||
_icon.scaleType = ImageView.ScaleType.CENTER_CROP;
|
||||
iconId = resourceId;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
fun withIcon(bitmap: Bitmap): ShortsButton {
|
||||
_icon.visibility = View.VISIBLE;
|
||||
_icon.setImageBitmap(bitmap);
|
||||
iconId = -1;
|
||||
|
||||
_icon.scaleType = ImageView.ScaleType.CENTER_CROP;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
fun setButtonEnabled(enabled: Boolean) {
|
||||
if(enabled) {
|
||||
alpha = 1f;
|
||||
isEnabled = true;
|
||||
isClickable = true;
|
||||
}
|
||||
else {
|
||||
alpha = 0.5f;
|
||||
isEnabled = false;
|
||||
isClickable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.futo.platformplayer.views.containers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import com.futo.platformplayer.R
|
||||
import kotlin.math.abs
|
||||
|
||||
class CustomMotionLayout(context: Context, attributeSet: AttributeSet? = null) :
|
||||
MotionLayout(context, attributeSet) {
|
||||
|
||||
private val viewToDetectTouch by lazy {
|
||||
findViewById<View>(R.id.layout_player_container) //TODO move to Attributes
|
||||
}
|
||||
private val viewToDetectTouch2 by lazy {
|
||||
findViewById<View>(R.id.minimize_controls) //TODO move to Attributes
|
||||
}
|
||||
|
||||
private var savedActionDown: MotionEvent? = null
|
||||
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
||||
|
||||
// intercepting touch events is necessary because something to do with PlayerControlView makes things not work
|
||||
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
|
||||
val ev = event ?: return super.onInterceptTouchEvent(null)
|
||||
|
||||
// special touch interception logic is unnecessary if interaction is disabled
|
||||
if (!isInteractionEnabled) {
|
||||
return super.onInterceptTouchEvent(ev)
|
||||
}
|
||||
|
||||
when (ev.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
val viewRect = Rect()
|
||||
viewToDetectTouch.getHitRect(viewRect)
|
||||
val isInView = viewRect.contains(ev.x.toInt(), ev.y.toInt())
|
||||
viewToDetectTouch2.getHitRect(viewRect)
|
||||
val isInView2 = viewRect.contains(ev.x.toInt(), ev.y.toInt())
|
||||
|
||||
// Don't intercept touches if they are outside of the player or the mini player controls
|
||||
if (!isInView && !isInView2) {
|
||||
return false
|
||||
}
|
||||
|
||||
val thing = super.onInterceptTouchEvent(ev)
|
||||
// If the MotionLayout is already intercepting this touch then don't track it
|
||||
if (thing) {
|
||||
return true
|
||||
}
|
||||
|
||||
// MotionLayout didn't intercept the touch but the touch is over the player/mini controls views
|
||||
// in the future the class will
|
||||
// save the touch event for later
|
||||
// need to replay this initial touch to the MotionLayout if it ends up turning into a drag
|
||||
// return false because that matches the return from the super call above
|
||||
savedActionDown?.recycle() // Recycle the old event to prevent memory leaks (if for some reason it wasn't cleaned up in the other code paths)
|
||||
savedActionDown = MotionEvent.obtain(ev)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val localSavedActionDown = savedActionDown
|
||||
|
||||
// only handle the move event if there is a saved action stored
|
||||
// then check to see if it has turned into a drag
|
||||
if (localSavedActionDown != null) {
|
||||
val dy = abs(ev.y - localSavedActionDown.y)
|
||||
if (dy > touchSlop) {
|
||||
// if it has turned into a drag then
|
||||
// replay the down action saved earlier
|
||||
// clean up our data
|
||||
// return true so that the MotionLayout's onTouchEvent will receive future events for this gesture
|
||||
//
|
||||
// it is necessary to replay the down action because otherwise MotionLayout will not always initialize the drag correctly
|
||||
super.onTouchEvent(localSavedActionDown)
|
||||
localSavedActionDown.recycle() // Clean up the saved event after replaying
|
||||
savedActionDown = null
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if it's an up or cancel action clean up our tracking
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
savedActionDown?.recycle()
|
||||
savedActionDown = null
|
||||
}
|
||||
}
|
||||
|
||||
// since the function hasn't handled the even this far send it to the parent class
|
||||
return super.onInterceptTouchEvent(ev)
|
||||
}
|
||||
|
||||
// onTouchEvent is necessary to make sure that only touch and drag on the video triggers the animation (instead of everywhere on the screen)
|
||||
@SuppressLint("ClickableViewAccessibility") // pretty sure this issue doesn't apply
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
val ev = event ?: return super.onTouchEvent(null)
|
||||
|
||||
// special touch event handling logic is unnecessary if interaction is disabled
|
||||
if (!isInteractionEnabled) {
|
||||
return super.onTouchEvent(ev)
|
||||
}
|
||||
|
||||
val viewRect = Rect()
|
||||
viewToDetectTouch.getHitRect(viewRect)
|
||||
val isInView = viewRect.contains(ev.x.toInt(), ev.y.toInt())
|
||||
viewToDetectTouch2.getHitRect(viewRect)
|
||||
val isInView2 = viewRect.contains(ev.x.toInt(), ev.y.toInt())
|
||||
|
||||
// don't want to handle touches outside of the player/mini controls views
|
||||
if ((!isInView && !isInView2) && event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
return false
|
||||
}
|
||||
return super.onTouchEvent(event)
|
||||
}
|
||||
}
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
package com.futo.platformplayer.views.containers
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import com.futo.platformplayer.R
|
||||
|
||||
class SingleViewTouchableMotionLayout(context: Context, attributeSet: AttributeSet? = null) : MotionLayout(context, attributeSet) {
|
||||
|
||||
private val viewToDetectTouch by lazy {
|
||||
findViewById<View>(R.id.touchContainer) //TODO move to Attributes
|
||||
}
|
||||
private val viewRect = Rect()
|
||||
private var touchStarted = false
|
||||
private val transitionListenerList = mutableListOf<TransitionListener?>()
|
||||
|
||||
var allowMotion : Boolean = true;
|
||||
|
||||
init {
|
||||
addTransitionListener(object : TransitionListener {
|
||||
override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {
|
||||
}
|
||||
|
||||
override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {
|
||||
touchStarted = false
|
||||
}
|
||||
|
||||
override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) {
|
||||
}
|
||||
|
||||
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) {
|
||||
}
|
||||
})
|
||||
|
||||
super.setTransitionListener(object : TransitionListener {
|
||||
override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {
|
||||
transitionListenerList.filterNotNull()
|
||||
.forEach { it.onTransitionChange(p0, p1, p2, p3) }
|
||||
}
|
||||
|
||||
override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {
|
||||
transitionListenerList.filterNotNull()
|
||||
.forEach { it.onTransitionCompleted(p0, p1) }
|
||||
}
|
||||
|
||||
override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) {
|
||||
}
|
||||
|
||||
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) {
|
||||
}
|
||||
})
|
||||
|
||||
//isInteractionEnabled = false;
|
||||
}
|
||||
|
||||
override fun setTransitionListener(listener: TransitionListener?) {
|
||||
addTransitionListener(listener)
|
||||
}
|
||||
|
||||
override fun addTransitionListener(listener: TransitionListener?) {
|
||||
transitionListenerList += listener
|
||||
}
|
||||
|
||||
//This always triggers, workaround calling super.onTouchEvent
|
||||
//Blocks click events underneath
|
||||
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
|
||||
if(!allowMotion)
|
||||
return false;
|
||||
if(event != null) {
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
touchStarted = false
|
||||
return super.onTouchEvent(event) && false;
|
||||
}
|
||||
}
|
||||
if (!touchStarted) {
|
||||
viewToDetectTouch.getHitRect(viewRect);
|
||||
val isInView = viewRect.contains(event.x.toInt(), event.y.toInt());
|
||||
touchStarted = isInView
|
||||
}
|
||||
}
|
||||
return touchStarted && super.onTouchEvent(event) && false;
|
||||
}
|
||||
|
||||
|
||||
//Not triggered on its own due to child views, intercept is used instead.
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.getDataLinkFromUrl
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.views.IdenticonView
|
||||
import userpackage.Protocol
|
||||
|
||||
@@ -82,14 +83,14 @@ class CreatorThumbnail : ConstraintLayout {
|
||||
Glide.with(_imageChannelThumbnail)
|
||||
.load(url)
|
||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
|
||||
.crossfade()
|
||||
.into(_imageChannelThumbnail);
|
||||
.into(_imageChannelThumbnail)
|
||||
} else {
|
||||
Glide.with(_imageChannelThumbnail)
|
||||
.load(url)
|
||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
|
||||
.into(_imageChannelThumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.animation.LinearInterpolator
|
||||
import androidx.annotation.Dimension
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.Player
|
||||
@@ -65,6 +66,8 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) :
|
||||
videoView = findViewById(R.id.short_player_view)
|
||||
progressBar = findViewById(R.id.short_player_progress_bar)
|
||||
|
||||
videoView.subtitleView?.setFixedTextSize(Dimension.SP, 18F);
|
||||
|
||||
if (!isInEditMode) {
|
||||
player = StatePlayer.instance.getShortPlayerOrCreate(context)
|
||||
player.player.repeatMode = Player.REPEAT_MODE_ONE
|
||||
|
||||
@@ -125,7 +125,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
private var _lastSourceFit: Float? = null;
|
||||
private var _lastWindowWidth: Int = resources.configuration.screenWidthDp
|
||||
private var _lastWindowHeight: Int = resources.configuration.screenHeightDp
|
||||
private var _originalBottomMargin: Int = 0;
|
||||
|
||||
private var _isControlsLocked: Boolean = false;
|
||||
|
||||
@@ -154,10 +153,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
val onSourceEnded = Event0();
|
||||
val onPrevious = Event0();
|
||||
val onNext = Event0();
|
||||
|
||||
val onChapterChanged = Event2<IChapter?, Boolean>();
|
||||
|
||||
val onVideoClicked = Event0();
|
||||
val onTimeBarChanged = Event2<Long, Long>();
|
||||
|
||||
val onChapterClicked = Event1<IChapter>();
|
||||
@@ -650,13 +646,10 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
|
||||
if (fullScreen) {
|
||||
val lp = background.layoutParams as ConstraintLayout.LayoutParams;
|
||||
lp.bottomMargin = 0;
|
||||
background.layoutParams = lp;
|
||||
_videoView.setPadding(_videoView.paddingLeft, _videoView.paddingTop, _videoView.paddingRight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0.0f, Resources.getSystem().displayMetrics).toInt())
|
||||
_videoView.setBackgroundColor(Color.parseColor("#FF000000"))
|
||||
|
||||
gestureControl.hideControls();
|
||||
//videoControlsBar.visibility = View.GONE;
|
||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||
|
||||
_videoControls_fullscreen.show();
|
||||
@@ -664,13 +657,10 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
videoControls.visibility = View.GONE;
|
||||
}
|
||||
else {
|
||||
val lp = background.layoutParams as ConstraintLayout.LayoutParams;
|
||||
lp.bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, Resources.getSystem().displayMetrics).toInt();
|
||||
background.layoutParams = lp;
|
||||
_videoView.setPadding(_videoView.paddingLeft, _videoView.paddingTop, _videoView.paddingRight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7.0f, Resources.getSystem().displayMetrics).toInt())
|
||||
_videoView.setBackgroundColor(Color.parseColor("#00000000"))
|
||||
|
||||
gestureControl.hideControls();
|
||||
//videoControlsBar.visibility = View.VISIBLE;
|
||||
_videoView.resizeMode = _desiredResizeModePortrait;
|
||||
|
||||
videoControls.show();
|
||||
@@ -701,10 +691,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
_isControlsLocked = locked;
|
||||
}
|
||||
|
||||
override fun play() {
|
||||
super.play();
|
||||
}
|
||||
|
||||
override fun onVideoSizeChanged(videoSize: VideoSize) {
|
||||
gestureControl.resetZoomPan()
|
||||
_lastSourceFit = null;
|
||||
@@ -774,11 +760,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
@OptIn(UnstableApi::class)
|
||||
fun fitHeight(videoSize: VideoSize? = null) {
|
||||
Logger.i(TAG, "Video Fit Height")
|
||||
if (_originalBottomMargin != 0) {
|
||||
val layoutParams = _videoView.layoutParams as ConstraintLayout.LayoutParams
|
||||
layoutParams.setMargins(0, 0, 0, _originalBottomMargin)
|
||||
_videoView.layoutParams = layoutParams
|
||||
}
|
||||
|
||||
var h = videoSize?.height ?: lastVideoSource?.height ?: exoPlayer?.player?.videoSize?.height
|
||||
?: 0
|
||||
@@ -819,15 +800,13 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
_videoView.resizeMode = _desiredResizeModePortrait
|
||||
|
||||
val marginBottom =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7f, resources.displayMetrics)
|
||||
val height = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
_lastSourceFit!!,
|
||||
resources.displayMetrics
|
||||
)
|
||||
val rootParams = LayoutParams(LayoutParams.MATCH_PARENT, (height + marginBottom).toInt())
|
||||
rootParams.bottomMargin = marginBottom.toInt()
|
||||
_videoView.setPadding(_videoView.paddingLeft, _videoView.paddingTop, _videoView.paddingRight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7.0f, Resources.getSystem().displayMetrics).toInt())
|
||||
val rootParams = LayoutParams(LayoutParams.MATCH_PARENT, (height).toInt())
|
||||
_root.layoutParams = rootParams
|
||||
isFitMode = true
|
||||
}
|
||||
@@ -835,9 +814,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
@OptIn(UnstableApi::class)
|
||||
fun fillHeight(isMiniPlayer: Boolean) {
|
||||
Logger.i(TAG, "Video Fill Height");
|
||||
val layoutParams = _videoView.layoutParams as ConstraintLayout.LayoutParams;
|
||||
_originalBottomMargin =
|
||||
if (layoutParams.bottomMargin > 0) layoutParams.bottomMargin else _originalBottomMargin;
|
||||
var layoutParams = _videoView.layoutParams as ConstraintLayout.LayoutParams;
|
||||
layoutParams.setMargins(0);
|
||||
_videoView.layoutParams = layoutParams;
|
||||
_videoView.invalidate();
|
||||
@@ -848,16 +825,14 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
if(isMiniPlayer){
|
||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
||||
_videoView.setPadding(_videoView.paddingLeft, _videoView.paddingTop, _videoView.paddingRight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7.0f, Resources.getSystem().displayMetrics).toInt())
|
||||
} else {
|
||||
_videoView.setPadding(_videoView.paddingLeft, _videoView.paddingTop, _videoView.paddingRight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0.0f, Resources.getSystem().displayMetrics).toInt())
|
||||
}
|
||||
|
||||
isFitMode = false;
|
||||
}
|
||||
|
||||
//Animated Calls
|
||||
fun setEndPadding(value: Float) {
|
||||
setPadding(0, 0, value.toInt(), 0)
|
||||
}
|
||||
|
||||
fun updateRotateLock() {
|
||||
_control_rotate_lock.visibility = View.VISIBLE;
|
||||
_control_rotate_lock_fullscreen.visibility = View.VISIBLE;
|
||||
@@ -907,11 +882,14 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
override fun switchToVideoMode() {
|
||||
super.switchToVideoMode()
|
||||
setArtwork(null)
|
||||
//setArtwork(null)
|
||||
}
|
||||
|
||||
override fun switchToAudioMode(video: IPlatformVideoDetails?) {
|
||||
super.switchToAudioMode(video)
|
||||
|
||||
//This causes issues, and is in general confusing, needs improvements
|
||||
/*
|
||||
val thumbnail = video?.thumbnails?.getHQThumbnail()
|
||||
if (!thumbnail.isNullOrBlank()) {
|
||||
Glide.with(context).asBitmap().load(thumbnail)
|
||||
@@ -928,5 +906,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
})
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -873,7 +873,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun loadSelectedSources(play: Boolean, resume: Boolean): Boolean {
|
||||
val sourceVideo = _lastVideoMediaSource
|
||||
val sourceVideo = if(!isAudioMode || _lastAudioMediaSource == null) _lastVideoMediaSource else null;
|
||||
val sourceAudio = _lastAudioMediaSource;
|
||||
val sourceSubs = _lastSubtitleMediaSource;
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:strokeColor="#222"
|
||||
android:strokeWidth="20"
|
||||
android:pathData="M240,560L720,560L720,480L240,480L240,560ZM240,440L720,440L720,360L240,360L240,440ZM240,320L720,320L720,240L240,240L240,320ZM880,880L720,720L160,720Q127,720 103.5,696.5Q80,673 80,640L80,160Q80,127 103.5,103.5Q127,80 160,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,880ZM160,640L754,640L800,685L800,160Q800,160 800,160Q800,160 800,160L160,160Q160,160 160,160Q160,160 160,160L160,640Q160,640 160,640Q160,640 160,640ZM160,640Q160,640 160,640Q160,640 160,640L160,160Q160,160 160,160Q160,160 160,160L160,160Q160,160 160,160Q160,160 160,160L160,640Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:strokeColor="#222"
|
||||
android:strokeWidth="20"
|
||||
android:pathData="M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM440,800L519,800L533,694Q564,686 590.5,670.5Q617,655 639,633L738,674L777,606L691,541Q696,527 698,511.5Q700,496 700,480Q700,464 698,448.5Q696,433 691,419L777,354L738,286L639,328Q617,305 590.5,289.5Q564,274 533,266L520,160L441,160L427,266Q396,274 369.5,289.5Q343,305 321,327L222,286L183,354L269,418Q264,433 262,448Q260,463 260,480Q260,496 262,511Q264,526 269,541L183,606L222,674L321,632Q343,655 369.5,670.5Q396,686 427,694L440,800ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620ZM480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:strokeColor="#222"
|
||||
android:strokeWidth="20"
|
||||
android:pathData="M680,880Q630,880 595,845Q560,810 560,760Q560,754 563,732L282,568Q266,583 245,591.5Q224,600 200,600Q150,600 115,565Q80,530 80,480Q80,430 115,395Q150,360 200,360Q224,360 245,368.5Q266,377 282,392L563,228Q561,221 560.5,214.5Q560,208 560,200Q560,150 595,115Q630,80 680,80Q730,80 765,115Q800,150 800,200Q800,250 765,285Q730,320 680,320Q656,320 635,311.5Q614,303 598,288L317,452Q319,459 319.5,465.5Q320,472 320,480Q320,488 319.5,494.5Q319,501 317,508L598,672Q614,657 635,648.5Q656,640 680,640Q730,640 765,675Q800,710 800,760Q800,810 765,845Q730,880 680,880ZM680,800Q697,800 708.5,788.5Q720,777 720,760Q720,743 708.5,731.5Q697,720 680,720Q663,720 651.5,731.5Q640,743 640,760Q640,777 651.5,788.5Q663,800 680,800ZM200,520Q217,520 228.5,508.5Q240,497 240,480Q240,463 228.5,451.5Q217,440 200,440Q183,440 171.5,451.5Q160,463 160,480Q160,497 171.5,508.5Q183,520 200,520ZM680,240Q697,240 708.5,228.5Q720,217 720,200Q720,183 708.5,171.5Q697,160 680,160Q663,160 651.5,171.5Q640,183 640,200Q640,217 651.5,228.5Q663,240 680,240ZM680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760ZM200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480ZM680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:strokeColor="#222"
|
||||
android:strokeWidth="20"
|
||||
android:pathData="M240,120L680,120L680,640L400,920L350,870Q343,863 338.5,851Q334,839 334,828L334,814L378,640L120,640Q88,640 64,616Q40,592 40,560L40,480Q40,473 42,465Q44,457 46,450L166,168Q175,148 196,134Q217,120 240,120ZM600,200L240,200Q240,200 240,200Q240,200 240,200L120,480L120,560Q120,560 120,560Q120,560 120,560L480,560L426,780L600,606L600,200ZM600,606L600,606L600,560L600,560Q600,560 600,560Q600,560 600,560L600,480L600,200Q600,200 600,200Q600,200 600,200L600,200L600,606ZM680,640L680,560L800,560L800,200L680,200L680,120L880,120L880,640L680,640Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@color/colorPrimary"
|
||||
android:pathData="M240,120L640,120L640,640L360,920L310,870Q303,863 298.5,851Q294,839 294,828L294,814L338,640L120,640Q88,640 64,616Q40,592 40,560L40,480Q40,473 41.5,465Q43,457 46,450L166,168Q175,148 196,134Q217,120 240,120ZM720,640L720,120L880,120L880,640L720,640Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:strokeColor="#222"
|
||||
android:strokeWidth="20"
|
||||
android:pathData="M720,840L280,840L280,320L560,40L610,90Q617,97 621.5,109Q626,121 626,132L626,146L582,320L840,320Q872,320 896,344Q920,368 920,400L920,480Q920,487 918,495Q916,503 914,510L794,792Q785,812 764,826Q743,840 720,840ZM360,760L720,760Q720,760 720,760Q720,760 720,760L840,480L840,400Q840,400 840,400Q840,400 840,400L480,400L534,180L360,354L360,760ZM360,354L360,354L360,400L360,400Q360,400 360,400Q360,400 360,400L360,480L360,760Q360,760 360,760Q360,760 360,760L360,760L360,354ZM280,320L280,400L160,400L160,760L280,760L280,840L80,840L80,320L280,320Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@color/colorPrimary"
|
||||
android:pathData="M720,840L320,840L320,320L600,40L650,90Q657,97 661.5,109Q666,121 666,132L666,146L622,320L840,320Q872,320 896,344Q920,368 920,400L920,480Q920,487 918.5,495Q917,503 914,510L794,792Q785,812 764,826Q743,840 720,840ZM240,320L240,840L80,840L80,320L240,320Z"/>
|
||||
</vector>
|
||||
@@ -55,7 +55,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:layout="@layout/fragview_video_detail"
|
||||
android:elevation="15dp"
|
||||
android:visibility="invisible" />
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
@@ -94,10 +94,13 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<!-- TODO: the padding for the recycler view needs to be the same as the minimized video player and perhaps should be dynamic based on whether the player is mini or closed-->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list_results"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="80dp"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:background="@color/transparent"
|
||||
app:layoutDescription="@xml/videodetail_scene"
|
||||
app:layout_collapseMode="parallax">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/touchContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="220dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:background="#222222" />
|
||||
|
||||
<com.futo.platformplayer.fragment.mainactivity.main.VideoDetailView
|
||||
android:id="@+id/fragview_videodetail"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/touchContainer"
|
||||
app:layout_constraintEnd_toEndOf="@+id/touchContainer"
|
||||
app:layout_constraintStart_toStartOf="@+id/touchContainer"
|
||||
app:layout_constraintTop_toTopOf="@+id/touchContainer"
|
||||
android:nestedScrollingEnabled="false" />
|
||||
|
||||
</com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout>
|
||||
@@ -1,24 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.futo.platformplayer.views.containers.CustomMotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:fitsSystemWindows="false"
|
||||
android:background="@drawable/bottom_menu_border"
|
||||
android:background="@color/transparent"
|
||||
android:id="@+id/videodetail_root"
|
||||
android:clickable="true">
|
||||
app:layoutDescription="@xml/videodetail_scene">
|
||||
|
||||
<com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
|
||||
<FrameLayout
|
||||
android:id="@+id/layout_player_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="6dp"
|
||||
android:elevation="2dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp">
|
||||
|
||||
<!--this acts as a background-->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:cardBackgroundColor="@color/black"
|
||||
android:translationY="-7dp"/>
|
||||
|
||||
<com.futo.platformplayer.views.video.FutoVideoPlayer
|
||||
android:id="@+id/videodetail_player"
|
||||
@@ -38,39 +40,31 @@
|
||||
android:visibility="gone"
|
||||
android:elevation="4dp"
|
||||
android:layout_marginBottom="6dp" />
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.media3.ui.PlayerControlView
|
||||
android:id="@+id/videodetail_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="12dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginLeft="-6dp"
|
||||
android:layout_marginRight="-6dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
app:show_timeout="-1"
|
||||
android:elevation="2dp"
|
||||
app:controller_layout_id="@layout/video_player_ui_bar" />
|
||||
</com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout>
|
||||
<androidx.media3.ui.PlayerControlView
|
||||
android:id="@+id/videodetail_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:show_timeout="-1"
|
||||
android:elevation="1dp"
|
||||
android:background="@color/black"
|
||||
app:controller_layout_id="@layout/video_player_ui_bar" />
|
||||
|
||||
<!--Minimized Controls-->
|
||||
<LinearLayout
|
||||
android:id="@+id/minimize_controls"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_height="0dp"
|
||||
android:paddingBottom="7dp"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="10dp"
|
||||
android:clickable="false"
|
||||
android:elevation="5dp"
|
||||
android:alpha="1"
|
||||
app:layout_constraintTop_toTopOf="@id/layout_player_container"
|
||||
app:layout_constraintBottom_toBottomOf="@id/layout_player_container"
|
||||
app:layout_constraintEnd_toEndOf="@id/layout_player_container"
|
||||
app:layout_constraintWidth_percent="0.7">
|
||||
android:background="@color/black"
|
||||
android:orientation="horizontal">
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:orientation="vertical">
|
||||
<!--Video Title-->
|
||||
@@ -180,16 +174,8 @@
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/contentContainer"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="-18dp"
|
||||
android:elevation="1dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_player_container"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp">
|
||||
<FrameLayout
|
||||
android:id="@+id/videodetail_container_main"
|
||||
android:layout_width="match_parent"
|
||||
@@ -394,19 +380,23 @@
|
||||
android:id="@+id/videodetail_rating"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/buttons_pins"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_marginStart="15dp" />
|
||||
android:layout_marginStart="15dp"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"/>
|
||||
|
||||
<com.futo.platformplayer.views.pills.RoundButtonGroup
|
||||
android:id="@+id/buttons_pins"
|
||||
app:layout_constraintLeft_toRightOf="@id/videodetail_rating"
|
||||
app:layout_constraintStart_toEndOf="@id/videodetail_rating"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_marginLeft="10dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content" />
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintWidth_max="500dp"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
@@ -611,15 +601,13 @@
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/videodetail_quality_overview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:elevation="100dp"
|
||||
android:visibility="gone" />
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/overlay_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:elevation="100dp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.futo.platformplayer.views.containers.CustomMotionLayout>
|
||||
@@ -7,8 +7,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="bottom"
|
||||
android:layoutDirection="ltr"
|
||||
android:orientation="vertical"
|
||||
tools:targetApi="28">
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_minimize"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layoutDirection="ltr"
|
||||
android:orientation="vertical">
|
||||
@@ -10,7 +10,7 @@
|
||||
<com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
|
||||
android:id="@+id/layout_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="12dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:shouldInterceptTouches="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
android:background="@color/transparent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<androidx.media3.ui.PlayerView
|
||||
android:paddingBottom="7dp"
|
||||
android:id="@+id/video_player"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:default_artwork="@drawable/placeholder_video_thumbnail"
|
||||
app:use_artwork="true"
|
||||
app:use_controller="false"
|
||||
app:show_buffering="always"
|
||||
android:layout_marginBottom="6dp" />
|
||||
app:show_buffering="always"/>
|
||||
<!--
|
||||
<androidx.media3.ui.PlayerControlView
|
||||
android:id="@+id/video_player_bar"
|
||||
@@ -28,8 +28,7 @@
|
||||
android:id="@+id/layout_controls_background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#55000000"
|
||||
android:layout_marginBottom="6dp">
|
||||
android:background="#55000000">
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
@@ -71,4 +70,4 @@
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -129,6 +129,19 @@
|
||||
android:text=""
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp" />
|
||||
<TextView
|
||||
android:id="@+id/video_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:shadowColor="@android:color/black"
|
||||
android:shadowRadius="8"
|
||||
android:text=""
|
||||
android:textColor="#CCC"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Buttons section -->
|
||||
@@ -143,341 +156,88 @@
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<!-- Like button -->
|
||||
<FrameLayout
|
||||
android:id="@+id/like_container"
|
||||
<com.futo.platformplayer.views.buttons.ShortsButton
|
||||
android:id="@+id/like_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/button_shadow"
|
||||
app:layout_constraintBottom_toBottomOf="@id/like_button"
|
||||
app:layout_constraintEnd_toEndOf="@id/like_button"
|
||||
app:layout_constraintStart_toStartOf="@id/like_button"
|
||||
app:layout_constraintTop_toTopOf="@id/like_button"
|
||||
app:tint="@color/black"
|
||||
tools:ignore="ImageContrastCheck" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/like_button"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:checkable="true"
|
||||
android:contentDescription="@string/cd_image_like_icon"
|
||||
app:backgroundTint="@color/transparent"
|
||||
app:icon="@drawable/thumb_up_selector"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@android:color/white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:rippleColor="@color/ripple"
|
||||
app:toggleCheckedStateOnClick="false" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/like_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:shadowColor="@android:color/black"
|
||||
android:shadowRadius="8"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
</FrameLayout>
|
||||
android:layout_marginBottom="10dp"
|
||||
android:checkable="true"
|
||||
android:contentDescription="@string/cd_image_like_icon"
|
||||
app:backgroundTint="@color/transparent"
|
||||
app:buttonIcon_s="@drawable/ic_thumb_up_s"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@android:color/white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:rippleColor="@color/ripple"
|
||||
app:toggleCheckedStateOnClick="false" />
|
||||
|
||||
<!-- Dislike button -->
|
||||
<FrameLayout
|
||||
android:id="@+id/dislike_container"
|
||||
<com.futo.platformplayer.views.buttons.ShortsButton
|
||||
android:id="@+id/dislike_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/button_shadow"
|
||||
app:layout_constraintBottom_toBottomOf="@id/dislike_button"
|
||||
app:layout_constraintEnd_toEndOf="@id/dislike_button"
|
||||
app:layout_constraintStart_toStartOf="@id/dislike_button"
|
||||
app:layout_constraintTop_toTopOf="@id/dislike_button"
|
||||
app:tint="@color/black"
|
||||
tools:ignore="ImageContrastCheck" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/dislike_button"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:checkable="true"
|
||||
android:contentDescription="@string/cd_image_dislike_icon"
|
||||
app:backgroundTint="@color/transparent"
|
||||
app:icon="@drawable/thumb_down_selector"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@android:color/white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:rippleColor="@color/ripple"
|
||||
app:toggleCheckedStateOnClick="false" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dislike_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:shadowColor="@android:color/black"
|
||||
android:shadowRadius="8"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
</FrameLayout>
|
||||
android:layout_marginBottom="20dp"
|
||||
android:checkable="true"
|
||||
android:contentDescription="@string/cd_image_dislike_icon"
|
||||
app:backgroundTint="@color/transparent"
|
||||
app:buttonIcon_s="@drawable/ic_thumb_down_s"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@android:color/white"
|
||||
app:rippleColor="@color/ripple"
|
||||
app:toggleCheckedStateOnClick="false" />
|
||||
|
||||
<!-- Comments button -->
|
||||
<FrameLayout
|
||||
<com.futo.platformplayer.views.buttons.ShortsButton
|
||||
android:id="@+id/comments_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/button_shadow"
|
||||
app:layout_constraintBottom_toBottomOf="@id/comments_button"
|
||||
app:layout_constraintEnd_toEndOf="@id/comments_button"
|
||||
app:layout_constraintStart_toStartOf="@id/comments_button"
|
||||
app:layout_constraintTop_toTopOf="@id/comments_button"
|
||||
app:tint="@color/black"
|
||||
tools:ignore="ImageContrastCheck" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/comments_button"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:contentDescription="@string/comments"
|
||||
app:icon="@drawable/desktop_comments"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@android:color/white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:rippleColor="@color/ripple" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:importantForAccessibility="no"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:shadowColor="@android:color/black"
|
||||
android:shadowRadius="8"
|
||||
android:text="@string/comments"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
tools:ignore="TextContrastCheck" />
|
||||
</FrameLayout>
|
||||
android:layout_marginBottom="20dp"
|
||||
android:contentDescription="@string/comments"
|
||||
app:buttonIcon_s="@drawable/ic_comment_s"
|
||||
app:buttonText_s=""
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@android:color/white"
|
||||
app:rippleColor="@color/ripple" />
|
||||
|
||||
<!-- Share button -->
|
||||
<FrameLayout
|
||||
<com.futo.platformplayer.views.buttons.ShortsButton
|
||||
android:id="@+id/share_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/button_shadow"
|
||||
app:layout_constraintBottom_toBottomOf="@id/share_button"
|
||||
app:layout_constraintEnd_toEndOf="@id/share_button"
|
||||
app:layout_constraintStart_toStartOf="@id/share_button"
|
||||
app:layout_constraintTop_toTopOf="@id/share_button"
|
||||
app:tint="@color/black"
|
||||
tools:ignore="ImageContrastCheck" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/share_button"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:contentDescription="@string/share"
|
||||
app:icon="@drawable/desktop_share"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@android:color/white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:rippleColor="@color/ripple" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:importantForAccessibility="no"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:shadowColor="@android:color/black"
|
||||
android:shadowRadius="8"
|
||||
android:text="@string/share"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
tools:ignore="TextContrastCheck" />
|
||||
</FrameLayout>
|
||||
android:layout_marginBottom="20dp"
|
||||
android:contentDescription="@string/share"
|
||||
app:buttonIcon_s="@drawable/ic_share_s"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@android:color/white"
|
||||
app:rippleColor="@color/ripple" />
|
||||
|
||||
<!-- Refresh button -->
|
||||
<FrameLayout
|
||||
android:id="@+id/refresh_button_container"
|
||||
<com.futo.platformplayer.views.buttons.ShortsButton
|
||||
android:id="@+id/refresh_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/button_shadow"
|
||||
app:layout_constraintBottom_toBottomOf="@id/refresh_button"
|
||||
app:layout_constraintEnd_toEndOf="@id/refresh_button"
|
||||
app:layout_constraintStart_toStartOf="@id/refresh_button"
|
||||
app:layout_constraintTop_toTopOf="@id/refresh_button"
|
||||
app:tint="@color/black"
|
||||
tools:ignore="ImageContrastCheck" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/refresh_button"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:contentDescription="@string/refresh"
|
||||
app:icon="@drawable/desktop_refresh"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@android:color/white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:rippleColor="@color/ripple" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:importantForAccessibility="no"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:shadowColor="@android:color/black"
|
||||
android:shadowRadius="8"
|
||||
android:text="@string/refresh"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
tools:ignore="TextContrastCheck" />
|
||||
</FrameLayout>
|
||||
android:layout_marginBottom="20dp"
|
||||
android:contentDescription="@string/refresh"
|
||||
app:buttonIcon_s="@drawable/ic_refresh"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@android:color/white"
|
||||
app:rippleColor="@color/ripple" />
|
||||
|
||||
<!-- Quality/More button -->
|
||||
<FrameLayout
|
||||
<com.futo.platformplayer.views.buttons.ShortsButton
|
||||
android:id="@+id/quality_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/button_shadow"
|
||||
app:layout_constraintBottom_toBottomOf="@id/quality_button"
|
||||
app:layout_constraintEnd_toEndOf="@id/quality_button"
|
||||
app:layout_constraintStart_toStartOf="@id/quality_button"
|
||||
app:layout_constraintTop_toTopOf="@id/quality_button"
|
||||
app:tint="@color/black"
|
||||
tools:ignore="ImageContrastCheck" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/quality_button"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:contentDescription="@string/quality"
|
||||
app:icon="@drawable/desktop_gear"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@android:color/white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:rippleColor="@color/ripple" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:importantForAccessibility="no"
|
||||
android:paddingHorizontal="4dp"
|
||||
android:shadowColor="@android:color/black"
|
||||
android:shadowRadius="8"
|
||||
android:text="@string/quality"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
tools:ignore="TextContrastCheck" />
|
||||
</FrameLayout>
|
||||
android:layout_marginBottom="10dp"
|
||||
android:contentDescription="@string/quality"
|
||||
app:buttonIcon_s="@drawable/ic_settings_s"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="@android:color/white"
|
||||
app:rippleColor="@color/ripple" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
android:layout_above="@+id/short_player_progress_bar"
|
||||
android:background="@color/black"
|
||||
app:default_artwork="@drawable/placeholder_video_thumbnail"
|
||||
app:resize_mode="fit"
|
||||
app:resize_mode="fill"
|
||||
app:show_buffering="when_playing"
|
||||
app:use_artwork="true"
|
||||
app:use_controller="false" />
|
||||
@@ -17,9 +17,9 @@
|
||||
<androidx.media3.ui.DefaultTimeBar
|
||||
android:id="@+id/short_player_progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="6dp"
|
||||
android:layout_height="3dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
app:bar_height="6dp"
|
||||
app:bar_height="3dp"
|
||||
app:buffered_color="#DDEEEEEE"
|
||||
app:played_color="@color/colorPrimary"
|
||||
app:scrubber_disabled_size="0dp"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="5dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/transparent"
|
||||
android:id="@+id/root"
|
||||
android:paddingTop="3dp"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="10dp">
|
||||
<ImageView
|
||||
android:id="@+id/image_icon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:src="@drawable/ic_qr" />
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14dp"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:textColor="@color/white"
|
||||
android:textAlignment="center"
|
||||
android:maxLines="1"
|
||||
android:text="" />
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="ShortsButton">
|
||||
<attr name="buttonIcon_s" format="reference" />
|
||||
<attr name="buttonText_s" format="string" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
@@ -247,6 +247,7 @@
|
||||
<string name="membership">Membership</string>
|
||||
<string name="store">Store</string>
|
||||
<string name="live_chat">Live Chat</string>
|
||||
<string name="vod_chat">VOD Chat</string>
|
||||
<string name="remove">Remove</string>
|
||||
<string name="space_videos">Videos</string>
|
||||
<string name="playlist">Playlist</string>
|
||||
@@ -435,6 +436,8 @@
|
||||
<string name="allow_full_screen_portrait">Allow full-screen portrait when watching horizontal videos</string>
|
||||
<string name="delete_watchlist_on_finish">Delete from WatchLater when watched</string>
|
||||
<string name="delete_watchlist_on_finish_description">After you leave a video that you mostly watched, it will be removed from watch later.</string>
|
||||
<string name="shorts_pregenerate">Pre-generate shorts sources</string>
|
||||
<string name="shorts_pregenerate_description">Generates short sources (when applicable) one video ahead</string>
|
||||
<string name="seek_offset">Seek duration</string>
|
||||
<string name="min_playback_speed">Minimum Playback Speed</string>
|
||||
<string name="min_playback_speed_description">Minimum Available Speed</string>
|
||||
|
||||
@@ -3,208 +3,180 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<Transition
|
||||
android:id="@+id/maximize"
|
||||
app:constraintSetEnd="@id/expanded"
|
||||
app:constraintSetStart="@id/collapsed"
|
||||
app:duration="300"
|
||||
app:motionInterpolator="easeInOut">
|
||||
app:duration="300">
|
||||
|
||||
<OnSwipe
|
||||
app:dragDirection="dragUp"
|
||||
app:maxAcceleration="200"
|
||||
app:touchAnchorId="@+id/touchContainer"
|
||||
app:nestedScrollFlags="disableScroll"
|
||||
app:touchAnchorId="@id/layout_player_container"
|
||||
app:touchAnchorSide="top" />
|
||||
|
||||
<KeyFrameSet>
|
||||
<!--
|
||||
<KeyAttribute
|
||||
android:alpha="0"
|
||||
app:framePosition="0"
|
||||
app:motionTarget="@id/contentContainer" />
|
||||
|
||||
<KeyAttribute
|
||||
android:alpha="1"
|
||||
app:framePosition="100"
|
||||
app:motionTarget="@id/contentContainer" /> -->
|
||||
|
||||
<KeyAttribute
|
||||
app:framePosition="3"
|
||||
app:motionTarget="@id/touchContainer">
|
||||
<CustomAttribute
|
||||
app:attributeName="cardElevation"
|
||||
app:customDimension="0dp" />
|
||||
</KeyAttribute>
|
||||
|
||||
<!--Minimize Progress-->
|
||||
<KeyAttribute
|
||||
app:framePosition="0"
|
||||
app:motionTarget="@id/fragview_videodetail">
|
||||
<CustomAttribute
|
||||
app:attributeName="MinimizeProgress"
|
||||
app:customFloatValue="0" />
|
||||
</KeyAttribute>
|
||||
<KeyAttribute
|
||||
app:framePosition="100"
|
||||
app:motionTarget="@id/fragview_videodetail">
|
||||
<CustomAttribute
|
||||
app:attributeName="MinimizeProgress"
|
||||
app:customFloatValue="1" />
|
||||
</KeyAttribute>
|
||||
|
||||
<!--Controller Alpha-->
|
||||
<KeyAttribute
|
||||
app:framePosition="0"
|
||||
app:motionTarget="@id/fragview_videodetail">
|
||||
<CustomAttribute
|
||||
app:attributeName="ControllerAlpha"
|
||||
app:customFloatValue="0" />
|
||||
</KeyAttribute>
|
||||
<KeyAttribute
|
||||
app:framePosition="100"
|
||||
app:motionTarget="@id/fragview_videodetail">
|
||||
<CustomAttribute
|
||||
app:attributeName="ControllerAlpha"
|
||||
app:customFloatValue="1" />
|
||||
</KeyAttribute>
|
||||
|
||||
<!--Content Alpha-->
|
||||
<KeyAttribute
|
||||
app:framePosition="0"
|
||||
app:motionTarget="@id/fragview_videodetail">
|
||||
<CustomAttribute
|
||||
app:attributeName="ContentAlpha"
|
||||
app:customFloatValue="0" />
|
||||
</KeyAttribute>
|
||||
<KeyAttribute
|
||||
app:framePosition="30"
|
||||
app:motionTarget="@id/fragview_videodetail">
|
||||
<CustomAttribute
|
||||
app:attributeName="ContentAlpha"
|
||||
app:customFloatValue="0" />
|
||||
</KeyAttribute>
|
||||
<KeyAttribute
|
||||
app:framePosition="100"
|
||||
app:motionTarget="@id/fragview_videodetail">
|
||||
<CustomAttribute
|
||||
app:attributeName="ContentAlpha"
|
||||
app:customFloatValue="1" />
|
||||
</KeyAttribute>
|
||||
|
||||
<!--MinimizeControlsAlpha Alpha -->
|
||||
<KeyAttribute
|
||||
app:framePosition="0"
|
||||
app:motionTarget="@id/fragview_videodetail">
|
||||
<CustomAttribute
|
||||
app:attributeName="MinimizeControlsAlpha"
|
||||
app:customFloatValue="1" />
|
||||
</KeyAttribute>
|
||||
<KeyAttribute
|
||||
app:framePosition="20"
|
||||
app:motionTarget="@id/fragview_videodetail">
|
||||
<CustomAttribute
|
||||
app:attributeName="MinimizeControlsAlpha"
|
||||
app:customFloatValue="0" />
|
||||
</KeyAttribute>
|
||||
<KeyAttribute
|
||||
app:framePosition="100"
|
||||
app:motionTarget="@id/fragview_videodetail">
|
||||
<CustomAttribute
|
||||
app:attributeName="MinimizeControlsAlpha"
|
||||
app:customFloatValue="0" />
|
||||
</KeyAttribute>
|
||||
|
||||
|
||||
|
||||
<!--Padding Right-->
|
||||
<KeyAttribute
|
||||
app:framePosition="0"
|
||||
app:motionTarget="@id/fragview_videodetail">
|
||||
<CustomAttribute
|
||||
app:attributeName="VideoMinimize"
|
||||
app:customFloatValue="1" />
|
||||
</KeyAttribute>
|
||||
<KeyAttribute
|
||||
app:framePosition="20"
|
||||
app:motionTarget="@id/fragview_videodetail">
|
||||
<CustomAttribute
|
||||
app:attributeName="VideoMinimize"
|
||||
app:customFloatValue="0" />
|
||||
</KeyAttribute>
|
||||
<KeyAttribute
|
||||
app:framePosition="100"
|
||||
app:motionTarget="@id/fragview_videodetail">
|
||||
<CustomAttribute
|
||||
app:attributeName="VideoMinimize"
|
||||
app:customFloatValue="0" />
|
||||
</KeyAttribute>
|
||||
|
||||
<!--Padding Top-->
|
||||
<KeyAttribute
|
||||
app:framePosition="0"
|
||||
app:motionTarget="@id/fragview_videodetail">
|
||||
<CustomAttribute
|
||||
app:attributeName="TopPadding"
|
||||
app:customDimension="1dp" />
|
||||
</KeyAttribute>
|
||||
<KeyAttribute
|
||||
app:framePosition="100"
|
||||
app:motionTarget="@id/fragview_videodetail">
|
||||
<CustomAttribute
|
||||
app:attributeName="TopPadding"
|
||||
app:customDimension="0dp" />
|
||||
</KeyAttribute>
|
||||
|
||||
</KeyFrameSet>
|
||||
<!--pretty sure this isn't doing anything right now-->
|
||||
<OnClick
|
||||
app:clickAction="transitionToEnd"
|
||||
app:targetId="@id/layout_player_container" />
|
||||
</Transition>
|
||||
|
||||
<ConstraintSet android:id="@+id/expanded">
|
||||
<Transition
|
||||
android:id="@+id/full_screen"
|
||||
app:constraintSetEnd="@id/full_screen_gesture"
|
||||
app:constraintSetStart="@id/expanded"
|
||||
app:duration="300">
|
||||
|
||||
<Constraint
|
||||
android:id="@id/touchContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="220dp"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<Constraint
|
||||
android:id="@id/fragview_videodetail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</ConstraintSet>
|
||||
<OnSwipe
|
||||
app:dragDirection="dragUp"
|
||||
app:maxAcceleration="200"
|
||||
app:nestedScrollFlags="disableScroll"
|
||||
app:onTouchUp="autoCompleteToStart"
|
||||
app:touchAnchorId="@id/layout_player_container"
|
||||
app:touchAnchorSide="bottom" />
|
||||
</Transition>
|
||||
|
||||
<ConstraintSet android:id="@+id/collapsed">
|
||||
|
||||
<Constraint
|
||||
android:id="@id/touchContainer"
|
||||
android:id="@id/layout_player_container"
|
||||
android:layout_height="60dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:layout_marginBottom="48dp"
|
||||
android:layout_marginBottom="47dp"
|
||||
android:elevation="3dp"
|
||||
android:paddingBottom="6dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/minimize_controls"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintHorizontal_weight="150"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintWidth_max="150dp" />
|
||||
<Constraint
|
||||
android:id="@id/contentContainer"
|
||||
android:elevation="1dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
|
||||
<Constraint
|
||||
android:id="@id/fragview_videodetail"
|
||||
android:id="@id/minimize_controls"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:layout_marginBottom="48dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:elevation="1dp"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintHorizontal_weight="350"
|
||||
app:layout_constraintStart_toEndOf="@id/layout_player_container"
|
||||
app:layout_constraintTop_toTopOf="@id/layout_player_container"
|
||||
app:layout_constraintWidth_max="350dp" />
|
||||
<Constraint
|
||||
android:id="@id/videodetail_progress"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-12dp"
|
||||
android:background="@color/black"
|
||||
android:elevation="2dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/minimize_controls"
|
||||
app:layout_constraintStart_toStartOf="@id/layout_player_container"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
|
||||
<Constraint
|
||||
android:id="@id/videodetail_quality_overview"
|
||||
app:visibilityMode="ignore"/>
|
||||
<Constraint
|
||||
android:id="@id/overlay_container"
|
||||
app:visibilityMode="ignore"/>
|
||||
</ConstraintSet>
|
||||
|
||||
</MotionScene>
|
||||
<ConstraintSet android:id="@+id/expanded">
|
||||
<Constraint
|
||||
android:id="@id/layout_player_container"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:elevation="2dp"
|
||||
android:paddingBottom="6dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
<Constraint
|
||||
android:id="@id/contentContainer"
|
||||
android:elevation="2dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
|
||||
<Constraint
|
||||
android:id="@id/minimize_controls"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="60dp"
|
||||
android:elevation="1dp"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/layout_player_container"
|
||||
app:layout_constraintTop_toTopOf="@id/layout_player_container" />
|
||||
<Constraint
|
||||
android:id="@id/videodetail_progress"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-12dp"
|
||||
android:background="@color/transparent"
|
||||
android:elevation="1dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/minimize_controls"
|
||||
app:layout_constraintStart_toStartOf="@id/layout_player_container"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
|
||||
<Constraint
|
||||
android:id="@id/videodetail_quality_overview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:elevation="100dp"
|
||||
android:visibility="gone"
|
||||
app:visibilityMode="ignore"/>
|
||||
<Constraint
|
||||
android:id="@id/overlay_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:elevation="100dp"
|
||||
android:visibility="gone"
|
||||
app:visibilityMode="ignore"/>
|
||||
</ConstraintSet>
|
||||
|
||||
<ConstraintSet android:id="@+id/full_screen_gesture">
|
||||
<Constraint
|
||||
android:id="@id/layout_player_container"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-130dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:elevation="2dp"
|
||||
android:paddingBottom="6dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
<Constraint
|
||||
android:id="@id/contentContainer"
|
||||
android:elevation="1dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
|
||||
<Constraint
|
||||
android:id="@id/minimize_controls"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="60dp"
|
||||
android:elevation="1dp"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/layout_player_container"
|
||||
app:layout_constraintTop_toTopOf="@id/layout_player_container" />
|
||||
<Constraint
|
||||
android:id="@id/videodetail_progress"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-12dp"
|
||||
android:background="@color/transparent"
|
||||
android:elevation="1dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/minimize_controls"
|
||||
app:layout_constraintStart_toStartOf="@id/layout_player_container"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_player_container" />
|
||||
</ConstraintSet>
|
||||
</MotionScene>
|
||||
|
||||
Submodule app/src/stable/assets/sources/apple-podcasts updated: 089987f007...8cff240ca7
Submodule app/src/stable/assets/sources/kick updated: b7173f1538...4ff0b02700
Submodule app/src/stable/assets/sources/peertube updated: 56bff39123...21dcf4bef5
Submodule app/src/stable/assets/sources/rumble updated: 401274b1ec...3368dfaa2c
Submodule app/src/stable/assets/sources/spotify updated: 8c0f03f5fb...207738f599
Submodule app/src/stable/assets/sources/youtube updated: 2b724f21a7...95c60c2dc6
Submodule app/src/unstable/assets/sources/apple-podcasts updated: 089987f007...8cff240ca7
Submodule app/src/unstable/assets/sources/kick updated: b7173f1538...4ff0b02700
Submodule app/src/unstable/assets/sources/peertube updated: 56bff39123...21dcf4bef5
Submodule app/src/unstable/assets/sources/rumble updated: 401274b1ec...3368dfaa2c
Submodule app/src/unstable/assets/sources/spotify updated: 8c0f03f5fb...207738f599
Submodule app/src/unstable/assets/sources/youtube updated: 2b724f21a7...95c60c2dc6
Reference in New Issue
Block a user