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

This commit is contained in:
Kelvin 2024-11-25 17:10:00 +01:00
commit 14b699485a
19 changed files with 587 additions and 575 deletions

View file

@ -51,7 +51,6 @@
android:name=".activities.MainActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:exported="true"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar"
android:launchMode="singleTask"
android:resizeableActivity="true"
@ -146,11 +145,9 @@
<data android:scheme="polycentric" />
</intent-filter>
</activity>
<activity
android:name=".activities.TestActivity"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SettingsActivity"
android:screenOrientation="sensorPortrait"
@ -173,7 +170,6 @@
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.AddSourceActivity"
android:screenOrientation="sensorPortrait"
android:exported="true"
android:theme="@style/Theme.FutoVideo.NoActionBar">
<intent-filter>
@ -217,7 +213,6 @@
android:name=".activities.ManageTabsActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.QRCaptureActivity"
android:screenOrientation="sensorPortrait"

View file

@ -2,11 +2,8 @@ package com.futo.platformplayer
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Context.POWER_SERVICE
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.webkit.CookieManager
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.activities.MainActivity
@ -27,7 +24,6 @@ import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePayment
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.states.StateUpdate
import com.futo.platformplayer.stores.FragmentedStorage
@ -37,9 +33,7 @@ import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldButton
import com.futo.platformplayer.views.fields.FormFieldWarning
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.stripe.android.customersheet.injection.CustomerSheetViewModelModule_Companion_ContextFactory.context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -426,8 +420,6 @@ class Settings : FragmentedStorageFileJson() {
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
var autoRotate: Int = 2;
fun isAutoRotate() = (autoRotate == 1 && !StatePlayer.instance.rotationLock) || (autoRotate == 2 && StateApp.instance.getCurrentSystemAutoRotate() && !StatePlayer.instance.rotationLock);
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
@DropdownFieldOptionsId(R.array.player_background_behavior)
var backgroundPlay: Int = 2;
@ -483,17 +475,6 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.reverse_portrait, FieldForm.TOGGLE, R.string.reverse_portrait_description, 14)
var reversePortrait: Boolean = false;
@FormField(R.string.rotation_zone, FieldForm.DROPDOWN, R.string.rotation_zone_description, 15)
@DropdownFieldOptionsId(R.array.rotation_zone)
var rotationZone: Int = 2;
@FormField(R.string.stability_threshold_time, FieldForm.DROPDOWN, R.string.stability_threshold_time_description, 16)
@DropdownFieldOptionsId(R.array.rotation_threshold_time)
var stabilityThresholdTime: Int = 1;
@FormField(R.string.full_autorotate_lock, FieldForm.TOGGLE, R.string.full_autorotate_lock_description, 17)
var fullAutorotateLock: Boolean = false;
@FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 18)
var preferWebmVideo: Boolean = false;
@FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 19)
@ -880,10 +861,6 @@ class Settings : FragmentedStorageFileJson() {
var other = Other();
@Serializable
class Other {
@FormField(R.string.bypass_rotation_prevention, FieldForm.TOGGLE, R.string.bypass_rotation_prevention_description, 1)
@FormFieldWarning(R.string.bypass_rotation_prevention_warning)
var bypassRotationPrevention: Boolean = false;
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
var playlistDeleteConfirmation: Boolean = true;

View file

@ -1,86 +0,0 @@
package com.futo.platformplayer
import android.app.Activity
import android.content.pm.ActivityInfo
import android.hardware.SensorManager
import android.view.OrientationEventListener
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class SimpleOrientationListener(
private val activity: Activity,
private val lifecycleScope: CoroutineScope
) {
private var lastOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
private var lastStableOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
private var _currentJob: Job? = null
val onOrientationChanged = Event1<Int>()
private val orientationListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_UI) {
override fun onOrientationChanged(orientation: Int) {
//val rotationZone = 45
val stabilityThresholdTime = when (Settings.instance.playback.stabilityThresholdTime) {
0 -> 100L
1 -> 500L
2 -> 750L
3 -> 1000L
4 -> 1500L
5 -> 2000L
else -> 500L
}
val rotationZone = when (Settings.instance.playback.rotationZone) {
0 -> 15
1 -> 30
2 -> 45
else -> 45
}
val newOrientation = when {
orientation in (90 - rotationZone)..(90 + rotationZone - 1) -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
orientation in (180 - rotationZone)..(180 + rotationZone - 1) -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
orientation in (270 - rotationZone)..(270 + rotationZone - 1) -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
orientation in (360 - rotationZone)..(360 + rotationZone - 1) || orientation in 0..(rotationZone - 1) -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
else -> lastOrientation
}
if (newOrientation != lastStableOrientation) {
lastStableOrientation = newOrientation
_currentJob?.cancel()
_currentJob = lifecycleScope.launch(Dispatchers.Main) {
try {
delay(stabilityThresholdTime)
if (newOrientation == lastStableOrientation) {
lastOrientation = newOrientation
onOrientationChanged.emit(newOrientation)
}
} catch (e: Throwable) {
Logger.i(TAG, "Failed to trigger onOrientationChanged", e)
}
}
}
}
}
init {
orientationListener.enable()
lastOrientation = activity.resources.configuration.orientation
}
fun stopListening() {
_currentJob?.cancel()
_currentJob = null
orientationListener.disable()
}
companion object {
private val TAG = "SimpleOrientationListener"
}
}

View file

@ -28,6 +28,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
@ -250,6 +251,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
}
@UnstableApi
override fun onCreate(savedInstanceState: Bundle?) {
Logger.i(TAG, "MainActivity Starting");
StateApp.instance.setGlobalContext(this, lifecycleScope);
@ -513,6 +515,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
//startActivity(Intent(this, TestActivity::class.java));
// updates the requestedOrientation based on user settings
_fragVideoDetail.updateOrientation()
val sharedPreferences =
getSharedPreferences("GrayjayFirstBoot", Context.MODE_PRIVATE)
val isFirstBoot = sharedPreferences.getBoolean("IsFirstBoot", true)

View file

@ -7,6 +7,7 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -34,7 +35,7 @@ import kotlin.math.roundToInt
class MenuBottomBarFragment : MainActivityFragment() {
private var _view: MenuBottomBarView? = null;
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = MenuBottomBarView(this, inflater);
_view = view;
return view;
@ -56,7 +57,13 @@ class MenuBottomBarFragment : MainActivityFragment() {
return _view?.onBackPressed() ?: false;
}
@SuppressLint("ViewConstructor")
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
_view?.updateAllButtonVisibility()
}
@SuppressLint("ViewConstructor")
class MenuBottomBarView : LinearLayout {
private val _fragment: MenuBottomBarFragment;
private val _inflater: LayoutInflater;
@ -76,7 +83,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
private var _buttonsVisible = 0;
private var _subscriptionsVisible = true;
var currentButtonDefinitions: List<ButtonDefinition>? = null;
private var currentButtonDefinitions: List<ButtonDefinition>? = null;
constructor(fragment: MenuBottomBarFragment, inflater: LayoutInflater) : super(inflater.context) {
_fragment = fragment;
@ -132,7 +139,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
val staggerFactor = 3.0f
if (visible) {
moreOverlay.visibility = LinearLayout.VISIBLE
moreOverlay.visibility = VISIBLE
val animations = arrayListOf<Animator>()
animations.add(ObjectAnimator.ofFloat(moreOverlayBackground, "alpha", 0.0f, 1.0f).setDuration(duration))
@ -161,7 +168,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
animatorSet.doOnEnd {
_moreVisibleAnimating = false
_moreVisible = false
moreOverlay.visibility = LinearLayout.INVISIBLE
moreOverlay.visibility = INVISIBLE
}
animatorSet.playTogether(animations)
animatorSet.start()
@ -178,7 +185,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
_layoutBottomBarButtons.removeAllViews();
_layoutBottomBarButtons.addView(Space(context).apply {
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
})
for ((index, button) in buttons.withIndex()) {
@ -192,7 +199,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
_layoutBottomBarButtons.addView(menuButton)
if (index < buttonDefinitions.size - 1) {
_layoutBottomBarButtons.addView(Space(context).apply {
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
})
}
@ -200,7 +207,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
}
_layoutBottomBarButtons.addView(Space(context).apply {
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
})
}
@ -255,9 +262,20 @@ class MenuBottomBarFragment : MainActivityFragment() {
button.updateActive(_fragment);
}
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
updateAllButtonVisibility()
}
fun updateAllButtonVisibility() {
// if the more fly-out menu is open the we should close it
if(_moreVisible) {
setMoreVisible(false)
}
val defs = currentButtonDefinitions?.toMutableList() ?: return
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
val metrics = resources.displayMetrics
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
if (_buttonsVisible >= defs.size) {
updateBottomMenuButtons(defs.toMutableList(), false);

View file

@ -4,7 +4,7 @@ import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
@ -45,9 +45,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
private var _videoOptionsOverlay: SlideUpMenuOverlay? = null;
protected open val shouldShowTimeBar: Boolean get() = true
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
}
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData)
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
return results;
@ -55,16 +53,10 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<IPlatformContent>): InsertedViewAdapterWithLoader<ContentPreviewViewHolder> {
val player = StatePlayer.instance.getThumbnailPlayerOrCreate(context);
player.modifyState("ThumbnailPlayer", { state -> state.muted = true });
player.modifyState("ThumbnailPlayer") { state -> state.muted = true };
_exoPlayer = player;
val v = LinearLayout(context).apply {
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
orientation = LinearLayout.VERTICAL;
};
headerView = v;
return PreviewContentListAdapter(context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(v), arrayListOf(), shouldShowTimeBar).apply {
return PreviewContentListAdapter(context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(), arrayListOf(), shouldShowTimeBar).apply {
attachAdapterEvents(this);
}
}
@ -142,7 +134,10 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
val newQueue = listOf(content) + recyclerData.results
.filterIsInstance<IPlatformVideo>()
.filter { it != content };
StatePlayer.instance.setQueue(newQueue, StatePlayer.TYPE_QUEUE, "Feed Queue", true, false);
StatePlayer.instance.setQueue(newQueue, StatePlayer.TYPE_QUEUE, "Feed Queue",
focus = true,
shuffle = false
);
})
);
}
@ -160,21 +155,22 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
adapter.onLongPress.remove(this);
}
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
super.onRestoreCachedData(cachedData)
val v = LinearLayout(context).apply {
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
orientation = LinearLayout.VERTICAL;
};
headerView = v;
cachedData.adapter.viewsToPrepend.add(v);
(cachedData.adapter as PreviewContentListAdapter?)?.let { attachAdapterEvents(it) };
}
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): LinearLayoutManager {
val llmResults = LinearLayoutManager(context);
llmResults.orientation = LinearLayoutManager.VERTICAL;
return llmResults;
override fun createLayoutManager(
recyclerResults: RecyclerView,
context: Context
): GridLayoutManager {
val glmResults =
GridLayoutManager(
context,
(resources.configuration.screenWidthDp / resources.getDimension(R.dimen.landscape_threshold)).toInt() + 1
);
return glmResults
}
override fun onScrollStateChanged(newState: Int) {
@ -217,11 +213,11 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
}
private fun playPreview() {
if(feedStyle == FeedStyle.THUMBNAIL)
if(feedStyle == FeedStyle.THUMBNAIL || recyclerData.layoutManager.spanCount > 1)
return;
val firstVisible = recyclerData.layoutManager.findFirstVisibleItemPosition();
val lastVisible = recyclerData.layoutManager.findLastVisibleItemPosition();
val firstVisible = recyclerData.layoutManager.findFirstVisibleItemPosition()
val lastVisible = recyclerData.layoutManager.findLastVisibleItemPosition()
val itemsVisible = lastVisible - firstVisible + 1;
val autoPlayIndex = (firstVisible + floor(itemsVisible / 2.0 + 0.49).toInt()).coerceAtLeast(0).coerceAtMost((recyclerData.results.size - 1));
@ -241,7 +237,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
(recyclerData.adapter as PreviewContentListAdapter?)?.preview(viewHolder.childViewHolder)
}
fun stopVideo() {
private fun stopVideo() {
//TODO: Is this still necessary?
(recyclerData.adapter as PreviewContentListAdapter?)?.stopPreview();
}
@ -269,6 +265,6 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
}
companion object {
private val TAG = "ContentFeedView";
private const val TAG = "ContentFeedView";
}
}

View file

@ -3,13 +3,9 @@ package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Context
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.ViewGroup.MarginLayoutParams
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.structures.*
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.adapters.*
@ -18,9 +14,7 @@ import com.futo.platformplayer.views.adapters.viewholders.CreatorViewHolder
abstract class CreatorFeedView<TFragment> : FeedView<TFragment, PlatformAuthorLink, PlatformAuthorLink, IPager<PlatformAuthorLink>, CreatorViewHolder> where TFragment : MainFragment {
override val feedStyle: FeedStyle = FeedStyle.THUMBNAIL; //R.layout.list_creator;
constructor(fragment: TFragment, inflater: LayoutInflater) : super(fragment, inflater) {
}
constructor(fragment: TFragment, inflater: LayoutInflater) : super(fragment, inflater)
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<PlatformAuthorLink>): InsertedViewAdapterWithLoader<CreatorViewHolder> {
return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
@ -34,18 +28,31 @@ abstract class CreatorFeedView<TFragment> : FeedView<TFragment, PlatformAuthorLi
);
}
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): LinearLayoutManager {
val glmResults = GridLayoutManager(context, 2);
glmResults.orientation = LinearLayoutManager.VERTICAL;
/*
* An empty override to remove the inherited span count update functionality
*/
override fun updateSpanCount(){
}
override fun createLayoutManager(
recyclerResults: RecyclerView,
context: Context
): GridLayoutManager {
val glmResults = GridLayoutManager(context, 2)
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
rightMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8.0f, context.resources.displayMetrics).toInt();
};
rightMargin = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
8.0f,
context.resources.displayMetrics
).toInt()
}
return glmResults;
return glmResults
}
companion object {
private val TAG = "CreatorFeedView";
private const val TAG = "CreatorFeedView";
}
}

View file

@ -1,13 +1,14 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@ -33,7 +34,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
protected val _recyclerResults: RecyclerView;
protected val _overlayContainer: FrameLayout;
protected val _swipeRefresh: SwipeRefreshLayout;
private val _progress_bar: ProgressBar;
private val _progressBar: ProgressBar;
private val _spinnerSortBy: Spinner;
private val _containerSortBy: LinearLayout;
private val _tagsView: TagsView;
@ -44,7 +45,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
private var _loading: Boolean = true;
private val _pager_lock = Object();
private val _pagerLock = Object();
private var _cache: ItemCache<TResult>? = null;
open val visibleThreshold = 15;
@ -58,21 +59,21 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
private var _activeTags: List<String>? = null;
private var _nextPageHandler: TaskHandler<TPager, List<TResult>>;
val recyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>;
val recyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>;
val fragment: TFragment;
private val _scrollListener: RecyclerView.OnScrollListener;
private var _automaticNextPageCounter = 0;
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>? = null) : super(inflater.context) {
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>? = null) : super(inflater.context) {
this.fragment = fragment;
inflater.inflate(R.layout.fragment_feed, this);
_textCentered = findViewById(R.id.text_centered);
_emptyPagerContainer = findViewById(R.id.empty_pager_container);
_progress_bar = findViewById(R.id.progress_bar);
_progress_bar.inactiveColor = Color.TRANSPARENT;
_progressBar = findViewById(R.id.progress_bar);
_progressBar.inactiveColor = Color.TRANSPARENT;
_swipeRefresh = findViewById(R.id.swipe_refresh);
val recyclerResults: RecyclerView = findViewById(R.id.list_results);
@ -158,7 +159,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
super.onScrolled(recyclerView, dx, dy);
val visibleItemCount = _recyclerResults.childCount;
val firstVisibleItem = recyclerData.layoutManager.findFirstVisibleItemPosition();
val firstVisibleItem = recyclerData.layoutManager.findFirstVisibleItemPosition()
//Logger.i(TAG, "onScrolled loadNextPage visibleItemCount=$visibleItemCount firstVisibleItem=$visibleItemCount")
if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= recyclerData.results.size && firstVisibleItem > 0) {
@ -179,14 +180,13 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
if (firstVisibleItemPosition != RecyclerView.NO_POSITION) {
val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition)
val itemHeight = firstVisibleView?.height ?: 0
val occupiedSpace = recyclerData.results.size * itemHeight
val occupiedSpace = recyclerData.results.size / recyclerData.layoutManager.spanCount * itemHeight
val recyclerViewHeight = _recyclerResults.height
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage occupiedSpace=$occupiedSpace recyclerViewHeight=$recyclerViewHeight")
occupiedSpace >= recyclerViewHeight
} else {
false
}
}
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter")
if (!canScroll || filteredResults.isEmpty()) {
@ -226,7 +226,19 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
}
}
open fun updateSpanCount() {
recyclerData.layoutManager.spanCount = (resources.configuration.screenWidthDp / resources.getDimension(R.dimen.landscape_threshold)).toInt() + 1
}
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
updateSpanCount()
}
fun onResume() {
updateSpanCount()
//Reload the pager if the plugin was killed
val pager = recyclerData.pager;
if((pager is MultiPager<*> && pager.findPager { it is JSPager<*> && !it.isAvailable } != null) ||
@ -252,7 +264,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
protected open fun setActiveTags(activeTags: List<String>?) {
_activeTags = activeTags;
if (activeTags != null && activeTags.isNotEmpty()) {
if (!activeTags.isNullOrEmpty()) {
_tagsView.setTags(activeTags);
_tagsView.visibility = View.VISIBLE;
} else {
@ -262,7 +274,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
protected open fun setSortByOptions(options: List<String>?) {
_sortByOptions = options;
if (options != null && options.isNotEmpty()) {
if (!options.isNullOrEmpty()) {
val allOptions = arrayListOf<String>();
allOptions.add("Default");
allOptions.addAll(options);
@ -277,19 +289,19 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
}
}
protected abstract fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<TConverted>): InsertedViewAdapterWithLoader<TViewHolder>;
protected abstract fun createLayoutManager(recyclerResults: RecyclerView, context: Context): LinearLayoutManager;
protected open fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>) {}
protected abstract fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager;
protected open fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>) {}
protected fun setProgress(fin: Int, total: Int) {
val progress = (fin.toFloat() / total);
_progress_bar.progress = progress;
_progressBar.progress = progress;
if(progress > 0 && progress < 1)
{
if(_progress_bar.height == 0)
_progress_bar.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 5);
if(_progressBar.height == 0)
_progressBar.layoutParams = LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 5);
}
else if(_progress_bar.height > 0) {
_progress_bar.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
else if(_progressBar.height > 0) {
_progressBar.layoutParams = LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
}
}
@ -345,7 +357,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
//insertPagerResults(_cache!!.cachePager.getResults(), false);
}
fun setPager(pager: TPager, cache: ItemCache<TResult>? = null) {
synchronized(_pager_lock) {
synchronized(_pagerLock) {
detachParentPagerEvents();
detachPagerEvents();
@ -425,7 +437,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
val p = recyclerData.pager;
if(p is IReplacerPager<*>) {
p.onReplaced.subscribe(this) { _, newItem ->
synchronized(_pager_lock) {
synchronized(_pagerLock) {
val filtered = filterResults(listOf(newItem as TResult));
if(filtered.isEmpty())
return@subscribe;
@ -443,7 +455,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
var _lastNextPage = false;
private fun loadNextPage() {
synchronized(_pager_lock) {
synchronized(_pagerLock) {
val pager: TPager = recyclerData.pager ?: return;
val hasMorePages = pager.hasMorePages();
Logger.i(TAG, "loadNextPage() hasMorePages=$hasMorePages, page size=${pager.getResults().size}");
@ -468,7 +480,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
}
companion object {
private val TAG = "FeedView";
private const val TAG = "FeedView";
}
abstract class ItemCache<TResult>(val cachePager: IPager<TResult>) {

View file

@ -6,7 +6,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.GridLayoutManager
import com.futo.platformplayer.*
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
@ -18,13 +18,9 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.NoResultsView
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
@ -32,11 +28,8 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder
import com.futo.platformplayer.views.announcements.AnnouncementView
import com.futo.platformplayer.views.buttons.BigButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.time.OffsetDateTime
import java.util.UUID
class HomeFragment : MainFragment() {
override val isMainView : Boolean = true;
@ -44,7 +37,7 @@ class HomeFragment : MainFragment() {
override val hasBottomBar: Boolean get() = true;
private var _view: HomeView? = null;
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
fun reloadFeed() {
_view?.reloadFeed()
@ -101,15 +94,19 @@ class HomeFragment : MainFragment() {
class HomeView : ContentFeedView<HomeFragment> {
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
private var _announcementsView: AnnouncementView;
private var _announcementsView: AnnouncementView = AnnouncementView(context, null).apply {
if(!this.isClosed()) {
recyclerData.adapter.viewsToPrepend.add(this)
this.onClose.subscribe {
recyclerData.adapter.viewsToPrepend.remove(this)
}
}
};
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
_announcementsView = AnnouncementView(context, null).apply {
headerView.addView(this);
};
constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
_taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({ fragment.lifecycleScope }, {
StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope)
@ -174,7 +171,7 @@ class HomeFragment : MainFragment() {
loadResults();
}
override fun getEmptyPagerView(): View? {
override fun getEmptyPagerView(): View {
val dp10 = 10.dp(resources);
val dp30 = 30.dp(resources);
@ -206,8 +203,7 @@ class HomeFragment : MainFragment() {
listOf(BigButton(context, "Sources", "Go to the sources tab", R.drawable.ic_creators) {
fragment.navigate<SourcesFragment>();
}.withMargin(dp10, dp30))
);
return null;
)
}
override fun reload() {
@ -227,7 +223,7 @@ class HomeFragment : MainFragment() {
//StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), context.getString(R.string.no_home_available), context.getString(R.string.no_home_page_is_available_please_check_if_you_are_connected_to_the_internet_and_refresh), AnnouncementType.SESSION);
}
Logger.i(TAG, "Got new home pager ${pager}");
Logger.i(TAG, "Got new home pager $pager");
finishRefreshLayoutLoader();
setLoading(false);
setPager(pager);
@ -237,7 +233,7 @@ class HomeFragment : MainFragment() {
}
companion object {
val TAG = "HomeFragment";
const val TAG = "HomeFragment";
fun newInstance() = HomeFragment().apply {}
}

View file

@ -5,12 +5,10 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.GridLayoutManager
import com.futo.platformplayer.*
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
@ -47,7 +45,6 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.nio.channels.Channel
import java.time.OffsetDateTime
import kotlin.system.measureTimeMillis
@ -58,7 +55,7 @@ class SubscriptionsFeedFragment : MainFragment() {
private var _view: SubscriptionsFeedView? = null;
private var _group: SubscriptionGroup? = null;
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
@ -111,7 +108,7 @@ class SubscriptionsFeedFragment : MainFragment() {
var subGroup: SubscriptionGroup? = null;
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
StateSubscriptions.instance.global.onUpdateProgress.subscribe(this) { progress, total ->
};
@ -152,16 +149,19 @@ class SubscriptionsFeedFragment : MainFragment() {
val homeTab = Settings.instance.tabs.find { it.id == 0 };
val isHomeEnabled = homeTab?.enabled == true;
if (announcementsView != null && isHomeEnabled) {
headerView.removeView(announcementsView);
_announcementsView = null;
recyclerData.adapter.viewsToPrepend.remove(announcementsView)
_announcementsView = null
}
if (announcementsView == null && !isHomeEnabled) {
val c = context;
if (c != null) {
_announcementsView = AnnouncementView(c, null).apply {
headerView.addView(this)
};
recyclerData.adapter.viewsToPrepend.add(this)
this.onClose.subscribe {
recyclerData.adapter.viewsToPrepend.remove(this)
}
}
}
}
@ -215,7 +215,7 @@ class SubscriptionsFeedFragment : MainFragment() {
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(group);
val reqCountStr = subRequestCounts.map { " ${it.key.config.name}: ${it.value}/${it.key.getSubscriptionRateLimit()}" }.joinToString("\n");
val rateLimitPlugins = subRequestCounts.filter { clientCount -> clientCount.key.getSubscriptionRateLimit()?.let { rateLimit -> clientCount.value > rateLimit } == true }
Logger.w(TAG, "Trying to refreshing subscriptions with requests:\n" + reqCountStr);
Logger.w(TAG, "Trying to refreshing subscriptions with requests:\n$reqCountStr");
if(rateLimitPlugins.any())
throw RateLimitException(rateLimitPlugins.map { it.key.id });
}
@ -277,7 +277,7 @@ class SubscriptionsFeedFragment : MainFragment() {
private fun initializeToolbarContent() {
_subscriptionBar = SubscriptionBar(context).apply {
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
};
_subscriptionBar?.onClickChannel?.subscribe { c -> fragment.navigate<ChannelFragment>(c); };
_subscriptionBar?.onToggleGroup?.subscribe { g ->
@ -397,7 +397,7 @@ class SubscriptionsFeedFragment : MainFragment() {
_taskGetPager.run(withRefetch);
}
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
super.onRestoreCachedData(cachedData);
setEmptyPager(cachedData.results.isEmpty());
}
@ -452,7 +452,7 @@ class SubscriptionsFeedFragment : MainFragment() {
if (toShow is PluginException)
UIDialogs.appToast(ToastView.Toast(
toShow.message +
(if(channel != null) "\nChannel: " + channel else ""), false, null,
(if(channel != null) "\nChannel: $channel" else ""), false, null,
"Plugin ${toShow.config.name} failed")
);
else
@ -463,14 +463,14 @@ class SubscriptionsFeedFragment : MainFragment() {
val failedChannels = exs.filterIsInstance<ChannelException>().map { it.channelNameOrUrl }.distinct().toList();
val failedPlugins = exs.filter { it is PluginException || (it is ChannelException && it.cause is PluginException) }
.map { if(it is ChannelException) (it.cause as PluginException) else if(it is PluginException) it else null }
.filter { it != null }
.filterNotNull()
.distinctBy { it?.config?.name }
.map { it!! }
.toList();
for(distinctPluginFail in failedPlugins)
UIDialogs.appToast(context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: ""));
if(failedChannels.isNotEmpty())
UIDialogs.appToast(ToastView.Toast(failedChannels.take(3).map { "- ${it}" }.joinToString("\n") +
UIDialogs.appToast(ToastView.Toast(failedChannels.take(3).map { "- $it" }.joinToString("\n") +
(if(failedChannels.size >= 3) "\nAnd ${failedChannels.size - 3} more" else ""), false, null, "Failed Channels"));
}
} catch (e: Throwable) {
@ -482,7 +482,7 @@ class SubscriptionsFeedFragment : MainFragment() {
}
companion object {
val TAG = "SubscriptionsFeedFragment";
const val TAG = "SubscriptionsFeedFragment";
fun newInstance() = SubscriptionsFeedFragment().apply {}
}

View file

@ -6,6 +6,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.IPlatformClient
@ -58,7 +59,15 @@ class TutorialFragment : MainFragment() {
}
@SuppressLint("ViewConstructor")
class TutorialView : LinearLayout {
class TutorialView(fragment: TutorialFragment, inflater: LayoutInflater) :
ScrollView(inflater.context) {
init {
addView(TutorialContainer(fragment, inflater))
}
}
@SuppressLint("ViewConstructor")
class TutorialContainer : LinearLayout {
val fragment: TutorialFragment
constructor(fragment: TutorialFragment, inflater: LayoutInflater) : super(inflater.context) {
@ -150,7 +159,7 @@ class TutorialFragment : MainFragment() {
}
companion object {
val TAG = "HomeFragment";
const val TAG = "HomeFragment";
fun newInstance() = TutorialFragment().apply {}
val initialSetupVideos = listOf(

View file

@ -1,11 +1,10 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -14,10 +13,9 @@ import android.view.WindowInsetsController
import android.view.WindowManager
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.SimpleOrientationListener
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
@ -25,14 +23,14 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.listeners.AutoRotateChangeListener
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 kotlin.math.min
@UnstableApi
class VideoDetailFragment : MainFragment {
override val isMainView : Boolean = false;
override val hasBottomBar: Boolean = true;
@ -43,11 +41,13 @@ class VideoDetailFragment : MainFragment {
private var _viewDetail : VideoDetailView? = null;
private var _view : SingleViewTouchableMotionLayout? = null;
private lateinit var _autoRotateChangeListener: AutoRotateChangeListener
private lateinit var _orientationListener: SimpleOrientationListener
private var _currentOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
var isFullscreen : Boolean = false;
/**
* whether the view is in the process of switching from full-screen maximized to minimized
* this is used to detect that the app is skipping the non full-screen maximized state
*/
var isMinimizingFromFullScreen : Boolean = false;
val onFullscreenChanged = Event1<Boolean>();
var isTransitioning : Boolean = false
private set;
@ -77,8 +77,7 @@ class VideoDetailFragment : MainFragment {
private var _leavingPiP = false;
//region Fragment
constructor() : super() {
}
constructor() : super()
fun nextVideo() {
_viewDetail?.nextVideo(true, true, true);
@ -88,65 +87,105 @@ class VideoDetailFragment : MainFragment {
_viewDetail?.prevVideo(true);
}
private fun onStateChanged(state: VideoDetailFragment.State) {
private fun isSmallWindow(): Boolean {
return min(
resources.configuration.screenWidthDp,
resources.configuration.screenHeightDp
) < resources.getDimension(R.dimen.landscape_threshold)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val isLandscapeVideo: Boolean = _viewDetail?.isLandscapeVideo() ?: false
val isSmallWindow = isSmallWindow()
if (
isSmallWindow
&& newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
&& !isFullscreen
&& state == State.MAXIMIZED
) {
_viewDetail?.setFullscreen(true)
} else if (
isSmallWindow
&& isFullscreen
&& !Settings.instance.playback.fullscreenPortrait
&& newConfig.orientation == Configuration.ORIENTATION_PORTRAIT
&& isLandscapeVideo
) {
_viewDetail?.setFullscreen(false)
}
}
private fun onStateChanged(state: State) {
if (
isSmallWindow()
&& state == State.MAXIMIZED
&& !isFullscreen
&& resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
) {
_viewDetail?.setFullscreen(true)
}
updateOrientation()
}
private fun updateOrientation() {
private fun onVideoChanged(videoWidth : Int, videoHeight: Int) {
if (
isSmallWindow()
&& state == State.MAXIMIZED
&& !isFullscreen
&& videoHeight > videoWidth
) {
_viewDetail?.setFullscreen(true)
}
}
@SuppressLint("SourceLockedOrientationActivity")
fun updateOrientation() {
val a = activity ?: return
val isMaximized = state == State.MAXIMIZED
val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait;
val bypassRotationPrevention = Settings.instance.other.bypassRotationPrevention;
val fullAutorotateLock = Settings.instance.playback.fullAutorotateLock
val currentRequestedOrientation = a.requestedOrientation
var currentOrientation = if (_currentOrientation == -1) currentRequestedOrientation else _currentOrientation
if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT && !Settings.instance.playback.reversePortrait)
currentOrientation = currentRequestedOrientation
val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait
val isReversePortraitAllowed = Settings.instance.playback.reversePortrait
val rotationLock = StatePlayer.instance.rotationLock
val isAutoRotate = Settings.instance.playback.isAutoRotate()
val isFs = isFullscreen
val isLandscapeVideo: Boolean = _viewDetail?.isLandscapeVideo() ?: false
if (fullAutorotateLock) {
if (isFs && isMaximized) {
if (isFullScreenPortraitAllowed) {
if (isAutoRotate) {
a.requestedOrientation = currentOrientation
}
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
if (isAutoRotate || currentOrientation != currentRequestedOrientation && (currentRequestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || currentRequestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT)) {
a.requestedOrientation = currentOrientation
}
} else {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
} else if (bypassRotationPrevention) {
a.requestedOrientation = currentOrientation
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT) {
a.requestedOrientation = currentOrientation
} else {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
val isSmallWindow = isSmallWindow()
// For small windows if the device isn't landscape right now and full screen portrait isn't allowed then we should force landscape
if (isSmallWindow && isFullscreen && !isFullScreenPortraitAllowed && resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT && !rotationLock && isLandscapeVideo) {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}
// For small windows if the device isn't in a portrait orientation and we're in the maximized state then we should force portrait
else if (isSmallWindow && !isMinimizingFromFullScreen && !isFullscreen && state == State.MAXIMIZED && resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
} else if (rotationLock) {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
} else {
if (isFs && isMaximized) {
if (isFullScreenPortraitAllowed) {
a.requestedOrientation = currentOrientation
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
a.requestedOrientation = currentOrientation
} else if (currentRequestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || currentRequestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
//Don't change anything
} else {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
when (Settings.instance.playback.autoRotate) {
0 -> {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
}
1 -> {
a.requestedOrientation = if (isReversePortraitAllowed) {
ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
} else {
ActivityInfo.SCREEN_ORIENTATION_SENSOR
}
}
2 -> {
a.requestedOrientation = if (isReversePortraitAllowed) {
ActivityInfo.SCREEN_ORIENTATION_FULL_USER
} else {
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
} else if (bypassRotationPrevention) {
a.requestedOrientation = currentOrientation
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT) {
a.requestedOrientation = currentOrientation
} else {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
Log.i(TAG, "updateOrientation (isFs = ${isFs}, currentOrientation = ${currentOrientation}, fullAutorotateLock = ${fullAutorotateLock}, currentRequestedOrientation = ${currentRequestedOrientation}, isMaximized = ${isMaximized}, isAutoRotate = ${isAutoRotate}, isFullScreenPortraitAllowed = ${isFullScreenPortraitAllowed}) resulted in requested orientation ${activity?.requestedOrientation}");
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
@ -188,10 +227,6 @@ class VideoDetailFragment : MainFragment {
return true;
}
override fun onHide() {
super.onHide();
}
fun preventPictureInPicture() {
Logger.i(TAG, "preventPictureInPicture() preventPictureInPicture = true");
_viewDetail?.preventPictureInPicture = true;
@ -231,7 +266,9 @@ class VideoDetailFragment : MainFragment {
_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 {
@ -268,6 +305,7 @@ class VideoDetailFragment : MainFragment {
if (state != State.MINIMIZED && progress < 0.1) {
state = State.MINIMIZED;
isMinimizingFromFullScreen = false
onMinimize.emit();
}
else if (state != State.MAXIMIZED && progress > 0.9) {
@ -306,13 +344,6 @@ class VideoDetailFragment : MainFragment {
minimizeVideoDetail();
}
_autoRotateChangeListener = AutoRotateChangeListener(requireContext(), Handler()) { _ ->
if (updateAutoFullscreen()) {
return@AutoRotateChangeListener
}
updateOrientation()
}
_loadUrlOnCreate?.let { _viewDetail?.setVideo(it.url, it.timeSeconds, it.playWhenReady) };
maximizeVideoDetail();
@ -321,40 +352,11 @@ class VideoDetailFragment : MainFragment {
}
StatePlayer.instance.onRotationLockChanged.subscribe(this) {
if (updateAutoFullscreen()) {
return@subscribe
}
updateOrientation()
}
_orientationListener = SimpleOrientationListener(requireActivity(), lifecycleScope)
_orientationListener.onOrientationChanged.subscribe {
_currentOrientation = it
Logger.i(TAG, "Current orientation changed (_currentOrientation = ${_currentOrientation})")
if (updateAutoFullscreen()) {
return@subscribe
}
updateOrientation()
}
return _view!!;
}
private fun updateAutoFullscreen(): Boolean {
if (Settings.instance.playback.isAutoRotate()) {
if (state == State.MAXIMIZED && !isFullscreen && (_currentOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || _currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE)) {
_viewDetail?.setFullscreen(true)
return true
}
if (state == State.MAXIMIZED && isFullscreen && !Settings.instance.playback.fullscreenPortrait && (_currentOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || (Settings.instance.playback.reversePortrait && _currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT))) {
_viewDetail?.setFullscreen(false)
return true
}
}
return false
}
fun onUserLeaveHint() {
val viewDetail = _viewDetail;
Logger.i(TAG, "onUserLeaveHint preventPictureInPicture=${viewDetail?.preventPictureInPicture} isCasting=${StateCasting.instance.isCasting} isBackgroundPictureInPicture=${Settings.instance.playback.isBackgroundPictureInPicture()} allowBackground=${viewDetail?.allowBackground}");
@ -443,15 +445,12 @@ class VideoDetailFragment : MainFragment {
if(shouldStop) {
_viewDetail?.onStop();
StateCasting.instance.onStop();
Logger.v(TAG, "called onStop() shouldStop: $shouldStop");
}
}
override fun onDestroyMainView() {
super.onDestroyMainView();
Logger.v(TAG, "onDestroyMainView");
_autoRotateChangeListener?.unregister()
_orientationListener.stopListening()
SettingsActivity.settingsActivityClosed.remove(this)
StatePlayer.instance.onRotationLockChanged.remove(this)
@ -532,7 +531,7 @@ class VideoDetailFragment : MainFragment {
}
companion object {
private val TAG = "VideoDetailFragment";
private const val TAG = "VideoDetailFragment";
fun newInstance() = VideoDetailFragment().apply {}
}

View file

@ -4,6 +4,7 @@ import android.app.PictureInPictureParams
import android.app.RemoteAction
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Rect
@ -81,6 +82,7 @@ import com.futo.platformplayer.casting.CastConnectionState
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.dp
@ -159,20 +161,20 @@ import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.protobuf.ByteString
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import okhttp3.Dispatcher
import org.w3c.dom.Text
import userpackage.Protocol
import java.time.OffsetDateTime
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToLong
@androidx.media3.common.util.UnstableApi
@UnstableApi
class VideoDetailView : ConstraintLayout {
private val TAG = "VideoDetailView"
@ -185,7 +187,7 @@ class VideoDetailView : ConstraintLayout {
private var _searchVideo: IPlatformVideo? = null;
var video: IPlatformVideoDetails? = null
private set;
var videoLocal: VideoLocal? = null;
private var videoLocal: VideoLocal? = null;
private var _playbackTracker: IPlaybackTracker? = null;
private var _historyIndex: DBHistory.Index? = null;
@ -200,7 +202,7 @@ class VideoDetailView : ConstraintLayout {
private val _timeBar: TimeBar;
private var _upNext: UpNextView;
val rootView: ConstraintLayout;
private val rootView: ConstraintLayout;
private val _title: TextView;
private val _subTitle: TextView;
@ -289,7 +291,7 @@ class VideoDetailView : ConstraintLayout {
var isPlaying: Boolean = false
private set;
var lastPositionMilliseconds: Long = 0
private var lastPositionMilliseconds: Long = 0
private set;
private var _historicalPosition: Long = 0;
private var _commentsCount = 0;
@ -304,6 +306,7 @@ class VideoDetailView : ConstraintLayout {
val onFullscreenChanged = Event1<Boolean>();
val onEnterPictureInPicture = Event0();
val onPlayChanged = Event1<Boolean>();
val onVideoChanged = Event2<Int, Int>()
var allowBackground : Boolean = false
private set;
@ -529,12 +532,14 @@ class VideoDetailView : ConstraintLayout {
_cast.onChapterChanged.subscribe(onChapterChanged);
_cast.onMinimizeClick.subscribe {
_player.setFullScreen(false);
onMinimize.emit();
// emit minimize before toggling fullscreen so we know that the full screen toggle is happening during a minimize operation
onMinimize.emit()
_player.setFullScreen(false)
};
_player.onMinimize.subscribe {
_player.setFullScreen(false);
onMinimize.emit();
// emit minimize before toggling fullscreen so we know that the full screen toggle is happening during a minimize operation
onMinimize.emit()
_player.setFullScreen(false)
};
_player.onTimeBarChanged.subscribe { position, _ ->
@ -723,7 +728,8 @@ class VideoDetailView : ConstraintLayout {
if (c is PolycentricPlatformComment) {
var parentComment: PolycentricPlatformComment = c;
_container_content_replies.load(if (_tabIndex!! == 0) false else true, metadata, c.contextUrl, c.reference, c,
_container_content_replies.load(
_tabIndex!! != 0, metadata, c.contextUrl, c.reference, c,
{ StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) },
{
val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1);
@ -731,7 +737,7 @@ class VideoDetailView : ConstraintLayout {
parentComment = newComment;
});
} else {
_container_content_replies.load(if (_tabIndex!! == 0) false else true, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
_container_content_replies.load(_tabIndex!! != 0, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
}
switchContentView(_container_content_replies);
};
@ -941,7 +947,7 @@ class VideoDetailView : ConstraintLayout {
else {
val selectedButtons = _buttonPinStore.getAllValues()
.map { x-> buttons.find { it.tagRef == x } }
.filter { it != null }
.filterNotNull()
.map { it!! };
_buttonPins.setButtons(*(selectedButtons +
buttons.filter { !selectedButtons.contains(it) } +
@ -1257,7 +1263,8 @@ class VideoDetailView : ConstraintLayout {
switchContentView(_container_content_main);
}
//@OptIn(ExperimentalCoroutinesApi::class)
@OptIn(ExperimentalCoroutinesApi::class)
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
_didTriggerDatasourceErrroCount = 0;
@ -1265,7 +1272,7 @@ class VideoDetailView : ConstraintLayout {
_autoplayVideo = null
Logger.i(TAG, "Autoplay video cleared (setVideoDetails)")
if(newVideo && this.video?.url == videoDetail.url)
if (newVideo && this.video?.url == videoDetail.url)
return;
if (newVideo) {
@ -1274,8 +1281,13 @@ class VideoDetailView : ConstraintLayout {
_lastSubtitleSource = null;
}
if(videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now())
UIDialogs.toast(context, context.getString(R.string.planned_in) + " ${videoDetail.datetime?.toHumanNowDiffString(true)}")
if (videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now())
UIDialogs.toast(
context,
context.getString(R.string.planned_in) + " ${
videoDetail.datetime?.toHumanNowDiffString(true)
}"
)
if (!videoDetail.isLive) {
_player.setPlaybackRate(Settings.instance.playback.getDefaultPlaybackSpeed());
@ -1284,26 +1296,25 @@ class VideoDetailView : ConstraintLayout {
val videoLocal: VideoLocal?;
val video: IPlatformVideoDetails?;
if(videoDetail is VideoLocal) {
if (videoDetail is VideoLocal) {
videoLocal = videoDetail;
video = videoDetail;
this.video = video;
val videoTask = StatePlatform.instance.getContentDetails(videoDetail.url);
videoTask.invokeOnCompletion { ex ->
if(ex != null) {
if (ex != null) {
Logger.e(TAG, "Failed to fetch live video for offline video", ex);
return@invokeOnCompletion;
}
val result = videoTask.getCompleted();
if(this.video == videoDetail && result is IPlatformVideoDetails) {
if (this.video == videoDetail && result is IPlatformVideoDetails) {
this.video = result;
fragment.lifecycleScope.launch(Dispatchers.Main) {
updateQualitySourcesOverlay(result, videoLocal);
}
}
};
}
else { //TODO: Update cached video if it exists with video
} else { //TODO: Update cached video if it exists with video
videoLocal = StateDownloads.instance.getCachedVideo(videoDetail.id);
video = videoDetail;
}
@ -1311,7 +1322,9 @@ class VideoDetailView : ConstraintLayout {
this.video = video;
cleanupPlaybackTracker();
if(video is JSVideoDetails) {
onVideoChanged.emit(video.video.videoSources[0].width, video.video.videoSources[0].height)
if (video is JSVideoDetails) {
val me = this;
fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
@ -1319,8 +1332,7 @@ class VideoDetailView : ConstraintLayout {
val chapters = null ?: StatePlatform.instance.getContentChapters(video.url);
_player.setChapters(chapters);
_cast.setChapters(chapters);
}
catch(ex: Throwable) {
} catch (ex: Throwable) {
Logger.e(TAG, "Failed to get chapters", ex);
_player.setChapters(null);
_cast.setChapters(null);
@ -1330,7 +1342,7 @@ class VideoDetailView : ConstraintLayout {
}*/
}
try {
if(!StateApp.instance.privateMode) {
if (!StateApp.instance.privateMode) {
val stopwatch = com.futo.platformplayer.debug.Stopwatch()
var tracker = video.getPlaybackTracker()
Logger.i(TAG, "video.getPlaybackTracker took ${stopwatch.elapsedMs}ms")
@ -1346,17 +1358,20 @@ class VideoDetailView : ConstraintLayout {
if (me.video == video)
me._playbackTracker = tracker;
}
else if(me.video == video)
} else if (me.video == video)
me._playbackTracker = null;
}
catch(ex: Throwable) {
} catch (ex: Throwable) {
Logger.e(TAG, "Playback tracker failed", ex);
if(me.video?.isLive == true || ex.message?.contains("Unable to resolve host") == true) withContext(Dispatchers.Main) {
UIDialogs.toast(context, context.getString(R.string.failed_to_get_playback_tracker));
};
else withContext(Dispatchers.Main) {
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_get_playback_tracker), ex);
UIDialogs.showGeneralErrorDialog(
context,
context.getString(R.string.failed_to_get_playback_tracker),
ex
);
}
}
};
@ -1373,8 +1388,11 @@ class VideoDetailView : ConstraintLayout {
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);
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)
}
@ -1384,9 +1402,16 @@ class VideoDetailView : ConstraintLayout {
//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() + " " + context.getString(R.string.subscribers) else "";
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0);
if (video.author.subscribers != null) {
_channelMeta.text = if ((video.author.subscribers
?: 0) > 0
) video.author.subscribers!!.toHumanNumber() + " " + context.getString(R.string.subscribers) else "";
(_channelName.layoutParams as MarginLayoutParams).setMargins(
0,
(DP_5 * -1).toInt(),
0,
0
);
} else {
_channelMeta.text = "";
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_2).toInt(), 0, 0);
@ -1394,7 +1419,7 @@ class VideoDetailView : ConstraintLayout {
video.author.let {
if(it is PlatformAuthorMembershipLink && !it.membershipUrl.isNullOrEmpty())
if (it is PlatformAuthorMembershipLink && !it.membershipUrl.isNullOrEmpty())
_monetization.setPlatformMembership(video.id.pluginId, it.membershipUrl);
else
_monetization.setPlatformMembership(null, null);
@ -1408,7 +1433,8 @@ class VideoDetailView : ConstraintLayout {
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url, true);
val cachedPolycentricProfile =
PolycentricCache.instance.getCachedProfile(video.author.url, true);
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = false);
} else {
@ -1417,13 +1443,19 @@ class VideoDetailView : ConstraintLayout {
}
_platform.setPlatformFromClientID(video.id.pluginId);
val subTitleSegments : ArrayList<String> = ArrayList();
if(video.viewCount > 0)
subTitleSegments.add("${video.viewCount.toHumanNumber()} ${if(video.isLive) context.getString(R.string.watching_now) else context.getString(R.string.views)}");
if(video.datetime != null) {
val subTitleSegments: ArrayList<String> = ArrayList();
if (video.viewCount > 0)
subTitleSegments.add(
"${video.viewCount.toHumanNumber()} ${
if (video.isLive) context.getString(
R.string.watching_now
) else context.getString(R.string.views)
}"
);
if (video.datetime != null) {
val diff = video.datetime?.getNowDiffSeconds() ?: 0;
val ago = video.datetime?.toHumanNowDiffString(true)
if(diff >= 0)
if (diff >= 0)
subTitleSegments.add("${ago} ago");
else
subTitleSegments.add("available in ${ago}");
@ -1436,20 +1468,27 @@ class VideoDetailView : ConstraintLayout {
fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
val queryReferencesResponse = ApiMethods.getQueryReferences(
PolycentricCache.SERVER, ref, null, null,
arrayListOf(
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
ByteString.copyFrom(Opinion.like.data)).build(),
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue(
ByteString.copyFrom(Opinion.dislike.data)).build()
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
.setFromType(ContentType.OPINION.value).setValue(
ByteString.copyFrom(Opinion.like.data)
).build(),
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
.setFromType(ContentType.OPINION.value).setValue(
ByteString.copyFrom(Opinion.dislike.data)
).build()
),
extraByteReferences = listOfNotNull(extraBytesRef)
);
val likes = queryReferencesResponse.countsList[0];
val dislikes = queryReferencesResponse.countsList[1];
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
val hasLiked =
StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
val hasDisliked =
StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
withContext(Dispatchers.Main) {
_rating.visibility = View.VISIBLE;
@ -1473,7 +1512,11 @@ class VideoDetailView : ConstraintLayout {
}
}
StatePolycentric.instance.updateLikeMap(ref, args.hasLiked, args.hasDisliked)
StatePolycentric.instance.updateLikeMap(
ref,
args.hasLiked,
args.hasDisliked
)
};
}
} catch (e: Throwable) {
@ -1495,6 +1538,7 @@ class VideoDetailView : ConstraintLayout {
_textDislikes.visibility = View.VISIBLE;
_textDislikes.text = r.dislikes.toHumanNumber();
}
is RatingLikes -> {
val r = video.rating as RatingLikes;
_layoutRating.visibility = View.VISIBLE;
@ -1506,6 +1550,7 @@ class VideoDetailView : ConstraintLayout {
_imageDislikeIcon.visibility = View.GONE;
_textDislikes.visibility = View.GONE;
}
else -> {
_layoutRating.visibility = View.GONE;
}
@ -1517,6 +1562,7 @@ class VideoDetailView : ConstraintLayout {
setLoading(false);
//Set Mediasource
val toResume = _videoResumePositionMilliseconds;
@ -1533,9 +1579,22 @@ class VideoDetailView : ConstraintLayout {
val historyItem = getHistoryIndex(videoDetail) ?: return@launch;
withContext(Dispatchers.Main) {
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong(), null, true);
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds");
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) {
_historicalPosition = StateHistory.instance.updateHistoryPosition(
video,
historyItem,
false,
(toResume.toFloat() / 1000.0f).toLong(),
null,
true
);
Logger.i(
TAG,
"Historical position: $_historicalPosition, last position: $lastPositionMilliseconds"
);
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(
_historicalPosition - lastPositionMilliseconds / 1000
) > 5.0
) {
_layoutResume.visibility = View.VISIBLE;
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
@ -1561,10 +1620,10 @@ class VideoDetailView : ConstraintLayout {
_liveChat?.stop();
_liveChat = null;
if(video.isLive && video.live != null) {
if (video.isLive && video.live != null) {
loadLiveChat(video);
}
if(video.isLive && video.live == null && !video.video.videoSources.any())
if (video.isLive && video.live == null && !video.video.videoSources.any())
startLiveTry(video);
@ -1945,7 +2004,7 @@ class VideoDetailView : ConstraintLayout {
?.map { x -> VideoHelper.selectBestVideoSource(videoSources.filter { x == it.height * it.width }, -1, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) }
?.plus(videoSources.filter { it is IHLSManifestSource || it is IDashManifestSource }))
?.distinct()
?.filter { it != null }
?.filterNotNull()
?.toList() ?: listOf() else videoSources?.toList() ?: listOf()
val bestAudioContainer = audioSources?.let { VideoHelper.selectBestAudioSource(it, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS)?.container };
val bestAudioSources = if(doDedup) audioSources
@ -2246,7 +2305,7 @@ class VideoDetailView : ConstraintLayout {
cleanupPlaybackTracker();
val url = _url;
if (url != null && url.isNotBlank()) {
if (!url.isNullOrBlank()) {
setLoading(true);
_taskLoadVideo.run(url);
}
@ -2258,7 +2317,7 @@ class VideoDetailView : ConstraintLayout {
if(fullscreen) {
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
val lp = _container_content.layoutParams as ConstraintLayout.LayoutParams;
val lp = _container_content.layoutParams as LayoutParams;
lp.topMargin = 0;
_container_content.layoutParams = lp;
@ -2271,7 +2330,7 @@ class VideoDetailView : ConstraintLayout {
else {
_layoutPlayerContainer.setPadding(0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6.0f, Resources.getSystem().displayMetrics).toInt());
val lp = _container_content.layoutParams as ConstraintLayout.LayoutParams;
val lp = _container_content.layoutParams as LayoutParams;
lp.topMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -18.0f, Resources.getSystem().displayMetrics).toInt();
_container_content.layoutParams = lp;
@ -2312,9 +2371,20 @@ class VideoDetailView : ConstraintLayout {
}
}
fun isLandscapeVideo(): Boolean? {
var videoSourceWidth = _player.exoPlayer?.player?.videoSize?.width
var videoSourceHeight = _player.exoPlayer?.player?.videoSize?.height
return if (videoSourceWidth == null || videoSourceHeight == null || videoSourceWidth == 0 || videoSourceHeight == 0){
null
} else{
videoSourceWidth >= videoSourceHeight
}
}
fun setFullscreen(fullscreen : Boolean) {
Logger.i(TAG, "setFullscreen(fullscreen=$fullscreen)")
_player.setFullScreen(fullscreen);
_player.setFullScreen(fullscreen)
}
private fun setLoading(isLoading : Boolean) {
if(isLoading){
@ -2505,7 +2575,8 @@ class VideoDetailView : ConstraintLayout {
_overlayContainer.removeAllViews();
_overlay_quality_selector?.hide();
_player.fillHeight();
_player.setFullScreen(true)
_player.fillHeight(false)
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
}
fun handleLeavePictureInPicture() {
@ -2641,7 +2712,7 @@ class VideoDetailView : ConstraintLayout {
else {
if(_player.layoutParams.height == WRAP_CONTENT) {
_player.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
_player.fillHeight();
_player.fillHeight(true)
_cast.layoutParams = _cast.layoutParams.apply {
(this as MarginLayoutParams).bottomMargin = 0;
};
@ -2714,13 +2785,24 @@ class VideoDetailView : ConstraintLayout {
if(_minimize_controls.isClickable != clickable)
_minimize_controls.isClickable = clickable;
}
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);
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
if (fragment.state == VideoDetailFragment.State.MINIMIZED) {
_player.fillHeight(true)
} else if (!fragment.isFullscreen) {
_player.fitHeight()
}
}
fun setTopPadding(value : Float) {
_player.setPadding(0, value.toInt(), _player.paddingRight, 0);
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

View file

@ -1,42 +0,0 @@
package com.futo.platformplayer.listeners
import android.content.Context
import android.database.ContentObserver
import android.os.Handler
import android.provider.Settings
class AutoRotateObserver(handler: Handler, private val onChangeCallback: () -> Unit) : ContentObserver(handler) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
onChangeCallback()
}
}
class AutoRotateChangeListener(context: Context, handler: Handler, private val onAutoRotateChanged: (Boolean) -> Unit) {
private val contentResolver = context.contentResolver
private val autoRotateObserver = AutoRotateObserver(handler) {
val isAutoRotateEnabled = isAutoRotateEnabled()
onAutoRotateChanged(isAutoRotateEnabled)
}
init {
contentResolver.registerContentObserver(
Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
false,
autoRotateObserver
)
}
fun unregister() {
contentResolver.unregisterContentObserver(autoRotateObserver)
}
private fun isAutoRotateEnabled(): Boolean {
return Settings.System.getInt(
contentResolver,
Settings.System.ACCELEROMETER_ROTATION,
0
) == 1
}
}

View file

@ -10,6 +10,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.Announcement
@ -35,6 +36,8 @@ class AnnouncementView : LinearLayout {
private val _category: String?;
private var _currentAnnouncement: Announcement? = null;
val onClose = Event0();
private val _scope: CoroutineScope?;
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
@ -101,6 +104,10 @@ class AnnouncementView : LinearLayout {
setAnnouncement(announcements.firstOrNull(), announcements.size);
}
fun isClosed(): Boolean{
return _currentAnnouncement == null
}
private fun setAnnouncement(announcement: Announcement?, count: Int) {
if(count == 0 && announcement == null)
Logger.i(TAG, "setAnnouncement announcement=$announcement count=$count");
@ -108,11 +115,12 @@ class AnnouncementView : LinearLayout {
_currentAnnouncement = announcement;
if (announcement == null) {
_root.visibility = View.GONE;
visibility = View.GONE
onClose.emit()
return;
}
_root.visibility = View.VISIBLE;
visibility = View.VISIBLE
_textTitle.text = announcement.title;
_textBody.text = announcement.msg;

View file

@ -20,7 +20,6 @@ import androidx.annotation.OptIn
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.setMargins
import androidx.media3.common.C
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.VideoSize
import androidx.media3.common.util.UnstableApi
@ -111,7 +110,9 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
private val _author_fullscreen: TextView;
private var _shouldRestartHideJobOnPlaybackStateChange: Boolean = false;
private var _lastSourceFit: Int? = null;
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;
@ -632,7 +633,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
private fun fitOrFill(fullScreen: Boolean) {
if (fullScreen) {
fillHeight();
fillHeight(false);
} else {
fitHeight();
}
@ -655,7 +656,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
gestureControl.resetZoomPan()
_lastSourceFit = null;
if(isFullScreen)
fillHeight();
fillHeight(false);
else if(_root.layoutParams.height != MATCH_PARENT)
fitHeight(videoSize);
}
@ -718,58 +719,73 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
//Sizing
@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;
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;
var w = videoSize?.width ?: lastVideoSource?.width ?: exoPlayer?.player?.videoSize?.width ?: 0;
var h = videoSize?.height ?: lastVideoSource?.height ?: exoPlayer?.player?.videoSize?.height
?: 0
var w =
videoSize?.width ?: lastVideoSource?.width ?: exoPlayer?.player?.videoSize?.width ?: 0
if(h == 0 && w == 0) {
Logger.i(TAG, "UNKNOWN VIDEO FIT: (videoSize: ${videoSize != null}, player.videoSize: ${exoPlayer?.player?.videoSize != null})");
w = 1280;
h = 720;
if (h == 0 && w == 0) {
Logger.i(
TAG,
"UNKNOWN VIDEO FIT: (videoSize: ${videoSize != null}, player.videoSize: ${exoPlayer?.player?.videoSize != null})"
);
w = 1280
h = 720
}
val configuration = resources.configuration
if(_lastSourceFit == null){
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
val windowWidth = configuration.screenWidthDp
val windowHeight = configuration.screenHeightDp
val viewWidth = Math.min(metrics.widthPixels, metrics.heightPixels); //TODO: Get parent width. was this.width
val deviceHeight = Math.max(metrics.widthPixels, metrics.heightPixels);
val maxHeight = deviceHeight * 0.4;
if (_lastSourceFit == null || windowWidth != _lastWindowWidth || windowHeight != _lastWindowHeight) {
val maxHeight = windowHeight * 0.4f
val determinedHeight = if(w > h)
((h * (viewWidth.toDouble() / w)).toInt())
val aspectRatio = h.toFloat() / w
val determinedHeight = (aspectRatio * windowWidth)
_lastSourceFit = determinedHeight
_lastSourceFit = _lastSourceFit!!.coerceAtLeast(220f)
_lastSourceFit = _lastSourceFit!!.coerceAtMost(maxHeight)
_desiredResizeModePortrait = if (_lastSourceFit != determinedHeight)
AspectRatioFrameLayout.RESIZE_MODE_FIT
else
((h * (viewWidth.toDouble() / w)).toInt());
_lastSourceFit = determinedHeight;
_lastSourceFit = Math.max(_lastSourceFit!!, 250);
_lastSourceFit = Math.min(_lastSourceFit!!, maxHeight.toInt());
if((_lastSourceFit ?: 0) < 300 || (_lastSourceFit ?: 0) > viewWidth) {
Log.d(TAG, "WEIRD HEIGHT DETECTED: ${_lastSourceFit}, Width: ${w}, Height: ${h}, VWidth: ${viewWidth}");
}
if(_lastSourceFit != determinedHeight)
_desiredResizeModePortrait = AspectRatioFrameLayout.RESIZE_MODE_FIT;
else
_desiredResizeModePortrait = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
_videoView.resizeMode = _desiredResizeModePortrait
}
AspectRatioFrameLayout.RESIZE_MODE_ZOOM
val marginBottom = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7f, resources.displayMetrics).toInt();
val rootParams = LayoutParams(LayoutParams.MATCH_PARENT, _lastSourceFit!! + marginBottom)
rootParams.bottomMargin = marginBottom;
_lastWindowWidth = windowWidth
_lastWindowHeight = windowHeight
}
_videoView.resizeMode = _desiredResizeModePortrait
val marginBottom =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 7f, resources.displayMetrics)
.toInt()
val height = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
_lastSourceFit!!,
resources.displayMetrics
)
val rootParams = LayoutParams(LayoutParams.MATCH_PARENT, (height + marginBottom).toInt())
rootParams.bottomMargin = marginBottom
_root.layoutParams = rootParams
isFitMode = true;
isFitMode = true
}
fun fillHeight(){
@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;
_originalBottomMargin =
if (layoutParams.bottomMargin > 0) layoutParams.bottomMargin else _originalBottomMargin;
layoutParams.setMargins(0);
_videoView.layoutParams = layoutParams;
_videoView.invalidate();
@ -777,6 +793,11 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
val rootParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
_root.layoutParams = rootParams;
_root.invalidate();
if(isMiniPlayer){
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM
}
isFitMode = false;
}

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<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"
@ -12,95 +13,108 @@
android:contentDescription="@string/cd_button_back"
android:padding="10dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_back_thin_white_16dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
<ImageButton
android:id="@+id/button_help"
android:layout_width="50dp"
android:layout_height="50dp"
android:contentDescription="@string/cd_button_help"
app:srcCompat="@drawable/ic_help"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_help" />
<ImageView
android:id="@+id/image_polycentric"
android:layout_width="80dp"
android:layout_height="80dp"
app:srcCompat="@drawable/neopass"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
<ScrollView
android:layout_width="match_parent"
android:layout_height="0px"
app:layout_constraintTop_toBottomOf="@id/button_help"
android:layout_marginTop="40dp"/>
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/text_polycentric"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/polycentric"
android:fontFamily="@font/inter_light"
android:textSize="32dp"
android:layout_marginTop="12dp"
app:layout_constraintTop_toBottomOf="@id/image_polycentric"
app:layout_constraintLeft_toLeftOf="@id/image_polycentric"
app:layout_constraintRight_toRightOf="@id/image_polycentric" />
<TextView
android:id="@+id/text_profile_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/profile_name"
android:fontFamily="@font/inter_light"
android:textSize="16dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@id/edit_profile_name"
app:layout_constraintLeft_toLeftOf="@id/edit_profile_name" />
<EditText
android:id="@+id/edit_profile_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/this_will_be_visible_to_other_users"
android:layout_marginStart="40dp"
android:layout_marginEnd="40dp"
android:layout_marginTop="60dp"
android:background="@drawable/background_16_round_4dp"
android:singleLine="true"
app:layout_constraintTop_toBottomOf="@id/text_polycentric"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toTopOf="@id/button_create_profile"/>
<LinearLayout
android:id="@+id/button_create_profile"
android:layout_width="140dp"
android:layout_height="40dp"
android:background="@drawable/background_button_primary_round"
android:gravity="center"
android:layout_marginTop="40dp"
android:layout_marginBottom="10dp"
android:orientation="horizontal"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_profile_name">
<TextView
android:layout_width="wrap_content"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_light"
android:textColor="@color/white"
android:textSize="16dp"
android:text="@string/create_profile" />
</LinearLayout>
<com.futo.platformplayer.views.LoaderView
android:id="@+id/loader"
android:layout_width="match_parent"
android:layout_height="50dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/button_create_profile"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
android:background="@color/black">
<ImageView
android:id="@+id/image_polycentric"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginTop="40dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/neopass" />
<TextView
android:id="@+id/text_polycentric"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:fontFamily="@font/inter_light"
android:text="@string/polycentric"
android:textSize="32dp"
app:layout_constraintLeft_toLeftOf="@id/image_polycentric"
app:layout_constraintRight_toRightOf="@id/image_polycentric"
app:layout_constraintTop_toBottomOf="@id/image_polycentric" />
<TextView
android:id="@+id/text_profile_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:fontFamily="@font/inter_light"
android:text="@string/profile_name"
android:textSize="16dp"
app:layout_constraintBottom_toTopOf="@id/edit_profile_name"
app:layout_constraintLeft_toLeftOf="@id/edit_profile_name" />
<EditText
android:id="@+id/edit_profile_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginTop="60dp"
android:layout_marginEnd="40dp"
android:background="@drawable/background_16_round_4dp"
android:hint="@string/this_will_be_visible_to_other_users"
android:singleLine="true"
app:layout_constraintBottom_toTopOf="@id/button_create_profile"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_polycentric" />
<LinearLayout
android:id="@+id/button_create_profile"
android:layout_width="140dp"
android:layout_height="40dp"
android:layout_marginTop="40dp"
android:layout_marginBottom="10dp"
android:background="@drawable/background_button_primary_round"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_profile_name">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_light"
android:text="@string/create_profile"
android:textColor="@color/white"
android:textSize="16dp" />
</LinearLayout>
<com.futo.platformplayer.views.LoaderView
android:id="@+id/loader"
android:layout_width="match_parent"
android:layout_height="50dp"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_create_profile" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="video_view_right_padding"></dimen>
<dimen name="minimized_player_max_width">500dp</dimen>
<dimen name="app_bar_height">200dp</dimen>
</resources>
<dimen name="landscape_threshold">300dp</dimen>
</resources>

View file

@ -259,7 +259,7 @@
<string name="add_a_comment">Add a comment…</string>
<string name="dismiss">Dismiss</string>
<string name="scan_a_qr_code_to_install">Scan a QR code to install</string>
<string name="toggle_fullscreen">Toggle fullscreen</string>
<string name="toggle_fullscreen">Toggle full-screen</string>
<string name="by">By</string>
<string name="signature">Signature</string>
<string name="valid">Valid</string>
@ -370,11 +370,11 @@
<string name="brightness_slider">Brightness slider</string>
<string name="brightness_slider_descr">Enable slide gesture to change brightness</string>
<string name="toggle_full_screen">Toggle full screen</string>
<string name="toggle_full_screen_descr">Enable swipe gesture to toggle fullscreen</string>
<string name="toggle_full_screen_descr">Enable swipe gesture to toggle full screen</string>
<string name="system_brightness">System brightness</string>
<string name="system_brightness_descr">Gesture controls adjust system brightness</string>
<string name="restore_system_brightness">Restore system brightness</string>
<string name="restore_system_brightness_descr">Restore system brightness when exiting fullscreen</string>
<string name="restore_system_brightness_descr">Restore system brightness when exiting full screen</string>
<string name="zoom_option">Enable zoom</string>
<string name="zoom_option_descr">Enable two finger pinch zoom gesture</string>
<string name="pan_option">Enable pan</string>
@ -382,7 +382,7 @@
<string name="system_volume">System volume</string>
<string name="system_volume_descr">Gesture controls adjust system volume</string>
<string name="live_chat_webview">Live Chat Webview</string>
<string name="full_screen_portrait">Fullscreen portrait</string>
<string name="full_screen_portrait">Full-screen portrait</string>
<string name="reverse_portrait">Allow reverse portrait</string>
<string name="reverse_portrait_description">Allow app to flip into reverse portrait</string>
<string name="rotation_zone">Rotation zone</string>
@ -396,12 +396,12 @@
<string name="prefer_webm_audio">Prefer Webm Audio Codecs</string>
<string name="prefer_webm_audio_description">If player should prefer Webm codecs (opus) over mp4 codecs (AAC), may result in worse compatibility.</string>
<string name="allow_under_cutout">Allow video under cutout</string>
<string name="allow_under_cutout_description">Allow video to go underneath the screen cutout in full-screen.\nMay require restart</string>
<string name="allow_under_cutout_description">Allow video to go underneath the screen cutout in full screen.\nMay require restart</string>
<string name="autoplay">Enable autoplay by default</string>
<string name="autoplay_description">Autoplay will be enabled by default whenever you watch a video</string>
<string name="allow_full_screen_portrait">Allow full-screen portrait</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="allow_full_screen_portrait">Allow fullscreen portrait</string>
<string name="background_switch_audio">Switch to Audio in Background</string>
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>
<string name="subscription_group_menu">Groups</string>
@ -853,7 +853,7 @@
<string name="cd_button_loop">Loop</string>
<string name="cd_button_previous">Previous</string>
<string name="cd_button_next">Next</string>
<string name="cd_button_fullscreen">Fullscreen</string>
<string name="cd_button_fullscreen">Full screen</string>
<string name="cd_button_autoplay">Autoplay</string>
<string name="cd_update_spinner">Update spinner</string>
<string name="cd_button_play">Play</string>