Compare commits

..

8 Commits

Author SHA1 Message Date
Kelvin 0ae05e7cd4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-06 19:46:14 +01:00
Kelvin b284176072 Loop support, Improve add to queue behavior, home retry, fix history search pager, defaults progressbar 2023-12-06 19:46:09 +01:00
Koen 5fffaf2f4e Added next/previous skip buttons. 2023-12-06 19:40:05 +01:00
Koen 58da91eae8 Made history properly reload. 2023-12-06 16:47:22 +01:00
Koen 98d92d3fe2 Updated HistoryView to use pager. 2023-12-06 16:32:17 +01:00
Koen c5d35b27f0 Cleanup on store PR. 2023-12-06 13:41:07 +01:00
Koen aee5b75c2f Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2023-12-06 10:34:32 +01:00
Koen fe02197bd8 Fixes to Polycentric flows. 2023-12-06 10:34:20 +01:00
35 changed files with 651 additions and 367 deletions
@@ -181,7 +181,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = false;
var progressBar: Boolean = true;
@FormField(R.string.clear_hidden, FieldForm.BUTTON, R.string.clear_hidden_description, 8)
@@ -212,7 +212,7 @@ class Settings : FragmentedStorageFileJson() {
var previewFeedItems: Boolean = true;
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = false;
var progressBar: Boolean = true;
fun getSearchFeedStyle(): FeedStyle {
@@ -230,7 +230,7 @@ class Settings : FragmentedStorageFileJson() {
class ChannelSettings {
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = false;
var progressBar: Boolean = true;
}
@FormField(R.string.subscriptions, "group", R.string.configure_how_your_subscriptions_works_and_feels, 4)
@@ -252,7 +252,7 @@ class Settings : FragmentedStorageFileJson() {
var previewFeedItems: Boolean = true;
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = false;
var progressBar: Boolean = true;
@FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 7)
@Serializable(with = FlexibleBooleanSerializer::class)
@@ -101,10 +101,6 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
return@TaskHandler it.getResults();
}).success {
setLoading(false);
if (it.isEmpty()) {
return@success;
}
val posBefore = _results.size;
val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }
_results.addAll(toAdd);
@@ -54,6 +54,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import okhttp3.internal.platform.Platform
@Serializable
data class PolycentricProfile(val system: PublicKey, val systemState: SystemState, val ownedClaims: List<OwnedClaim>);
@@ -298,7 +299,7 @@ class ChannelFragment : MainFragment() {
Glide.with(_imageBanner)
.clear(_imageBanner);
_taskLoadPolycentricProfile.run(parameter.id);
loadPolycentricProfile(parameter.id, parameter.url)
};
_url = parameter.url;
@@ -311,7 +312,7 @@ class ChannelFragment : MainFragment() {
Glide.with(_imageBanner)
.clear(_imageBanner);
_taskLoadPolycentricProfile.run(parameter.channel.id);
loadPolycentricProfile(parameter.channel.id, parameter.channel.url)
};
_url = parameter.channel.url;
@@ -327,6 +328,18 @@ class ChannelFragment : MainFragment() {
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex));
}
private fun loadPolycentricProfile(id: PlatformID, url: String) {
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true);
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = true)
if (cachedPolycentricProfile.expired) {
_taskLoadPolycentricProfile.run(id);
}
} else {
_taskLoadPolycentricProfile.run(id);
}
}
private fun setLoading(isLoading: Boolean) {
if (_isLoading == isLoading) {
return;
@@ -448,8 +461,6 @@ class ChannelFragment : MainFragment() {
}
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
Log.i(TAG, "setPolycentricProfile(cachedPolycentricProfile = $cachedPolycentricProfile, animate = $animate)")
val dp_35 = 35.dp(resources)
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
@@ -168,7 +168,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
protected open fun onContentClicked(content: IPlatformContent, time: Long) {
if(content is IPlatformVideo) {
if (StatePlayer.instance.hasQueue) {
StatePlayer.instance.addToQueue(content)
StatePlayer.instance.insertToQueue(content, true);
} else {
if (Settings.instance.playback.shouldResumePreview(time))
fragment.navigate<VideoDetailFragment>(content.withTimestamp(time)).maximizeVideoDetail();
@@ -132,10 +132,6 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
}).success {
setLoading(false);
if (it.isEmpty()) {
return@success;
}
val posBefore = recyclerData.results.size;
val filteredResults = filterResults(it);
recyclerData.results.addAll(filteredResults);
@@ -1,5 +1,6 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
@@ -8,88 +9,285 @@ import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.structures.IAsyncPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.PlatformContentPager
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.views.others.TagsView
import com.futo.platformplayer.views.adapters.HistoryListAdapter
import com.futo.platformplayer.views.adapters.HistoryListViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class HistoryFragment : MainFragment() {
override val isMainView : Boolean = true;
override val isTab: Boolean = true;
override val hasBottomBar: Boolean get() = true;
private var _adapter: HistoryListAdapter? = null;
private var _view: HistoryView? = null;
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_history, container, false);
val inputMethodManager = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
val recyclerHistory = view.findViewById<RecyclerView>(R.id.recycler_history);
val clearSearch = view.findViewById<ImageButton>(R.id.button_clear_search);
val editSearch = view.findViewById<EditText>(R.id.edit_search);
var tagsView = view.findViewById<TagsView>(R.id.tags_text);
tagsView.setPairs(listOf(
Pair(getString(R.string.last_hour), 60L),
Pair(getString(R.string.last_24_hours), 24L * 60L),
Pair(getString(R.string.last_week), 7L * 24L * 60L),
Pair(getString(R.string.last_30_days), 30L * 24L * 60L),
Pair(getString(R.string.last_year), 365L * 30L * 24L * 60L),
Pair(getString(R.string.all_time), -1L)));
val adapter = HistoryListAdapter();
adapter.onClick.subscribe { v ->
val diff = v.video.duration - v.position;
val vid: Any = if (diff > 5) { v.video.withTimestamp(v.position) } else { v.video };
StatePlayer.instance.clearQueue();
navigate<VideoDetailFragment>(vid).maximizeVideoDetail();
editSearch.clearFocus();
inputMethodManager.hideSoftInputFromWindow(editSearch.windowToken, 0);
};
_adapter = adapter;
recyclerHistory.adapter = adapter;
recyclerHistory.isSaveEnabled = false;
recyclerHistory.layoutManager = LinearLayoutManager(context);
tagsView.onClick.subscribe { timeMinutesToErase ->
UIDialogs.showConfirmationDialog(requireContext(), getString(R.string.are_you_sure_delete_historical), {
StateHistory.instance.removeHistoryRange(timeMinutesToErase.second as Long);
UIDialogs.toast(view.context, timeMinutesToErase.first + " " + getString(R.string.removed));
adapter.updateFilteredVideos();
adapter.notifyDataSetChanged();
});
};
clearSearch.setOnClickListener {
editSearch.text.clear();
clearSearch.visibility = View.GONE;
adapter.setQuery("");
editSearch.clearFocus();
inputMethodManager.hideSoftInputFromWindow(editSearch.windowToken, 0);
};
editSearch.addTextChangedListener { _ ->
val text = editSearch.text;
clearSearch.visibility = if (text.isEmpty()) { View.GONE } else { View.VISIBLE };
adapter.setQuery(text.toString());
};
val view = HistoryView(this, inflater);
_view = view;
return view;
}
override fun onDestroyMainView() {
super.onDestroyMainView();
_adapter?.cleanup();
_adapter = null;
_view = null;
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack)
_view?.setPager(StateHistory.instance.getHistoryPager());
}
@SuppressLint("ViewConstructor")
class HistoryView : LinearLayout {
private val _fragment: HistoryFragment;
private val _adapter: InsertedViewAdapterWithLoader<HistoryListViewHolder>;
private val _recyclerHistory: RecyclerView;
private val _clearSearch: ImageButton;
private val _editSearch: EditText;
private val _tagsView: TagsView;
private val _llmHistory: LinearLayoutManager;
private val _pagerLock = Object();
private var _nextPageHandler: TaskHandler<IPager<HistoryVideo>, List<HistoryVideo>>;
private var _pager: IPager<HistoryVideo>? = null;
private val _results = arrayListOf<HistoryVideo>();
private var _loading = false;
private var _automaticNextPageCounter = 0;
constructor(fragment: HistoryFragment, inflater: LayoutInflater) : super(inflater.context) {
_fragment = fragment;
inflater.inflate(R.layout.fragment_history, this);
_recyclerHistory = findViewById(R.id.recycler_history);
_clearSearch = findViewById(R.id.button_clear_search);
_editSearch = findViewById(R.id.edit_search);
_tagsView = findViewById(R.id.tags_text);
_tagsView.setPairs(listOf(
Pair(context.getString(R.string.last_hour), 60L),
Pair(context.getString(R.string.last_24_hours), 24L * 60L),
Pair(context.getString(R.string.last_week), 7L * 24L * 60L),
Pair(context.getString(R.string.last_30_days), 30L * 24L * 60L),
Pair(context.getString(R.string.last_year), 365L * 30L * 24L * 60L),
Pair(context.getString(R.string.all_time), -1L)
));
_adapter = InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
{ _results.size },
{ view, _ ->
val holder = HistoryListViewHolder(view);
holder.onRemove.subscribe(::onHistoryVideoRemove);
holder.onClick.subscribe(::onHistoryVideoClick);
return@InsertedViewAdapterWithLoader holder;
},
{ viewHolder, position ->
var watchTime: String? = null;
if (position == 0) {
watchTime = _results[position].date.toHumanNowDiffStringMinDay();
} else {
val previousWatchTime = _results[position - 1].date.toHumanNowDiffStringMinDay();
val currentWatchTime = _results[position].date.toHumanNowDiffStringMinDay();
if (previousWatchTime != currentWatchTime) {
watchTime = currentWatchTime;
}
}
viewHolder.bind(_results[position], watchTime);
}
);
_recyclerHistory.adapter = _adapter;
_recyclerHistory.isSaveEnabled = false;
_llmHistory = LinearLayoutManager(context);
_recyclerHistory.layoutManager = _llmHistory;
_tagsView.onClick.subscribe { timeMinutesToErase ->
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_delete_historical), {
StateHistory.instance.removeHistoryRange(timeMinutesToErase.second as Long);
UIDialogs.toast(context, timeMinutesToErase.first + " " + context.getString(R.string.removed));
updatePager();
});
};
_clearSearch.setOnClickListener {
val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
_editSearch.text.clear();
_clearSearch.visibility = View.GONE;
setPager(StateHistory.instance.getHistoryPager());
_editSearch.clearFocus();
inputMethodManager.hideSoftInputFromWindow(_editSearch.windowToken, 0);
};
_editSearch.addTextChangedListener { _ ->
val text = _editSearch.text;
_clearSearch.visibility = if (text.isEmpty()) { View.GONE } else { View.VISIBLE };
updatePager();
};
_recyclerHistory.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy);
val visibleItemCount = _recyclerHistory.childCount;
val firstVisibleItem = _llmHistory.findFirstVisibleItemPosition();
Logger.i(TAG, "onScrolled _loading = $_loading, firstVisibleItem = $firstVisibleItem, visibleItemCount = $visibleItemCount, _results.size = ${_results.size}")
val visibleThreshold = 15;
if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= _results.size && firstVisibleItem > 0) {
loadNextPage();
}
}
});
_nextPageHandler = TaskHandler<IPager<HistoryVideo>, List<HistoryVideo>>({fragment.lifecycleScope}, {
if (it is IAsyncPager<*>)
it.nextPageAsync();
else
it.nextPage();
return@TaskHandler it.getResults();
}).success {
setLoading(false);
val posBefore = _results.size;
_results.addAll(it);
_adapter.notifyItemRangeInserted(_adapter.childToParentPosition(posBefore), it.size);
ensureEnoughContentVisible(it)
}.exception<Throwable> {
Logger.w(TAG, "Failed to load next page.", it);
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
loadNextPage();
});
};
}
private fun updatePager() {
val query = _editSearch.text.toString();
if (_editSearch.text.isNotEmpty()) {
setPager(StateHistory.instance.getHistorySearchPager(query));
//setPager(StateHistory.instance.getHistorySearchPager(query));
} else {
setPager(StateHistory.instance.getHistoryPager());
}
}
fun setPager(pager: IPager<HistoryVideo>) {
Logger.i(TAG, "setPager()");
synchronized(_pagerLock) {
loadPagerInternal(pager);
}
}
private fun onHistoryVideoRemove(v: HistoryVideo) {
val index = _results.indexOf(v);
if (index == -1) {
return;
}
StateHistory.instance.removeHistory(v.video.url);
_results.removeAt(index);
_adapter.notifyItemRemoved(index);
}
private fun onHistoryVideoClick(v: HistoryVideo) {
val index = _results.indexOf(v);
if (index == -1) {
return;
}
val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
val diff = v.video.duration - v.position;
val vid: Any = if (diff > 5) { v.video.withTimestamp(v.position) } else { v.video };
StatePlayer.instance.clearQueue();
_fragment.navigate<VideoDetailFragment>(vid).maximizeVideoDetail();
_editSearch.clearFocus();
inputMethodManager.hideSoftInputFromWindow(_editSearch.windowToken, 0);
_fragment.lifecycleScope.launch(Dispatchers.Main) {
delay(2000)
updatePager()
}
}
private fun loadNextPage() {
synchronized(_pagerLock) {
val pager: IPager<HistoryVideo> = _pager ?: return;
val hasMorePages = pager.hasMorePages();
Logger.i(TAG, "loadNextPage() hasMorePages=$hasMorePages");
if (pager.hasMorePages()) {
setLoading(true);
_nextPageHandler.run(pager);
}
}
}
private fun setLoading(loading: Boolean) {
Logger.v(TAG, "setLoading loading=${loading}");
_loading = loading;
_adapter.setLoading(loading);
}
private fun loadPagerInternal(pager: IPager<HistoryVideo>) {
Logger.i(TAG, "Setting new internal pager on feed");
_results.clear();
val toAdd = pager.getResults();
_results.addAll(toAdd);
_adapter.notifyDataSetChanged();
ensureEnoughContentVisible(toAdd)
_pager = pager;
}
private fun ensureEnoughContentVisible(results: List<HistoryVideo>) {
val canScroll = if (_results.isEmpty()) false else {
val layoutManager = _llmHistory
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
if (firstVisibleItemPosition != RecyclerView.NO_POSITION) {
val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition)
val itemHeight = firstVisibleView?.height ?: 0
val occupiedSpace = _results.size * itemHeight
val recyclerViewHeight = _recyclerHistory.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 || results.isEmpty()) {
_automaticNextPageCounter++
if(_automaticNextPageCounter <= 4)
loadNextPage()
} else {
_automaticNextPageCounter = 0;
}
}
}
companion object {
fun newInstance() = HistoryFragment().apply {}
private const val TAG = "HistoryFragment"
}
}
@@ -596,9 +596,12 @@ class PostDetailFragment : MainFragment {
private fun fetchPolycentricProfile() {
val author = _post?.author ?: _postOverview?.author ?: return;
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(author.url);
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(author.url, true);
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = false);
if (cachedPolycentricProfile.expired) {
_taskLoadPolycentricProfile.run(author.id);
}
} else {
setPolycentricProfile(null, animate = false);
_taskLoadPolycentricProfile.run(author.id);
@@ -465,6 +465,8 @@ class VideoDetailView : ConstraintLayout {
nextVideo();
};
_player.onDatasourceError.subscribe(::onDataSourceError);
_player.onNext.subscribe { nextVideo(true) };
_player.onPrevious.subscribe { previousVideo(true) };
_minimize_controls_play.setOnClickListener { handlePlay(); };
_minimize_controls_pause.setOnClickListener { handlePause(); };
@@ -542,7 +544,7 @@ class VideoDetailView : ConstraintLayout {
MediaControlReceiver.onPlayReceived.subscribe(this) { handlePlay() };
MediaControlReceiver.onPauseReceived.subscribe(this) { handlePause() };
MediaControlReceiver.onNextReceived.subscribe(this) { nextVideo(true) };
MediaControlReceiver.onPreviousReceived.subscribe(this) { prevVideo() };
MediaControlReceiver.onPreviousReceived.subscribe(this) { previousVideo(true) };
MediaControlReceiver.onCloseReceived.subscribe(this) {
Logger.i(TAG, "MediaControlReceiver.onCloseReceived")
onClose.emit()
@@ -1003,14 +1005,17 @@ class VideoDetailView : ConstraintLayout {
_descriptionContainer.visibility = View.GONE;
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
_channelName.text = video.author.name;
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url);
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url, true);
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = false);
if (cachedPolycentricProfile.expired) {
_taskLoadPolycentricProfile.run(video.author.id);
}
} else {
setPolycentricProfile(null, animate = false);
_taskLoadPolycentricProfile.run(video.author.id);
_channelName.text = video.author.name;
}
_player.clear();
@@ -1157,7 +1162,7 @@ class VideoDetailView : ConstraintLayout {
setDescription(video.description.fixHtmlLinks());
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url);
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url, true);
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = false);
} else {
@@ -1529,25 +1534,63 @@ class VideoDetailView : ConstraintLayout {
_slideUpOverlay = _overlay_quality_selector;
}
fun prevVideo() {
Logger.i(TAG, "prevVideo")
val next = StatePlayer.instance.prevQueueItem(_player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
if(next != null) {
setVideoOverview(next);
private fun getPreviousVideo(withoutRemoval: Boolean, forceLoop: Boolean = false): IPlatformVideo? {
if (!StatePlayer.instance.hasQueue) {
if (forceLoop) {
return StatePlayer.instance.currentVideo
} else {
return null
}
}
val shouldNotRemove = _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9
var previous = StatePlayer.instance.prevQueueItem(withoutRemoval || shouldNotRemove);
if(previous == null && forceLoop)
previous = StatePlayer.instance.getQueue().last();
return previous;
}
private fun getNextVideo(withoutRemoval: Boolean, forceLoop: Boolean = false): IPlatformVideo? {
if (!StatePlayer.instance.hasQueue) {
if (forceLoop) {
return StatePlayer.instance.currentVideo
} else {
return null
}
}
val shouldNotRemove = _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9
var next = StatePlayer.instance.nextQueueItem(withoutRemoval || shouldNotRemove);
if(next == null && forceLoop)
next = StatePlayer.instance.restartQueue();
return next;
}
fun previousVideo(forceLoop: Boolean = false): Boolean {
Logger.i(TAG, "previousVideo")
val previous = getPreviousVideo(false, forceLoop);
if(previous != null) {
setVideoOverview(previous);
return true;
} else {
StatePlayer.instance.setCurrentlyPlaying(null);
}
return false;
}
fun nextVideo(forceLoop: Boolean = false): Boolean {
Logger.i(TAG, "nextVideo")
var next = StatePlayer.instance.nextQueueItem(_player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
if(next == null && forceLoop)
next = StatePlayer.instance.restartQueue();
val next = getNextVideo(false, forceLoop);
if(next != null) {
setVideoOverview(next);
return true;
}
else
} else {
StatePlayer.instance.setCurrentlyPlaying(null);
}
return false;
}
@@ -1687,6 +1730,10 @@ class VideoDetailView : ConstraintLayout {
private fun updateQueueState() {
_upNext.update();
/*_player.updateNextPrevious(
getPreviousVideo(withoutRemoval = true, forceLoop = true) != null,
getNextVideo(withoutRemoval = true, forceLoop = true) != null
)*/
}
//Handlers
@@ -2189,6 +2236,11 @@ class VideoDetailView : ConstraintLayout {
_creatorThumbnail.setHarborAvailable(profile != null, animate);
}
val username = cachedPolycentricProfile?.profile?.systemState?.username
if (username != null) {
_channelName.text = username
}
_monetization.setPolycentricProfile(cachedPolycentricProfile, animate);
}
@@ -50,7 +50,9 @@ class PolycentricCache {
ContentType.MEMBERSHIP_URLS.value,
ContentType.DONATION_DESTINATIONS.value
)
).eventsList.map { e -> SignedEvent.fromProto(e) };
).eventsList.map { e -> SignedEvent.fromProto(e) }
.groupBy { e -> e.event.contentType }
.map { (_, events) -> events.maxBy { it.event.unixMilliseconds ?: 0 } };
val storageSystemState = StorageTypeSystemState.create()
for (signedEvent in signedProfileEvents) {
@@ -553,27 +553,6 @@ class StateApp {
if(StateHistory.instance.shouldMigrateLegacyHistory())
StateHistory.instance.migrateLegacyHistory();
if(false) {
/*
Logger.i(TAG, "TEST:--------(200)---------");
testHistoryDB(200);
Logger.i(TAG, "TEST:--------(1000)---------");
testHistoryDB(1000);
Logger.i(TAG, "TEST:--------(2000)---------");
testHistoryDB(2000);
Logger.i(TAG, "TEST:--------(4000)---------");
testHistoryDB(4000);
Logger.i(TAG, "TEST:--------(6000)---------");
testHistoryDB(6000);
Logger.i(TAG, "TEST:--------(100000)---------");
scope.launch(Dispatchers.Default) {
StateHistory.instance.testHistoryDB(100000);
}
*/
}
}
fun mainAppStartedWithExternalFiles(context: Context) {
@@ -39,27 +39,16 @@ class StateCache {
}
fun getChannelCachePager(channelUrl: String): IPager<IPlatformContent> {
val result: IPager<IPlatformContent>;
val time = measureTimeMillis {
result = _subscriptionCache.queryPager(DBSubscriptionCache.Index::channelUrl, channelUrl, 20) {
it.obj;
}
}
return result;
return _subscriptionCache.queryPager(DBSubscriptionCache.Index::channelUrl, channelUrl, 20) { it.obj }
}
fun getAllChannelCachePager(channelUrls: List<String>): IPager<IPlatformContent> {
val result: IPager<IPlatformContent>;
val time = measureTimeMillis {
result = _subscriptionCache.queryInPager(DBSubscriptionCache.Index::channelUrl, channelUrls, 20) {
it.obj;
}
}
return result;
return _subscriptionCache.queryInPager(DBSubscriptionCache.Index::channelUrl, channelUrls, 20) { it.obj }
}
fun getChannelCachePager(channelUrls: List<String>): IPager<IPlatformContent> {
val pagers = MultiChronoContentPager(channelUrls.map { _subscriptionCache.queryPager(DBSubscriptionCache.Index::channelUrl, it, 20) {
fun getChannelCachePager(channelUrls: List<String>, pageSize: Int = 20): IPager<IPlatformContent> {
val pagers = MultiChronoContentPager(channelUrls.map { _subscriptionCache.queryPager(DBSubscriptionCache.Index::channelUrl, it, pageSize) {
it.obj;
} }, false, 20);
} }, false, pageSize);
return DedupContentPager(pagers, StatePlatform.instance.getEnabledClients().map { it.id });
}
fun getSubscriptionCachePager(): DedupContentPager {
@@ -67,7 +56,7 @@ class StateCache {
val subs = StateSubscriptions.instance.getSubscriptions();
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
val allUrls = subs.map {
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
if(!otherUrls.contains(it.channel.url))
return@map listOf(listOf(it.channel.url), otherUrls).flatten();
else
@@ -79,13 +68,6 @@ class StateCache {
val timeCacheRetrieving = measureTimeMillis {
pagers = listOf(getAllChannelCachePager(allUrls));
/*allUrls.parallelStream()
.map {
getChannelCachePager(it)
}
.asSequence()
.toList();*/
}
Logger.i(TAG, "Subscriptions CachePager compiling (retrieved in ${timeCacheRetrieving}ms)");
@@ -100,7 +100,7 @@ class StateHistory {
return _historyDBStore.getObjectPager();
}
fun getHistorySearchPager(query: String): IPager<HistoryVideo> {
return _historyDBStore.queryLikeObjectPager(DBHistory.Index::url, "%${query}%", 10);
return _historyDBStore.queryLikeObjectPager(DBHistory.Index::name, "%${query}%", 10);
}
fun getHistoryIndexByUrl(url: String): DBHistory.Index? {
return historyIndex[url];
@@ -40,6 +40,7 @@ import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.stores.*
import kotlinx.coroutines.*
import okhttp3.internal.concat
import java.lang.Thread.sleep
import java.time.OffsetDateTime
import kotlin.reflect.jvm.internal.impl.builtins.jvm.JavaToKotlinClassMap.PlatformMutabilityMapping
import kotlin.streams.asSequence
@@ -405,7 +406,12 @@ class StatePlatform {
val deferred: List<Pair<IPlatformClient, Deferred<IPager<IPlatformContent>?>>> = clients.map {
return@map Pair(it, scope.async(Dispatchers.IO) {
try {
val searchResult = it.fromPool(_pagerClientPool).getHome();
var searchResult = it.fromPool(_pagerClientPool).getHome();
if(searchResult.getResults().size == 0) {
Logger.i(TAG, "No home results, retrying");
sleep(500);
searchResult = it.fromPool(_pagerClientPool).getHome();
}
return@async searchResult;
} catch(ex: Throwable) {
Logger.e(TAG, "getHomeRefresh", ex);
@@ -37,6 +37,7 @@ class StatePlayer {
//Video Status
var rotationLock : Boolean = false;
var loopVideo : Boolean = false;
val isPlaying: Boolean get() = _exoplayer?.player?.playWhenReady ?: false;
@@ -286,6 +287,31 @@ class StatePlayer {
}
onQueueChanged.emit(true);
}
fun insertToQueue(video: IPlatformVideo, playNow: Boolean = false) {
synchronized(_queue) {
if(_queue.isEmpty()) {
setQueueType(TYPE_QUEUE);
currentVideo?.let {
_queue.add(it);
}
}
if(_queue.isEmpty())
_queue.add(video);
else
_queue.add(_queuePosition.coerceAtLeast(0).coerceAtMost(_queue.size - 1), video);
if (queueShuffle) {
addToShuffledQueue(video);
}
if (_queuePosition < 0) {
_queuePosition = 0;
}
}
onQueueChanged.emit(true);
if(playNow)
setQueuePosition(video);
}
fun setQueuePosition(video: IPlatformVideo) {
synchronized(_queue) {
if (getCurrentQueueItem() == video) {
@@ -348,6 +374,8 @@ class StatePlayer {
}
fun getNextQueueItem() : IPlatformVideo? {
if(loopVideo)
return currentVideo;
synchronized(_queue) {
val shuffledQueue = _queueShuffled;
val queue = if (queueShuffle && shuffledQueue != null) {
@@ -375,6 +403,8 @@ class StatePlayer {
}
};
fun nextQueueItem(withoutRemoval: Boolean = false) : IPlatformVideo? {
if(loopVideo)
return currentVideo;
synchronized(_queue) {
if (_queue.isEmpty())
return null;
@@ -1,27 +0,0 @@
package com.futo.platformplayer.stores.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
/*
@Dao
class ManagedDBContext<T, I: ManagedDBIndex<T>> {
fun get(id: Int): I;
fun gets(vararg id: Int): List<I>;
fun getAll(): List<I>;
@Insert
fun insert(index: I);
@Insert
fun insertAll(vararg indexes: I)
@Update
fun update(index: I);
@Delete
fun delete(index: I);
}*/
@@ -1,42 +1,23 @@
package com.futo.platformplayer.subscription
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.structures.DedupContentPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.PlatformContentPager
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.toSafeFileName
import kotlinx.coroutines.CoroutineScope
import java.util.concurrent.ForkJoinPool
class CachedSubscriptionAlgorithm(pageSize: Int = 150, scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = true, threadPool: ForkJoinPool? = null)
class CachedSubscriptionAlgorithm(scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = true, threadPool: ForkJoinPool? = null, pageSize: Int = 50)
: SubscriptionFetchAlgorithm(scope, allowFailure, withCacheFallback, threadPool) {
private val _pageSize: Int = pageSize;
override fun countRequests(subs: Map<Subscription, List<String>>): Map<JSClient, Int> {
return mapOf<JSClient, Int>();
return mapOf();
}
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
val validSubIds = subs.flatMap { it.value } .map { it.toSafeFileName() }.toHashSet();
/*
val validStores = StateCache.instance._channelContents
.filter { validSubIds.contains(it.key) }
.map { it.value };*/
/*
val items = validStores.flatMap { it.getItems() }
.sortedByDescending { it.datetime };
*/
return Result(DedupContentPager(StateCache.instance.getChannelCachePager(subs.flatMap { it.value }.distinct()), StatePlatform.instance.getEnabledClients().map { it.id }), listOf());
return Result(DedupContentPager(StateCache.instance.getChannelCachePager(subs.flatMap { it.value }.distinct(), _pageSize), StatePlatform.instance.getEnabledClients().map { it.id }), listOf());
}
}
@@ -38,7 +38,7 @@ abstract class SubscriptionFetchAlgorithm(
fun getAlgorithm(algo: SubscriptionFetchAlgorithms, scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = false, pool: ForkJoinPool? = null): SubscriptionFetchAlgorithm {
return when(algo) {
SubscriptionFetchAlgorithms.CACHE -> CachedSubscriptionAlgorithm(150, scope, allowFailure, withCacheFallback, pool);
SubscriptionFetchAlgorithms.CACHE -> CachedSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool, 50);
SubscriptionFetchAlgorithms.SIMPLE -> SimpleSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
SubscriptionFetchAlgorithms.SMART -> SmartSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
else -> throw IllegalStateException("Unknown algorithm ${algo}");
@@ -1,120 +0,0 @@
package com.futo.platformplayer.views.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlaylists
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> {
private lateinit var _filteredVideos: MutableList<HistoryVideo>;
val onClick = Event1<HistoryVideo>();
private var _query: String = "";
constructor() : super() {
updateFilteredVideos();
StateHistory.instance.onHistoricVideoChanged.subscribe(this) { video, position ->
StateApp.instance.scope.launch(Dispatchers.Main) {
val index = _filteredVideos.indexOfFirst { v -> v.video.url == video.url };
if (index == -1) {
return@launch;
}
_filteredVideos[index].position = position;
if (index < _filteredVideos.size - 2) {
notifyItemRangeChanged(index, 2);
} else {
notifyItemChanged(index);
}
}
};
}
fun setQuery(query: String) {
_query = query;
updateFilteredVideos();
}
fun updateFilteredVideos() {
val videos = StateHistory.instance.getHistory();
val pager = StateHistory.instance.getHistoryPager();
//filtered val pager = StateHistory.instance.getHistorySearchPager("querrryyyyy");
if (_query.isBlank()) {
_filteredVideos = videos.toMutableList();
} else {
_filteredVideos = videos.filter { v -> v.video.name.lowercase().contains(_query); }.toMutableList();
}
notifyDataSetChanged();
}
fun cleanup() {
StateHistory.instance.onHistoricVideoChanged.remove(this);
}
override fun getItemCount() = _filteredVideos.size;
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): HistoryListViewHolder {
val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.list_history, viewGroup, false);
val holder = HistoryListViewHolder(view);
holder.onRemove.subscribe { v ->
val videos = _filteredVideos;
val index = videos.indexOf(v);
if (index == -1) {
return@subscribe;
}
StateHistory.instance.removeHistory(v.video.url);
_filteredVideos.removeAt(index);
notifyItemRemoved(index);
};
holder.onClick.subscribe { v ->
val videos = _filteredVideos;
val index = videos.indexOf(v);
if (index == -1) {
return@subscribe;
}
_filteredVideos.removeAt(index);
_filteredVideos.add(0, v);
notifyItemMoved(index, 0);
notifyItemRangeChanged(0, 2);
onClick.emit(v);
};
return holder;
}
override fun onBindViewHolder(viewHolder: HistoryListViewHolder, position: Int) {
val videos = _filteredVideos;
var watchTime: String? = null;
if (position == 0) {
watchTime = videos[position].date.toHumanNowDiffStringMinDay();
} else {
val previousWatchTime = videos[position - 1].date.toHumanNowDiffStringMinDay();
val currentWatchTime = videos[position].date.toHumanNowDiffStringMinDay();
if (previousWatchTime != currentWatchTime) {
watchTime = currentWatchTime;
}
}
viewHolder.bind(videos[position], watchTime);
}
companion object {
val TAG = "HistoryListAdapter";
}
}
@@ -1,6 +1,8 @@
package com.futo.platformplayer.views.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
@@ -35,26 +37,26 @@ class HistoryListViewHolder : ViewHolder {
val onClick = Event1<HistoryVideo>();
val onRemove = Event1<HistoryVideo>();
constructor(view: View) : super(view) {
_root = view.findViewById(R.id.root);
_imageThumbnail = view.findViewById(R.id.image_video_thumbnail);
_imageThumbnail?.clipToOutline = true;
_textName = view.findViewById(R.id.text_video_name);
_textAuthor = view.findViewById(R.id.text_author);
_textMetadata = view.findViewById(R.id.text_video_metadata);
_textVideoDuration = view.findViewById(R.id.thumbnail_duration);
_containerDuration = view.findViewById(R.id.thumbnail_duration_container);
_containerLive = view.findViewById(R.id.thumbnail_live_container);
_imageRemove = view.findViewById(R.id.image_trash);
_textHeader = view.findViewById(R.id.text_header);
_timeBar = view.findViewById(R.id.time_bar);
constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_history, viewGroup, false)) {
_root = itemView.findViewById(R.id.root);
_imageThumbnail = itemView.findViewById(R.id.image_video_thumbnail);
_imageThumbnail.clipToOutline = true;
_textName = itemView.findViewById(R.id.text_video_name);
_textAuthor = itemView.findViewById(R.id.text_author);
_textMetadata = itemView.findViewById(R.id.text_video_metadata);
_textVideoDuration = itemView.findViewById(R.id.thumbnail_duration);
_containerDuration = itemView.findViewById(R.id.thumbnail_duration_container);
_containerLive = itemView.findViewById(R.id.thumbnail_live_container);
_imageRemove = itemView.findViewById(R.id.image_trash);
_textHeader = itemView.findViewById(R.id.text_header);
_timeBar = itemView.findViewById(R.id.time_bar);
_root.setOnClickListener {
val v = video ?: return@setOnClickListener;
onClick.emit(v);
};
_imageRemove?.setOnClickListener {
_imageRemove.setOnClickListener {
val v = video ?: return@setOnClickListener;
onRemove.emit(v);
};
@@ -82,7 +82,6 @@ class SubscriptionViewHolder : ViewHolder {
this.subscription = sub;
_creatorThumbnail.setThumbnail(sub.channel.thumbnail, false);
_taskLoadProfile.run(sub.channel.id);
_textName.text = sub.channel.name;
bindViewMetrics(sub);
_platformIndicator.setPlatformFromClientID(sub.channel.id.pluginId);
@@ -93,6 +92,8 @@ class SubscriptionViewHolder : ViewHolder {
if (cachedProfile.expired) {
_taskLoadProfile.run(sub.channel.id);
}
} else {
_taskLoadProfile.run(sub.channel.id);
}
}
@@ -184,7 +184,7 @@ open class PreviewVideoView : LinearLayout {
.placeholder(R.drawable.placeholder_channel_thumbnail)
.into(_imageChannel);
}
_taskLoadProfile.run(content.author.id);
_textChannelName.text = content.author.name
val cachedProfile = PolycentricCache.instance.getCachedProfile(content.author.url, true);
@@ -193,6 +193,8 @@ open class PreviewVideoView : LinearLayout {
if (cachedProfile.expired) {
_taskLoadProfile.run(content.author.id);
}
} else {
_taskLoadProfile.run(content.author.id);
}
_imageChannel?.clipToOutline = true;
@@ -66,7 +66,6 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
_taskLoadProfile.cancel();
_creatorThumbnail.setThumbnail(authorLink.thumbnail, false);
_taskLoadProfile.run(authorLink.id);
_textName.text = authorLink.name;
val cachedProfile = PolycentricCache.instance.getCachedProfile(authorLink.url, true);
@@ -75,6 +74,8 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
if (cachedProfile.expired) {
_taskLoadProfile.run(authorLink.id);
}
} else {
_taskLoadProfile.run(authorLink.id);
}
if(authorLink.subscribers == null || (authorLink.subscribers ?: 0) <= 0L)
@@ -52,7 +52,6 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
_channel = subscription.channel;
_creatorThumbnail.setThumbnail(subscription.channel.thumbnail, false);
_taskLoadProfile.run(subscription.channel.id);
_name.text = subscription.channel.name;
val cachedProfile = PolycentricCache.instance.getCachedProfile(subscription.channel.url, true);
@@ -61,6 +60,8 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
if (cachedProfile.expired) {
_taskLoadProfile.run(subscription.channel.id);
}
} else {
_taskLoadProfile.run(subscription.channel.id);
}
_subscription = subscription;
@@ -26,6 +26,7 @@ class CastView : ConstraintLayout {
private val _thumbnail: ImageView;
private val _buttonMinimize: ImageButton;
private val _buttonSettings: ImageButton;
private val _buttonLoop: ImageButton;
private val _buttonPlay: ImageButton;
private val _buttonPause: ImageButton;
private val _buttonCast: CastButton;
@@ -49,6 +50,7 @@ class CastView : ConstraintLayout {
_thumbnail = findViewById(R.id.image_thumbnail);
_buttonMinimize = findViewById(R.id.button_minimize);
_buttonSettings = findViewById(R.id.button_settings);
_buttonLoop = findViewById(R.id.button_loop);
_buttonPlay = findViewById(R.id.button_play);
_buttonPause = findViewById(R.id.button_pause);
_buttonCast = findViewById(R.id.button_cast);
@@ -65,6 +67,12 @@ class CastView : ConstraintLayout {
StateCasting.instance.videoSeekTo(d.expectedCurrentTime + it / 1000);
};
_buttonLoop.setOnClickListener {
StatePlayer.instance.loopVideo = !StatePlayer.instance.loopVideo;
_buttonLoop.setImageResource(if(StatePlayer.instance.loopVideo) R.drawable.ic_loop_active else R.drawable.ic_loop);
}
_buttonLoop.setImageResource(if(StatePlayer.instance.loopVideo) R.drawable.ic_loop_active else R.drawable.ic_loop);
_timeBar.addListener(object : OnScrubListener {
override fun onScrubStart(timeBar: TimeBar, position: Long) {
StateCasting.instance.videoSeekTo(position.toDouble());
@@ -4,7 +4,6 @@ import android.content.Context
import android.content.res.Resources
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.os.Handler
import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue
@@ -17,7 +16,6 @@ import android.widget.RelativeLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.setMargins
import androidx.lifecycle.LifecycleOwner
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
@@ -68,20 +66,26 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
private val _control_videosettings: ImageButton;
private val _control_minimize: ImageButton;
private val _control_rotate_lock: ImageButton;
private val _control_loop: ImageButton;
private val _control_cast: ImageButton;
private val _control_play: ImageButton;
private val _control_chapter: TextView;
private val _time_bar: TimeBar;
private val _buttonPrevious: ImageButton;
private val _buttonNext: ImageButton;
private val _control_fullscreen_fullscreen: ImageButton;
private val _control_videosettings_fullscreen: ImageButton;
private val _control_minimize_fullscreen: ImageButton;
private val _control_rotate_lock_fullscreen: ImageButton;
private val _control_loop_fullscreen: ImageButton;
private val _control_cast_fullscreen: ImageButton;
private val _control_play_fullscreen: ImageButton;
private val _time_bar_fullscreen: TimeBar;
private val _overlay_brightness: FrameLayout;
private val _control_chapter_fullscreen: TextView;
private val _buttonPrevious_fullscreen: ImageButton;
private val _buttonNext_fullscreen: ImageButton;
private val _title_fullscreen: TextView;
private val _author_fullscreen: TextView;
@@ -110,6 +114,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
val onToggleFullScreen = Event1<Boolean>();
val onSourceChanged = Event3<IVideoSource?, IAudioSource?, Boolean>();
val onSourceEnded = Event0();
val onPrevious = Event0();
val onNext = Event0();
val onChapterChanged = Event2<IChapter?, Boolean>();
@@ -128,25 +134,36 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_control_videosettings = videoControls.findViewById(R.id.exo_settings);
_control_minimize = videoControls.findViewById(R.id.exo_minimize);
_control_rotate_lock = videoControls.findViewById(R.id.exo_rotate_lock);
_control_loop = videoControls.findViewById(R.id.exo_loop);
_control_cast = videoControls.findViewById(R.id.exo_cast);
_control_play = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_play);
_time_bar = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress);
_control_chapter = videoControls.findViewById(R.id.text_chapter_current);
_buttonNext = videoControls.findViewById(R.id.button_next);
_buttonPrevious = videoControls.findViewById(R.id.button_previous);
_videoControls_fullscreen = findViewById(R.id.video_player_controller_fullscreen);
_control_fullscreen_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_fullscreen);
_control_minimize_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_minimize);
_control_videosettings_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_settings);
_control_rotate_lock_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_rotate_lock);
_control_loop_fullscreen = videoControls.findViewById(R.id.exo_loop);
_control_cast_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_cast);
_control_play_fullscreen = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_play);
_control_chapter_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_chapter_current);
_time_bar_fullscreen = _videoControls_fullscreen.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress);
_buttonPrevious_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_previous);
_buttonNext_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_next);
val castVisibility = if (Settings.instance.casting.enabled) View.VISIBLE else View.GONE
_control_cast.visibility = castVisibility
_control_cast_fullscreen.visibility = castVisibility
_buttonPrevious.setOnClickListener { onPrevious.emit() };
_buttonNext.setOnClickListener { onNext.emit() };
_buttonPrevious_fullscreen.setOnClickListener { onPrevious.emit() };
_buttonNext_fullscreen.setOnClickListener { onNext.emit() };
_overlay_brightness = findViewById(R.id.overlay_brightness);
_title_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_title);
@@ -244,6 +261,16 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
UIDialogs.showCastingDialog(context);
};
_control_loop.setOnClickListener {
StatePlayer.instance.loopVideo = !StatePlayer.instance.loopVideo;
updateLoopVideoUI();
}
_control_loop_fullscreen.setOnClickListener {
StatePlayer.instance.loopVideo = !StatePlayer.instance.loopVideo;
updateLoopVideoUI();
}
_control_minimize_fullscreen.setOnClickListener {
onMinimize.emit(this);
};
@@ -273,11 +300,18 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
}
updateLoopVideoUI();
if(!isInEditMode) {
gestureControl.hideControls();
}
}
/*fun updateNextPrevious(hasNext: Boolean, hasPrevious: Boolean) {
_buttonNext.visibility = if (hasNext) View.VISIBLE else View.GONE
_buttonPrevious.visibility = if (hasPrevious) View.VISIBLE else View.GONE
}*/
private val _currentChapterUpdateInterval: Long = 1000L / Settings.instance.playback.getChapterUpdateFrames();
private var _currentChapterUpdateLastPos = 0L;
private val _currentChapterUpdateExecuter = Executors.newSingleThreadScheduledExecutor();
@@ -555,6 +589,17 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_control_rotate_lock.setImageResource(R.drawable.ic_screen_lock_rotation);
}
}
fun updateLoopVideoUI() {
if(StatePlayer.instance.loopVideo) {
_control_loop.setImageResource(R.drawable.ic_loop_active);
_control_loop_fullscreen.setImageResource(R.drawable.ic_loop_active);
}
else {
_control_loop.setImageResource(R.drawable.ic_loop);
_control_loop_fullscreen.setImageResource(R.drawable.ic_loop);
}
}
fun setGestureSoundFactor(soundFactor: Float) {
gestureControl.setSoundFactor(soundFactor);
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M292.31,840L160,707.69L292.31,575.39L320.61,604.15L237.08,687.69L692.31,687.69L692.31,527.69L732.31,527.69L732.31,727.69L237.08,727.69L320.61,811.23L292.31,840ZM227.69,432.31L227.69,232.31L722.92,232.31L639.39,148.77L667.69,120L800,252.31L667.69,384.61L639.39,355.85L722.92,272.31L267.69,272.31L267.69,432.31L227.69,432.31Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@color/colorPrimary"
android:pathData="M292.31,840L160,707.69L292.31,575.39L320.61,604.15L237.08,687.69L692.31,687.69L692.31,527.69L732.31,527.69L732.31,727.69L237.08,727.69L320.61,811.23L292.31,840ZM227.69,432.31L227.69,232.31L722.92,232.31L639.39,148.77L667.69,120L800,252.31L667.69,384.61L639.39,355.85L722.92,272.31L267.69,272.31L267.69,432.31L227.69,432.31Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M660,720L660,240L740,240L740,720L660,720ZM220,720L220,240L580,480L220,720Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M220,720L220,240L300,240L300,720L220,720ZM740,720L380,480L740,240L740,720Z"/>
</vector>
@@ -3,11 +3,17 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.futo.platformplayer.views.SupportView
android:id="@+id/support"
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
android:layout_height="match_parent">
<com.futo.platformplayer.views.SupportView
android:id="@+id/support"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"/>
</androidx.core.widget.NestedScrollView>
<TextView
android:id="@+id/text_monetization"
+10 -3
View File
@@ -15,11 +15,18 @@
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<com.futo.platformplayer.views.SupportView
android:id="@+id/support"
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/topbar"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintBottom_toBottomOf="parent">
<com.futo.platformplayer.views.SupportView
android:id="@+id/support"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
+54 -14
View File
@@ -45,6 +45,14 @@
android:clickable="true"
android:padding="12dp"
app:srcCompat="@drawable/ic_screen_lock_rotation" />
<ImageButton
android:id="@+id/exo_loop"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
app:srcCompat="@drawable/ic_loop" />
<ImageButton
android:id="@+id/exo_settings"
android:layout_width="50dp"
@@ -56,28 +64,60 @@
</LinearLayout>
<ImageButton
android:id="@id/exo_play"
android:id="@id/button_previous"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:scaleType="centerCrop"
android:clickable="true"
app:srcCompat="@drawable/ic_play_white_nopad" />
android:layout_marginRight="40dp"
android:padding="5dp"
app:srcCompat="@drawable/ic_skip_previous"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@id/layout_play_pause"
app:layout_constraintBottom_toBottomOf="parent" />
<FrameLayout
android:id="@+id/layout_play_pause"
android:layout_width="60dp"
android:layout_height="60dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent">
<ImageButton
android:id="@id/exo_play"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
android:clickable="true"
app:srcCompat="@drawable/ic_play_white_nopad" />
<ImageButton
android:id="@id/exo_pause"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
android:clickable="true"
app:srcCompat="@drawable/ic_pause_white"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</FrameLayout>
<ImageButton
android:id="@id/exo_pause"
android:id="@id/button_next"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:clickable="true"
app:srcCompat="@drawable/ic_pause_white" />
android:scaleType="centerCrop"
android:padding="5dp"
android:layout_marginLeft="40dp"
app:srcCompat="@drawable/ic_skip_next"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@id/layout_play_pause"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageButton
android:id="@+id/exo_fullscreen"
@@ -73,6 +73,14 @@
android:clickable="true"
android:padding="12dp"
app:srcCompat="@drawable/ic_screen_lock_rotation" />
<ImageButton
android:id="@+id/exo_loop"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
app:srcCompat="@drawable/ic_loop" />
<ImageButton
android:id="@+id/exo_settings"
android:layout_width="50dp"
@@ -84,28 +92,60 @@
</LinearLayout>
<ImageButton
android:id="@id/exo_play"
android:id="@id/button_previous"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
android:scaleType="centerCrop"
android:clickable="true"
app:srcCompat="@drawable/ic_play_white_nopad"
android:layout_marginRight="40dp"
android:padding="5dp"
app:srcCompat="@drawable/ic_skip_previous"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@id/layout_play_pause"
app:layout_constraintBottom_toBottomOf="parent" />
<FrameLayout
android:id="@+id/layout_play_pause"
android:layout_width="60dp"
android:layout_height="60dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
app:layout_constraintRight_toRightOf="parent">
<ImageButton
android:id="@id/exo_play"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
android:clickable="true"
app:srcCompat="@drawable/ic_play_white_nopad" />
<ImageButton
android:id="@id/exo_pause"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
android:clickable="true"
app:srcCompat="@drawable/ic_pause_white"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</FrameLayout>
<ImageButton
android:id="@id/exo_pause"
android:id="@id/button_next"
android:layout_width="60dp"
android:layout_height="60dp"
android:padding="10dp"
android:clickable="true"
app:srcCompat="@drawable/ic_pause_white"
android:scaleType="centerCrop"
android:padding="5dp"
android:layout_marginLeft="40dp"
app:srcCompat="@drawable/ic_skip_next"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
app:layout_constraintLeft_toRightOf="@id/layout_play_pause"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageButton
android:id="@+id/exo_fullscreen"
+9
View File
@@ -54,6 +54,15 @@
android:clickable="true"
android:padding="12dp"
app:srcCompat="@drawable/ic_cast" />
<ImageButton
android:id="@+id/button_loop"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
app:srcCompat="@drawable/ic_loop" />
<ImageButton
android:id="@+id/button_settings"
android:layout_width="50dp"
+3 -6
View File
@@ -1,11 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_margin="18dp"
android:showDividers="middle"
@@ -187,5 +185,4 @@
android:divider="@drawable/divider_transparent_vertical_8dp">
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>