diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9ef606c7..6c69fb32 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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 @@ - - @@ -217,7 +213,6 @@ android:name=".activities.ManageTabsActivity" android:screenOrientation="sensorPortrait" android:theme="@style/Theme.FutoVideo.NoActionBar" /> - () - - 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" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index 76b06525..492b7c39 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -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) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt index f0315665..c4afcf74 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt @@ -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? = null; + private var currentButtonDefinitions: List? = 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() 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); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt index 908730fe..6dccdde8 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt @@ -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 : FeedView, LinearLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null) : super(fragment, inflater, cachedRecyclerData) { - - } + constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData, GridLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null) : super(fragment, inflater, cachedRecyclerData) override fun filterResults(results: List): List { return results; @@ -55,16 +53,10 @@ abstract class ContentFeedView : FeedView): InsertedViewAdapterWithLoader { 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 : FeedView() .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 : FeedView, LinearLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>) { + override fun onRestoreCachedData(cachedData: RecyclerData, GridLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>) { 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 : FeedView 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 : FeedView : FeedView : FeedView, 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): InsertedViewAdapterWithLoader { return InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(), @@ -34,18 +28,31 @@ abstract class CreatorFeedView : FeedView : 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 : L private var _loading: Boolean = true; - private val _pager_lock = Object(); + private val _pagerLock = Object(); private var _cache: ItemCache? = null; open val visibleThreshold = 15; @@ -58,21 +59,21 @@ abstract class FeedView : L private var _activeTags: List? = null; private var _nextPageHandler: TaskHandler>; - val recyclerData: RecyclerData, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder>; + val recyclerData: RecyclerData, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder>; val fragment: TFragment; private val _scrollListener: RecyclerView.OnScrollListener; private var _automaticNextPageCounter = 0; - constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder>? = null) : super(inflater.context) { + constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder>? = 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 : 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 : 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 : 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 : L protected open fun setActiveTags(activeTags: List?) { _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 : L protected open fun setSortByOptions(options: List?) { _sortByOptions = options; - if (options != null && options.isNotEmpty()) { + if (!options.isNullOrEmpty()) { val allOptions = arrayListOf(); allOptions.add("Default"); allOptions.addAll(options); @@ -277,19 +289,19 @@ abstract class FeedView : L } } protected abstract fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList): InsertedViewAdapterWithLoader; - protected abstract fun createLayoutManager(recyclerResults: RecyclerView, context: Context): LinearLayoutManager; - protected open fun onRestoreCachedData(cachedData: RecyclerData, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder>) {} + protected abstract fun createLayoutManager(recyclerResults: RecyclerView, context: Context): GridLayoutManager; + protected open fun onRestoreCachedData(cachedData: RecyclerData, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder>) {} 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 : L //insertPagerResults(_cache!!.cachePager.getResults(), false); } fun setPager(pager: TPager, cache: ItemCache? = null) { - synchronized(_pager_lock) { + synchronized(_pagerLock) { detachParentPagerEvents(); detachPagerEvents(); @@ -425,7 +437,7 @@ abstract class FeedView : 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 : 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 : L } companion object { - private val TAG = "FeedView"; + private const val TAG = "FeedView"; } abstract class ItemCache(val cachePager: IPager) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt index 3ddddbf0..4ac4557b 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt @@ -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, LinearLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null; + private var _cachedRecyclerData: FeedView.RecyclerData, GridLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null; fun reloadFeed() { _view?.reloadFeed() @@ -101,15 +94,19 @@ class HomeFragment : MainFragment() { class HomeView : ContentFeedView { 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>; override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar - constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData, LinearLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null) : super(fragment, inflater, cachedRecyclerData) { - _announcementsView = AnnouncementView(context, null).apply { - headerView.addView(this); - }; + constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData, GridLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null) : super(fragment, inflater, cachedRecyclerData) { _taskGetPager = TaskHandler>({ 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(); }.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 {} } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt index 05f064aa..b1d630e4 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt @@ -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, LinearLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null; + private var _cachedRecyclerData: FeedView.RecyclerData, GridLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = 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, LinearLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null) : super(fragment, inflater, cachedRecyclerData) { + constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData, GridLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = 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(c); }; _subscriptionBar?.onToggleGroup?.subscribe { g -> @@ -397,7 +397,7 @@ class SubscriptionsFeedFragment : MainFragment() { _taskGetPager.run(withRefetch); } - override fun onRestoreCachedData(cachedData: RecyclerData, LinearLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>) { + override fun onRestoreCachedData(cachedData: RecyclerData, GridLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>) { 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().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 {} } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/TutorialFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/TutorialFragment.kt index 9fddac25..45c1d0a2 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/TutorialFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/TutorialFragment.kt @@ -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( diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt index 233d9dc4..ea7bf5f1 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt @@ -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(); 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(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 {} } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index d2bbf1b5..a3f403e6 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -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(); val onEnterPictureInPicture = Event0(); val onPlayChanged = Event1(); + val onVideoChanged = Event2() 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 = 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 = 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 diff --git a/app/src/main/java/com/futo/platformplayer/listeners/AutoRotateChangeListener.kt b/app/src/main/java/com/futo/platformplayer/listeners/AutoRotateChangeListener.kt deleted file mode 100644 index 12efb3aa..00000000 --- a/app/src/main/java/com/futo/platformplayer/listeners/AutoRotateChangeListener.kt +++ /dev/null @@ -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 - } -} diff --git a/app/src/main/java/com/futo/platformplayer/views/announcements/AnnouncementView.kt b/app/src/main/java/com/futo/platformplayer/views/announcements/AnnouncementView.kt index 7767265f..467f23c4 100644 --- a/app/src/main/java/com/futo/platformplayer/views/announcements/AnnouncementView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/announcements/AnnouncementView.kt @@ -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; diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt index 3f465269..76a8a0f7 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt @@ -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; } diff --git a/app/src/main/res/layout/activity_polycentric_create_profile.xml b/app/src/main/res/layout/activity_polycentric_create_profile.xml index 198488fd..57226b68 100644 --- a/app/src/main/res/layout/activity_polycentric_create_profile.xml +++ b/app/src/main/res/layout/activity_polycentric_create_profile.xml @@ -1,5 +1,6 @@ - + app:srcCompat="@drawable/ic_back_thin_white_16dp" /> + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/ic_help" /> - + app:layout_constraintBottom_toBottomOf="parent"> - - - - - - - - - - - + android:background="@color/black"> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimensions.xml b/app/src/main/res/values/dimensions.xml index d3c299e7..5e75240b 100644 --- a/app/src/main/res/values/dimensions.xml +++ b/app/src/main/res/values/dimensions.xml @@ -1,5 +1,6 @@ - + 500dp 200dp - \ No newline at end of file + 300dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ad3a4c49..2b809271 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -259,7 +259,7 @@ Add a comment… Dismiss Scan a QR code to install - Toggle fullscreen + Toggle full-screen By Signature Valid @@ -370,11 +370,11 @@ Brightness slider Enable slide gesture to change brightness Toggle full screen - Enable swipe gesture to toggle fullscreen + Enable swipe gesture to toggle full screen System brightness Gesture controls adjust system brightness Restore system brightness - Restore system brightness when exiting fullscreen + Restore system brightness when exiting full screen Enable zoom Enable two finger pinch zoom gesture Enable pan @@ -382,7 +382,7 @@ System volume Gesture controls adjust system volume Live Chat Webview - Fullscreen portrait + Full-screen portrait Allow reverse portrait Allow app to flip into reverse portrait Rotation zone @@ -396,12 +396,12 @@ Prefer Webm Audio Codecs If player should prefer Webm codecs (opus) over mp4 codecs (AAC), may result in worse compatibility. Allow video under cutout - Allow video to go underneath the screen cutout in full-screen.\nMay require restart + Allow video to go underneath the screen cutout in full screen.\nMay require restart Enable autoplay by default Autoplay will be enabled by default whenever you watch a video + Allow full-screen portrait Delete from WatchLater when watched After you leave a video that you mostly watched, it will be removed from watch later. - Allow fullscreen portrait Switch to Audio in Background Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter Groups @@ -853,7 +853,7 @@ Loop Previous Next - Fullscreen + Full screen Autoplay Update spinner Play