mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-19 14:32:34 +02:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ae05e7cd4 | |||
| b284176072 | |||
| 5fffaf2f4e | |||
| 58da91eae8 | |||
| 98d92d3fe2 | |||
| c5d35b27f0 | |||
| aee5b75c2f | |||
| fe02197bd8 |
@@ -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)
|
||||
|
||||
-4
@@ -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);
|
||||
|
||||
+15
-4
@@ -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)
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+257
-59
@@ -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"
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -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);
|
||||
|
||||
+66
-14
@@ -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);
|
||||
}*/
|
||||
+3
-22
@@ -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());
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -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;
|
||||
|
||||
+2
-1
@@ -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)
|
||||
|
||||
+2
-1
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user