Merge branch 'shorts-tab' into 'master'

shorts tab

See merge request videostreaming/grayjay!92
This commit is contained in:
Koen 2025-07-21 12:33:14 +00:00
commit 4e49b5bc63
51 changed files with 3281 additions and 43 deletions

View file

@ -156,6 +156,7 @@ android {
dependencies {
implementation 'com.google.dagger:dagger:2.48'
implementation 'androidx.test:monitor:1.7.2'
implementation 'com.google.android.material:material:1.12.0'
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
//Core

View file

@ -785,6 +785,7 @@ let plugin = {
//To override by plugin
const source = {
getHome() { return new ContentPager([], false, {}); },
getShorts() { return new VideoPager([], false, {}); },
enable(config){ },
disable() {},

View file

@ -62,6 +62,7 @@ import com.futo.platformplayer.fragment.mainactivity.main.PlaylistSearchResultsF
import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
import com.futo.platformplayer.fragment.mainactivity.main.PostDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.RemotePlaylistFragment
import com.futo.platformplayer.fragment.mainactivity.main.ShortsFragment
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
import com.futo.platformplayer.fragment.mainactivity.main.SourcesFragment
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
@ -169,6 +170,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment;
lateinit var _fragWatchlist: WatchLaterFragment;
lateinit var _fragHistory: HistoryFragment;
lateinit var _fragShorts: ShortsFragment;
lateinit var _fragSourceDetail: SourceDetailFragment;
lateinit var _fragDownloads: DownloadsFragment;
lateinit var _fragImportSubscriptions: ImportSubscriptionsFragment;
@ -338,6 +340,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragWebDetail = WebDetailFragment.newInstance();
_fragWatchlist = WatchLaterFragment.newInstance();
_fragHistory = HistoryFragment.newInstance();
_fragShorts = ShortsFragment.newInstance();
_fragSourceDetail = SourceDetailFragment.newInstance();
_fragDownloads = DownloadsFragment();
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
@ -1253,6 +1256,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
WebDetailFragment::class -> _fragWebDetail as T;
WatchLaterFragment::class -> _fragWatchlist as T;
HistoryFragment::class -> _fragHistory as T;
ShortsFragment::class -> _fragShorts as T;
SourceDetailFragment::class -> _fragSourceDetail as T;
DownloadsFragment::class -> _fragDownloads as T;
ImportSubscriptionsFragment::class -> _fragImportSubscriptions as T;

View file

@ -13,6 +13,7 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.models.ImageVariable
@ -36,6 +37,11 @@ interface IPlatformClient {
*/
fun getHome(): IPager<IPlatformContent>
/**
* Gets the shorts feed
*/
fun getShorts(): IPager<IPlatformVideo>
//Search
/**
* Gets search suggestion for the provided query string

View file

@ -23,6 +23,7 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
@ -43,6 +44,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistPager
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoPager
import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1
@ -124,6 +126,7 @@ open class JSClient : IPlatformClient {
val enableInSearch get() = descriptor.appSettings.tabEnabled.enableSearch ?: true
val enableInHome get() = descriptor.appSettings.tabEnabled.enableHome ?: true
val enableInShorts get() = descriptor.appSettings.tabEnabled.enableShorts ?: true
fun getSubscriptionRateLimit(): Int? {
val pluginRateLimit = config.subscriptionRateLimit;
@ -328,6 +331,13 @@ open class JSClient : IPlatformClient {
plugin.executeTyped("source.getHome()"));
}
@JSDocs(2, "source.getShorts()", "Gets the Shorts feed of the platform")
override fun getShorts(): IPager<IPlatformVideo> = isBusyWith("getShorts") {
ensureEnabled()
return@isBusyWith JSVideoPager(config, this,
plugin.executeTyped("source.getShorts()"))
}
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
@JSDocsParameter("query", "Query to complete suggestions for")
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {

View file

@ -48,6 +48,7 @@ class SourcePluginConfig(
var subscriptionRateLimit: Int? = null,
var enableInSearch: Boolean = true,
var enableInHome: Boolean = true,
var enableInShorts: Boolean = true,
var supportedClaimTypes: List<Int> = listOf(),
var primaryClaimFieldType: Int? = null,
var developerSubmitUrl: String? = null,

View file

@ -103,9 +103,11 @@ class SourcePluginDescriptor {
@FormField(R.string.home, FieldForm.TOGGLE, R.string.show_content_in_home_tab, 1)
var enableHome: Boolean? = null;
@FormField(R.string.search, FieldForm.TOGGLE, R.string.show_content_in_search_results, 2)
var enableSearch: Boolean? = null;
@FormField(R.string.shorts, FieldForm.TOGGLE, R.string.show_content_in_shorts_tab, 3)
var enableShorts: Boolean? = null;
}
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 3)
@ -143,6 +145,8 @@ class SourcePluginDescriptor {
tabEnabled.enableHome = config.enableInHome
if(tabEnabled.enableSearch == null)
tabEnabled.enableSearch = config.enableInSearch
if(tabEnabled.enableShorts == null)
tabEnabled.enableShorts = config.enableInShorts
}
}

View file

@ -7,12 +7,12 @@ import java.util.stream.IntStream
* A Content MultiPager that returns results based on a specified distribution
* TODO: Merge all basic distribution pagers
*/
class MultiDistributionContentPager : MultiPager<IPlatformContent> {
class MultiDistributionContentPager<T : IPlatformContent> : MultiPager<T> {
private val dist : HashMap<IPager<IPlatformContent>, Float>;
private val distConsumed : HashMap<IPager<IPlatformContent>, Float>;
private val dist : HashMap<IPager<T>, Float>;
private val distConsumed : HashMap<IPager<T>, Float>;
constructor(pagers : Map<IPager<IPlatformContent>, Float>) : super(pagers.keys.toMutableList()) {
constructor(pagers : Map<IPager<T>, Float>) : super(pagers.keys.toMutableList()) {
val distTotal = pagers.values.sum();
dist = HashMap();
@ -25,7 +25,7 @@ class MultiDistributionContentPager : MultiPager<IPlatformContent> {
}
@Synchronized
override fun selectItemIndex(options: Array<SelectionOption<IPlatformContent>>): Int {
override fun selectItemIndex(options: Array<SelectionOption<T>>): Int {
if(options.size == 0)
return -1;
var bestIndex = 0;
@ -42,6 +42,4 @@ class MultiDistributionContentPager : MultiPager<IPlatformContent> {
distConsumed[options[bestIndex].pager.getPager()] = bestConsumed;
return bestIndex;
}
}

View file

@ -25,6 +25,7 @@ import com.futo.platformplayer.api.media.structures.IReplacerPager
import com.futo.platformplayer.api.media.structures.MultiPager
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.constructs.Event3
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp
import com.futo.platformplayer.engine.exceptions.PluginException
@ -61,7 +62,7 @@ class ChannelContentsFragment(private val subType: String? = null) : Fragment(),
private var _query: String? = null
private var _searchView: SearchView? = null
val onContentClicked = Event2<IPlatformContent, Long>();
val onContentClicked = Event3<IPlatformContent, Long, Pair<IPager<IPlatformContent>, ArrayList<IPlatformContent>>?>();
val onContentUrlClicked = Event2<String, ContentType>();
val onUrlClicked = Event1<String>();
val onChannelClicked = Event1<PlatformAuthorLink>();
@ -211,7 +212,10 @@ class ChannelContentsFragment(private val subType: String? = null) : Fragment(),
_adapterResults = PreviewContentListAdapter(view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar, viewsToPrepend = arrayListOf(searchView)).apply {
this.onContentUrlClicked.subscribe(this@ChannelContentsFragment.onContentUrlClicked::emit);
this.onUrlClicked.subscribe(this@ChannelContentsFragment.onUrlClicked::emit);
this.onContentClicked.subscribe(this@ChannelContentsFragment.onContentClicked::emit);
this.onContentClicked.subscribe { content, num ->
val results = ArrayList(_results)
this@ChannelContentsFragment.onContentClicked.emit(content, num, Pair(_pager!!, results))
}
this.onChannelClicked.subscribe(this@ChannelContentsFragment.onChannelClicked::emit);
this.onAddToClicked.subscribe(this@ChannelContentsFragment.onAddToClicked::emit);
this.onAddToQueueClicked.subscribe(this@ChannelContentsFragment.onAddToQueueClicked::emit);

View file

@ -15,6 +15,7 @@ import android.view.ViewGroup
import android.widget.*
import androidx.core.animation.doOnEnd
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
@ -375,6 +376,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
fun newInstance() = MenuBottomBarFragment().apply { }
@UnstableApi
//Add configurable buttons here
var buttonDefinitions = listOf(
ButtonDefinition(0, R.drawable.ic_home, R.drawable.ic_home_filled, R.string.home, canToggle = true, { it.currentMain is HomeFragment }, {
@ -390,13 +392,14 @@ class MenuBottomBarFragment : MainActivityFragment() {
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>(withHistory = false) }),
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>(withHistory = false) }),
ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate<PlaylistsFragment>(withHistory = false) }),
ButtonDefinition(11, R.drawable.ic_smart_display, R.drawable.ic_smart_display_filled, R.string.shorts, canToggle = true, { it.currentMain is ShortsFragment && !(it.currentMain as ShortsFragment).isChannelShortsMode }, { it.navigate<ShortsFragment>(withHistory = false) }),
ButtonDefinition(5, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate<HistoryFragment>(withHistory = false) }),
ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>(withHistory = false) }),
ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>(withHistory = false) }),
ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate<SubscriptionGroupListFragment>(withHistory = false) }),
ButtonDefinition(10, R.drawable.ic_help_square, R.drawable.ic_help_square_fill, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate<TutorialFragment>(withHistory = false) }),
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, {
val c = it.context ?: return@ButtonDefinition;
val c = it.context ?: return@ButtonDefinition;
Logger.i(TAG, "settings preventPictureInPicture()");
it.requireFragment<VideoDetailFragment>().preventPictureInPicture();
val intent = Intent(c, SettingsActivity::class.java);

View file

@ -211,6 +211,14 @@ class ChannelFragment : MainFragment() {
}
}
}
adapter.onShortClicked.subscribe { v, _, pagerPair ->
when (v) {
is IPlatformVideo -> {
StatePlayer.instance.clearQueue()
fragment.navigate<ShortsFragment>(Triple(v, pagerPair!!.first, pagerPair.second))
}
}
}
adapter.onAddToClicked.subscribe { content ->
_overlayContainer.let {
if (content is IPlatformVideo) _slideUpOverlay =

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,363 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.annotation.SuppressLint
import android.graphics.drawable.Animatable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.SoundEffectConstants
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.views.buttons.BigButton
@UnstableApi
class ShortsFragment : MainFragment() {
override val isMainView: Boolean = true
override val isTab: Boolean = true
override val hasBottomBar: Boolean get() = true
private var loadPagerTask: TaskHandler<ShortsFragment, IPager<IPlatformVideo>>? = null
private var nextPageTask: TaskHandler<ShortsFragment, List<IPlatformVideo>>? = null
private var mainShortsPager: IPager<IPlatformVideo>? = null
private val mainShorts: MutableList<IPlatformVideo> = mutableListOf()
// the pager to call next on
private var currentShortsPager: IPager<IPlatformVideo>? = null
// the shorts array bound to the ViewPager2 adapter
private val currentShorts: MutableList<IPlatformVideo> = mutableListOf()
private var channelShortsPager: IPager<IPlatformVideo>? = null
private val channelShorts: MutableList<IPlatformVideo> = mutableListOf()
val isChannelShortsMode: Boolean
get() = channelShortsPager != null
private var viewPager: ViewPager2? = null
private lateinit var zeroState: LinearLayout
private lateinit var sourcesButton: BigButton
private lateinit var overlayLoading: FrameLayout
private lateinit var overlayLoadingSpinner: ImageView
private lateinit var overlayQualityContainer: FrameLayout
private var customViewAdapter: CustomViewAdapter? = null
init {
loadPager()
}
// we just completely reset the data structure so we want to tell the adapter that
@SuppressLint("NotifyDataSetChanged")
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
(activity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
super.onShownWithView(parameter, isBack)
if (parameter is Triple<*, *, *>) {
setLoading(false)
channelShorts.clear()
@Suppress("UNCHECKED_CAST") // TODO replace with a strongly typed parameter
channelShorts.addAll(parameter.third as ArrayList<IPlatformVideo>)
@Suppress("UNCHECKED_CAST") // TODO replace with a strongly typed parameter
channelShortsPager = parameter.second as IPager<IPlatformVideo>
currentShorts.clear()
currentShorts.addAll(channelShorts)
currentShortsPager = channelShortsPager
viewPager?.adapter?.notifyDataSetChanged()
viewPager?.post {
viewPager?.currentItem = channelShorts.indexOfFirst {
return@indexOfFirst (parameter.first as IPlatformVideo).id == it.id
}
}
} else if (isChannelShortsMode) {
channelShortsPager = null
channelShorts.clear()
currentShorts.clear()
if (loadPagerTask == null) {
currentShorts.addAll(mainShorts)
currentShortsPager = mainShortsPager
} else {
setLoading(true)
}
viewPager?.adapter?.notifyDataSetChanged()
viewPager?.currentItem = 0
}
updateZeroState()
}
override fun onCreateMainView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_shorts, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager = view.findViewById(R.id.view_pager)
zeroState = view.findViewById(R.id.zero_state)
sourcesButton = view.findViewById(R.id.sources_button)
overlayLoading = view.findViewById(R.id.short_view_loading_overlay)
overlayLoadingSpinner = view.findViewById(R.id.short_view_loader)
overlayQualityContainer = view.findViewById(R.id.shorts_quality_overview)
sourcesButton.onClick.subscribe {
sourcesButton.playSoundEffect(SoundEffectConstants.CLICK)
navigate<SourcesFragment>()
}
setLoading(true)
Logger.i(TAG, "Creating adapter")
val customViewAdapter =
CustomViewAdapter(currentShorts, layoutInflater, this@ShortsFragment, overlayQualityContainer, { isChannelShortsMode }) {
if (!currentShortsPager!!.hasMorePages()) {
return@CustomViewAdapter
}
nextPage()
}
customViewAdapter.onResetTriggered.subscribe {
setLoading(true)
loadPager()
loadPagerTask!!.success {
setLoading(false)
}
}
val viewPager = viewPager!!
viewPager.adapter = customViewAdapter
this.customViewAdapter = customViewAdapter
if (loadPagerTask == null && currentShorts.isEmpty()) {
loadPager()
loadPagerTask!!.success {
setLoading(false)
updateZeroState()
}
} else {
setLoading(false)
updateZeroState()
}
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
fun play(adapter: CustomViewAdapter, position: Int) {
val recycler = (viewPager.getChildAt(0) as RecyclerView)
val viewHolder =
recycler.findViewHolderForAdapterPosition(position) as CustomViewHolder?
if (viewHolder == null) {
adapter.needToPlay = position
} else {
val focusedView = viewHolder.shortView
focusedView.play()
adapter.previousShownView = focusedView
}
}
override fun onPageSelected(position: Int) {
val adapter = (viewPager.adapter as CustomViewAdapter)
if (adapter.previousShownView == null) {
// play if this page selection didn't trigger by a swipe from another page
play(adapter, position)
} else {
adapter.previousShownView?.stop()
adapter.previousShownView = null
adapter.newPosition = position
}
}
// wait for the state to idle to prevent UI lag
override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
if (state == ViewPager2.SCROLL_STATE_IDLE) {
val adapter = (viewPager.adapter as CustomViewAdapter)
val position = adapter.newPosition ?: return
adapter.newPosition = null
play(adapter, position)
}
}
})
}
private fun updateZeroState() {
if (mainShorts.isEmpty() && !isChannelShortsMode && loadPagerTask == null) {
zeroState.visibility = View.VISIBLE
} else {
zeroState.visibility = View.GONE
}
}
private fun nextPage() {
nextPageTask?.cancel()
val nextPageTask =
TaskHandler<ShortsFragment, List<IPlatformVideo>>(StateApp.instance.scopeGetter, {
currentShortsPager!!.nextPage()
return@TaskHandler currentShortsPager!!.getResults()
}).success { newVideos ->
val prevCount = customViewAdapter!!.itemCount
currentShorts.addAll(newVideos)
if (isChannelShortsMode) {
channelShorts.addAll(newVideos)
} else {
mainShorts.addAll(newVideos)
}
customViewAdapter!!.notifyItemRangeInserted(prevCount, newVideos.size)
nextPageTask = null
}
nextPageTask.run(this)
this.nextPageTask = nextPageTask
}
// we just completely reset the data structure so we want to tell the adapter that
@SuppressLint("NotifyDataSetChanged")
private fun loadPager() {
loadPagerTask?.cancel()
val loadPagerTask =
TaskHandler<ShortsFragment, IPager<IPlatformVideo>>(StateApp.instance.scopeGetter, {
val pager = StatePlatform.instance.getShorts()
return@TaskHandler pager
}).success { pager ->
mainShorts.clear()
mainShorts.addAll(pager.getResults())
mainShortsPager = pager
if (!isChannelShortsMode) {
currentShorts.clear()
currentShorts.addAll(mainShorts)
currentShortsPager = pager
// if the view pager exists go back to the beginning
viewPager?.adapter?.notifyDataSetChanged()
viewPager?.currentItem = 0
}
loadPagerTask = null
}.exception<Throwable> { err ->
val message = "Unable to load shorts $err"
Logger.i(TAG, message)
if (context != null) {
UIDialogs.showDialog(
requireContext(), R.drawable.ic_sources, message, null, null, 0, UIDialogs.Action(
"Close", { }, UIDialogs.ActionStyle.PRIMARY
)
)
}
return@exception
}
this.loadPagerTask = loadPagerTask
loadPagerTask.run(this)
}
private fun setLoading(isLoading: Boolean) {
if (isLoading) {
(overlayLoadingSpinner.drawable as Animatable?)?.start()
overlayLoading.visibility = View.VISIBLE
} else {
overlayLoading.visibility = View.GONE
(overlayLoadingSpinner.drawable as Animatable?)?.stop()
}
}
override fun onPause() {
super.onPause()
customViewAdapter?.previousShownView?.pause()
}
override fun onDestroy() {
super.onDestroy()
loadPagerTask?.cancel()
loadPagerTask = null
nextPageTask?.cancel()
nextPageTask = null
customViewAdapter?.previousShownView?.stop()
}
companion object {
private const val TAG = "ShortsFragment"
fun newInstance() = ShortsFragment()
}
class CustomViewAdapter(
private val videos: MutableList<IPlatformVideo>,
private val inflater: LayoutInflater,
private val fragment: MainFragment,
private val overlayQualityContainer: FrameLayout,
private val isChannelShortsMode: () -> Boolean,
private val onNearEnd: () -> Unit,
) : RecyclerView.Adapter<CustomViewHolder>() {
val onResetTriggered = Event0()
var previousShownView: ShortView? = null
var newPosition: Int? = null
var needToPlay: Int? = null
@OptIn(UnstableApi::class)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
val shortView = ShortView(inflater, fragment, overlayQualityContainer)
shortView.onResetTriggered.subscribe {
onResetTriggered.emit()
}
return CustomViewHolder(shortView)
}
@OptIn(UnstableApi::class)
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
holder.shortView.changeVideo(videos[position], isChannelShortsMode())
if (position == itemCount - 1) {
onNearEnd()
}
}
override fun onViewRecycled(holder: CustomViewHolder) {
super.onViewRecycled(holder)
holder.shortView.cancel()
}
override fun onViewAttachedToWindow(holder: CustomViewHolder) {
super.onViewAttachedToWindow(holder)
if (holder.absoluteAdapterPosition == needToPlay) {
holder.shortView.play()
needToPlay = null
previousShownView = holder.shortView
}
}
override fun getItemCount(): Int = videos.size
}
@OptIn(UnstableApi::class)
class CustomViewHolder(val shortView: ShortView) : RecyclerView.ViewHolder(shortView)
}

View file

@ -463,6 +463,47 @@ class StatePlatform {
pager.initialize();
return pager;
}
fun getShorts(): IPager<IPlatformVideo> {
Logger.i(TAG, "Platform - getShorts");
var clientIdsOngoing = mutableListOf<String>();
val clients = getSortedEnabledClient().filter { if (it is JSClient) it.enableInShorts else true };
StateApp.instance.scopeOrNull?.let {
it.launch(Dispatchers.Default) {
try {
// plugins that take longer than 5 seconds to load are considered "slow"
delay(5000);
val slowClients = synchronized(clientIdsOngoing) {
return@synchronized clients.filter { clientIdsOngoing.contains(it.id) };
};
for(client in slowClients)
UIDialogs.toast("${client.name} is still loading..\nConsider disabling it for Home", false);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show toast for slow source.", e)
}
}
}
val pages = clients.parallelStream()
.map {
Logger.i(TAG, "getShorts - ${it.name}")
synchronized(clientIdsOngoing) {
clientIdsOngoing.add(it.id);
}
val shortsResult = it.fromPool(_pagerClientPool).getShorts();
synchronized(clientIdsOngoing) {
clientIdsOngoing.remove(it.id);
}
return@map shortsResult;
}
.asSequence()
.toList()
.associateWith { 1f };
val pager = MultiDistributionContentPager(pages);
pager.initialize();
return pager;
}
suspend fun getHomeRefresh(scope: CoroutineScope): IPager<IPlatformContent> {
Logger.i(TAG, "Platform - getHome (Refresh)");
val clients = getSortedEnabledClient().filter { if (it is JSClient) it.enableInHome else true };

View file

@ -38,6 +38,7 @@ class StatePlayer {
//Players
private var _exoplayer : PlayerManager? = null;
private var _thumbnailExoPlayer : PlayerManager? = null;
private var _shortExoPlayer: PlayerManager? = null
//Video Status
var rotationLock: Boolean = false
@ -633,6 +634,13 @@ class StatePlayer {
}
return _thumbnailExoPlayer!!;
}
fun getShortPlayerOrCreate(context: Context) : PlayerManager {
if(_shortExoPlayer == null) {
val player = createExoPlayer(context);
_shortExoPlayer = PlayerManager(player);
}
return _shortExoPlayer!!;
}
@OptIn(UnstableApi::class)
private fun createExoPlayer(context : Context): ExoPlayer {
@ -656,10 +664,13 @@ class StatePlayer {
fun dispose(){
val player = _exoplayer;
val thumbPlayer = _thumbnailExoPlayer;
val shortPlayer = _shortExoPlayer
_exoplayer = null;
_thumbnailExoPlayer = null;
_shortExoPlayer = null
player?.release();
thumbPlayer?.release();
shortPlayer?.release()
}

View file

@ -34,15 +34,18 @@ class PlayerManager {
@Synchronized
fun attach(view: PlayerView, stateName: String) {
if(view != _currentView) {
_currentView?.player = null;
switchState(stateName);
view.player = player;
_currentView = view;
if (view != _currentView) {
_currentView?.player = null
_currentView = null
switchState(stateName)
view.player = player
_currentView = view
}
}
fun detach() {
_currentView?.player = null;
_currentView?.player = null
_currentView = null
}
fun getState(name: String): PlayerState {

View file

@ -9,8 +9,10 @@ import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.constructs.Event3
import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment
import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment
import com.futo.platformplayer.fragment.channel.tab.ChannelListFragment
@ -38,6 +40,7 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
val onContentUrlClicked = Event2<String, ContentType>()
val onUrlClicked = Event1<String>()
val onContentClicked = Event2<IPlatformContent, Long>()
val onShortClicked = Event3<IPlatformContent, Long, Pair<IPager<IPlatformContent>, ArrayList<IPlatformContent>>?>()
val onChannelClicked = Event1<PlatformAuthorLink>()
val onAddToClicked = Event1<IPlatformContent>()
val onAddToQueueClicked = Event1<IPlatformContent>()
@ -81,7 +84,9 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
when (_tabs[position]) {
ChannelTab.VIDEOS -> {
fragment = ChannelContentsFragment.newInstance().apply {
onContentClicked.subscribe(this@ChannelViewPagerAdapter.onContentClicked::emit)
onContentClicked.subscribe { video, num, _ ->
this@ChannelViewPagerAdapter.onContentClicked.emit(video, num)
}
onContentUrlClicked.subscribe(this@ChannelViewPagerAdapter.onContentUrlClicked::emit)
onUrlClicked.subscribe(this@ChannelViewPagerAdapter.onUrlClicked::emit)
onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit)
@ -94,7 +99,7 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
ChannelTab.SHORTS -> {
fragment = ChannelContentsFragment.newInstance(ResultCapabilities.TYPE_SHORTS).apply {
onContentClicked.subscribe(this@ChannelViewPagerAdapter.onContentClicked::emit)
onContentClicked.subscribe(this@ChannelViewPagerAdapter.onShortClicked::emit)
onContentUrlClicked.subscribe(this@ChannelViewPagerAdapter.onContentUrlClicked::emit)
onUrlClicked.subscribe(this@ChannelViewPagerAdapter.onUrlClicked::emit)
onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit)

View file

@ -19,7 +19,9 @@ class WebviewOverlay : LinearLayout {
inflate(context, R.layout.overlay_webview, this)
_topbar = findViewById(R.id.topbar);
_webview = findViewById(R.id.webview);
_webview.settings.javaScriptEnabled = true;
if (!isInEditMode){
_webview.settings.javaScriptEnabled = true;
}
_topbar.onClose.subscribe(this, onClose::emit);
}

View file

@ -68,15 +68,7 @@ class CommentsList : ConstraintLayout {
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadNextPage() });
};
private val _scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy);
onScrolled();
val totalScrollDistance = recyclerView.computeVerticalScrollOffset()
_layoutScrollToTop.visibility = if (totalScrollDistance > recyclerView.height) View.VISIBLE else View.GONE
}
};
private val _scrollListener: RecyclerView.OnScrollListener
private var _loader: (suspend () -> IPager<IPlatformComment>)? = null;
private val _adapterComments: InsertedViewAdapterWithLoader<CommentViewHolder>;
@ -131,6 +123,14 @@ class CommentsList : ConstraintLayout {
_llmReplies = LinearLayoutManager(context);
_recyclerComments.layoutManager = _llmReplies;
_recyclerComments.adapter = _adapterComments;
_scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy);
onScrolled();
_layoutScrollToTop.visibility = if (_llmReplies.findFirstCompletelyVisibleItemPosition() > 5) View.VISIBLE else View.GONE
}
};
_recyclerComments.addOnScrollListener(_scrollListener);
}

View file

@ -0,0 +1,153 @@
package com.futo.platformplayer.views.video
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.animation.LinearInterpolator
import androidx.annotation.OptIn
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerView
import androidx.media3.ui.TimeBar
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.video.PlayerManager
@UnstableApi
class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) :
FutoVideoPlayerBase(PLAYER_STATE_NAME, context, attrs) {
companion object {
private const val TAG = "FutoShortVideoPlayer"
private const val PLAYER_STATE_NAME: String = "ShortPlayer"
}
private var playerAttached = false
private val videoView: PlayerView
private val progressBar: DefaultTimeBar
private lateinit var player: PlayerManager
private var progressAnimator: ValueAnimator = createProgressBarAnimator()
val onPlaybackStateChanged = Event1<Int>();
private var playerEventListener = object : Player.Listener {
override fun onEvents(player: Player, events: Player.Events) {
if (events.containsAny(
Player.EVENT_POSITION_DISCONTINUITY, Player.EVENT_IS_PLAYING_CHANGED, Player.EVENT_PLAYBACK_STATE_CHANGED
)
) {
progressAnimator.cancel()
if (player.duration >= 0) {
progressAnimator.duration = player.duration
setProgressBarDuration(player.duration)
progressAnimator.currentPlayTime = player.currentPosition
}
if (player.isPlaying) {
progressAnimator.start()
}
}
if (events.containsAny(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
onPlaybackStateChanged.emit(player.playbackState)
}
}
}
init {
LayoutInflater.from(context).inflate(R.layout.view_short_player, this, true)
videoView = findViewById(R.id.short_player_view)
progressBar = findViewById(R.id.short_player_progress_bar)
if (!isInEditMode) {
player = StatePlayer.instance.getShortPlayerOrCreate(context)
player.player.repeatMode = Player.REPEAT_MODE_ONE
}
progressBar.addListener(object : TimeBar.OnScrubListener {
override fun onScrubStart(timeBar: TimeBar, position: Long) {
progressAnimator.cancel()
}
override fun onScrubMove(timeBar: TimeBar, position: Long) {}
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
if (canceled) {
progressAnimator.currentPlayTime = player.player.currentPosition
progressAnimator.duration = player.player.duration
progressAnimator.start()
return
}
// the progress bar should never be available to the user without the player being attached to this view
assert(playerAttached)
seekTo(position)
}
})
}
@OptIn(UnstableApi::class)
private fun createProgressBarAnimator(): ValueAnimator {
return ValueAnimator.ofFloat(0f, 1f).apply {
interpolator = LinearInterpolator()
addUpdateListener { animation ->
progressBar.setPosition(animation.currentPlayTime)
}
}
}
fun setProgressBarDuration(duration: Long) {
progressBar.setDuration(duration)
}
/**
* Attaches this short player instance to the exo player instance for shorts
*/
fun attach() {
// connect the exo player for shorts to the view for this instance
player.attach(videoView, PLAYER_STATE_NAME)
// direct the base player what exo player instance to use
changePlayer(player)
playerAttached = true
player.player.addListener(playerEventListener)
}
fun detach() {
playerAttached = false
player.player.removeListener(playerEventListener)
player.detach()
}
@OptIn(UnstableApi::class)
fun setArtwork(drawable: Drawable?) {
if (drawable != null) {
videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_FILL
videoView.defaultArtwork = drawable
} else {
videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_OFF
videoView.defaultArtwork = null
}
}
fun getPlaybackRate(): Float {
return exoPlayer?.player?.playbackParameters?.speed ?: 1.0f
}
fun setPlaybackRate(playbackRate: Float) {
val exoPlayer = exoPlayer?.player
Logger.i(TAG, "setPlaybackRate playbackRate=$playbackRate exoPlayer=${exoPlayer}")
val param = PlaybackParameters(playbackRate)
exoPlayer?.playbackParameters = param
}
}

View file

@ -6,6 +6,7 @@ import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.RelativeLayout
import androidx.annotation.OptIn
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
@ -86,8 +87,6 @@ import kotlin.math.abs
abstract class FutoVideoPlayerBase : RelativeLayout {
private val TAG = "FutoVideoPlayerBase"
private val TEMP_DIRECTORY = StateApp.instance.getTempDirectory();
private var _mediaSource: MediaSource? = null;
var lastVideoSource: IVideoSource? = null

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<gradient
android:centerX="50%"
android:centerY="50%"
android:endColor="#00FFFFFF"
android:gradientRadius="35%p"
android:startColor="#40FFFFFF"
android:type="radial" />
</shape>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6.31516 18.2609C6.86932 17.8571 7.58672 17.754 8.23108 17.9859C9.36947 18.3983 10.6453 18.6346 12.0028 18.6346C17.3596 18.6346 20.938 15.1765 20.938 11.7613C20.938 8.3462 17.3596 4.88809 12.0028 4.88809C6.64594 4.88809 3.06755 8.3462 3.06755 11.7613C3.06755 13.136 3.60022 14.4591 4.60114 15.5932C4.97058 16.0099 5.151 16.5597 5.10805 17.1182C5.04791 17.8957 4.86319 18.6088 4.62262 19.2403C5.35291 18.9009 5.95861 18.5229 6.31516 18.2652V18.2609ZM1.91628 20.005C1.9936 19.889 2.06663 19.773 2.13536 19.657C2.56494 18.9439 2.97304 18.0074 3.05466 16.955C1.76592 15.4901 1.00557 13.6987 1.00557 11.7613C1.00557 6.82549 5.92854 2.82611 12.0028 2.82611C18.077 2.82611 23 6.82549 23 11.7613C23 16.6972 18.077 20.6966 12.0028 20.6966C10.409 20.6966 8.89693 20.4217 7.53087 19.9276C7.01967 20.3014 6.18629 20.8126 5.19826 21.2422C4.54959 21.5257 3.81072 21.7834 3.04607 21.9338C3.0117 21.9424 2.97734 21.9467 2.94297 21.9553C2.75396 21.9896 2.56924 22.0197 2.37593 22.0369C2.36733 22.0369 2.35445 22.0412 2.34586 22.0412C2.12677 22.0626 1.90769 22.0755 1.6886 22.0755C1.40937 22.0755 1.16022 21.908 1.05282 21.6503C0.945429 21.3925 1.00557 21.1004 1.19888 20.9028C1.37501 20.7224 1.53395 20.529 1.6843 20.3229C1.75733 20.224 1.82607 20.1252 1.8905 20.0264C1.8948 20.0179 1.89909 20.0136 1.90339 20.005H1.91628Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M13.7562 3.3125C13.24 1.5625 10.76 1.5625 10.2437 3.3125L10.1187 3.7375C10.0416 3.99949 9.90682 4.2409 9.72424 4.44402C9.54166 4.64713 9.31594 4.80681 9.06362 4.91133C8.8113 5.01585 8.53878 5.06258 8.26606 5.04807C7.99333 5.03356 7.72731 4.9582 7.4875 4.8275L7.1 4.615C5.49625 3.7425 3.7425 5.49625 4.61625 7.09875L4.8275 7.4875C5.385 8.5125 4.85625 9.78875 3.7375 10.1187L3.3125 10.2437C1.5625 10.76 1.5625 13.24 3.3125 13.7562L3.7375 13.8813C3.99949 13.9584 4.2409 14.0932 4.44402 14.2758C4.64713 14.4583 4.80681 14.6841 4.91133 14.9364C5.01585 15.1887 5.06258 15.4612 5.04807 15.7339C5.03356 16.0067 4.9582 16.2727 4.8275 16.5125L4.615 16.9C3.7425 18.5037 5.49625 20.2575 7.09875 19.3837L7.4875 19.1725C7.72731 19.0418 7.99333 18.9664 8.26606 18.9519C8.53878 18.9374 8.8113 18.9841 9.06362 19.0887C9.31594 19.1932 9.54166 19.3529 9.72424 19.556C9.90682 19.7591 10.0416 20.0005 10.1187 20.2625L10.2437 20.6875C10.76 22.4375 13.24 22.4375 13.7562 20.6875L13.8813 20.2625C13.9584 20.0005 14.0932 19.7591 14.2758 19.556C14.4583 19.3529 14.6841 19.1932 14.9364 19.0887C15.1887 18.9841 15.4612 18.9374 15.7339 18.9519C16.0067 18.9664 16.2727 19.0418 16.5125 19.1725L16.9 19.385C18.5037 20.2575 20.2575 18.5037 19.3837 16.9012L19.1725 16.5125C19.0418 16.2727 18.9664 16.0067 18.9519 15.7339C18.9374 15.4612 18.9841 15.1887 19.0887 14.9364C19.1932 14.6841 19.3529 14.4583 19.556 14.2758C19.7591 14.0932 20.0005 13.9584 20.2625 13.8813L20.6875 13.7562C22.4375 13.24 22.4375 10.76 20.6875 10.2437L20.2625 10.1187C20.0005 10.0416 19.7591 9.90682 19.556 9.72424C19.3529 9.54166 19.1932 9.31594 19.0887 9.06362C18.9841 8.8113 18.9374 8.53878 18.9519 8.26606C18.9664 7.99333 19.0418 7.72731 19.1725 7.4875L19.385 7.1C20.2575 5.49625 18.5037 3.7425 16.9012 4.61625L16.5125 4.8275C16.2727 4.9582 16.0067 5.03356 15.7339 5.04807C15.4612 5.06258 15.1887 5.01585 14.9364 4.91133C14.6841 4.80681 14.4583 4.64713 14.2758 4.44402C14.0932 4.2409 13.9584 3.99949 13.8813 3.7375L13.7562 3.3125ZM12 15.6625C11.0286 15.6625 10.0971 15.2766 9.41022 14.5898C8.72337 13.9029 8.3375 12.9714 8.3375 12C8.3375 11.0286 8.72337 10.0971 9.41022 9.41022C10.0971 8.72337 11.0286 8.3375 12 8.3375C12.971 8.3375 13.9023 8.72324 14.5889 9.40985C15.2755 10.0965 15.6613 11.0277 15.6613 11.9987C15.6613 12.9698 15.2755 13.901 14.5889 14.5876C13.9023 15.2743 12.971 15.66 12 15.66V15.6625Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M4.8338 12.6821H2C2 15.083 2.93455 17.5131 4.7735 19.2991C8.48155 22.9003 14.5109 22.9003 18.219 19.2991C21.927 15.6978 21.927 9.8421 18.219 6.24085C16.5307 4.60125 14.33 3.7229 12.0992 3.57651V1L4.8338 5.06971L12.0992 9.1687V6.32869C13.6065 6.4458 15.0536 7.08993 16.1991 8.20251C18.8219 10.7205 18.8219 14.8194 16.1991 17.3374C13.6065 19.8846 9.38596 19.8846 6.79334 17.3374C5.46688 16.0784 4.80365 14.351 4.8338 12.6821Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M11.2046 1.70818C11.4156 1.50196 11.7017 1.38611 12 1.38611C12.2983 1.38611 12.5844 1.50196 12.7954 1.70818L16.1704 5.00821C16.3753 5.21568 16.4887 5.49354 16.4861 5.78196C16.4836 6.07038 16.3653 6.34628 16.1567 6.55023C15.9481 6.75418 15.6659 6.86987 15.371 6.87237C15.076 6.87488 14.7918 6.764 14.5796 6.56363L13.125 5.14131V15.686C13.125 15.9778 13.0065 16.2576 12.7955 16.4639C12.5845 16.6701 12.2984 16.786 12 16.786C11.7016 16.786 11.4155 16.6701 11.2045 16.4639C10.9935 16.2576 10.875 15.9778 10.875 15.686V5.14131L9.42037 6.56363C9.2082 6.764 8.92402 6.87488 8.62905 6.87237C8.33408 6.86987 8.05191 6.75418 7.84333 6.55023C7.63475 6.34628 7.51643 6.07038 7.51387 5.78196C7.5113 5.49354 7.6247 5.21568 7.82963 5.00821L11.2046 1.70818ZM3 11.286C3 10.7025 3.23705 10.1429 3.65901 9.73033C4.08097 9.31774 4.65326 9.08596 5.25 9.08596H7.5C7.79837 9.08596 8.08452 9.20185 8.2955 9.40814C8.50647 9.61443 8.625 9.89423 8.625 10.186C8.625 10.4777 8.50647 10.7575 8.2955 10.9638C8.08452 11.1701 7.79837 11.286 7.5 11.286H5.25V21.1861H18.75V11.286H16.5C16.2016 11.286 15.9155 11.1701 15.7045 10.9638C15.4935 10.7575 15.375 10.4777 15.375 10.186C15.375 9.89423 15.4935 9.61443 15.7045 9.40814C15.9155 9.20185 16.2016 9.08596 16.5 9.08596H18.75C19.3467 9.08596 19.919 9.31774 20.341 9.73033C20.7629 10.1429 21 10.7025 21 11.286V21.1861C21 21.7696 20.7629 22.3292 20.341 22.7417C19.919 23.1543 19.3467 23.3861 18.75 23.3861H5.25C4.65326 23.3861 4.08097 23.1543 3.65901 22.7417C3.23705 22.3292 3 21.7696 3 21.1861V11.286Z"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:pathData="M9 14.3L5 14.3C4.68082 14.2887 4.36901 14.201 4.09064 14.0444C3.81228 13.8879 3.57546 13.6669 3.4 13.4C3.21079 13.1453 3.08491 12.8492 3.03274 12.5362C2.98058 12.2232 3.00363 11.9023 3.1 11.6L5.5 4.40002C5.8 3.50002 6 3.00002 7.4 3.00002C9.4 3.00002 11.6 3.70002 13.5 4.30002L15 4.70003L15 14.5C13.4053 16.1761 11.9971 18.0203 10.8 20C10.7 20.4 10.3 20.7 9.9 20.9L8.7 20.9C8.3 20.8 8 20.5 7.7 20.2L7.5 18.9L9 14.3ZM19.8 14L17 14L17 6.00002C17 5.46959 17.2107 4.96088 17.5858 4.58581C17.9609 4.21074 18.4696 4.00002 19 4.00002C19.5304 4.00002 20.0391 4.21074 20.4142 4.58581C20.7893 4.96088 21 5.46959 21 6.00002L21 12.8C21 13.5 20.5 14 19.8 14Z"
android:strokeColor="@color/white"
android:strokeWidth="1.5"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M8.99448 14.4L4.99448 14.4C4.6753 14.3886 4.36348 14.301 4.08512 14.1444C3.80675 13.9878 3.56993 13.7669 3.39448 13.5C3.20527 13.2453 3.07938 12.9492 3.02722 12.6362C2.97506 12.3232 2.99811 12.0023 3.09448 11.7L5.49448 4.5C5.79448 3.6 5.99448 3.1 7.39448 3.1C9.39448 3.1 11.5945 3.8 13.4945 4.4L14.9945 4.8L14.9945 14.6C13.3998 16.2761 11.9915 18.1202 10.7945 20.1C10.6945 20.5 10.2945 20.8 9.89448 21L8.69448 21C8.29448 20.9 7.99448 20.6 7.69448 20.3L7.49448 19L8.99448 14.4ZM19.7945 14.1L16.9945 14.1L16.9945 6.1C16.9945 5.56957 17.2052 5.06086 17.5803 4.68579C17.9553 4.31071 18.464 4.1 18.9945 4.1C19.5249 4.1 20.0336 4.31071 20.4087 4.68579C20.7838 5.06086 20.9945 5.56957 20.9945 6.1L20.9945 12.9C20.9945 13.6 20.4945 14.1 19.7945 14.1Z" />
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:pathData="M15 9.69998H19C19.3192 9.71134 19.631 9.79898 19.9094 9.95556C20.1877 10.1121 20.4245 10.3331 20.6 10.6C20.7892 10.8547 20.9151 11.1508 20.9673 11.4638C21.0194 11.7768 20.9964 12.0977 20.9 12.4L18.5 19.6C18.2 20.5 18 21 16.6 21C14.6 21 12.4 20.3 10.5 19.7L9 19.3V9.49998C10.5947 7.82386 12.0029 5.97974 13.2 3.99998C13.3 3.59998 13.7 3.29998 14.1 3.09998H15.3C15.7 3.19998 16 3.49998 16.3 3.79998L16.5 5.09998L15 9.69998ZM4.2 9.99998H7V18C7 18.5304 6.78929 19.0391 6.41421 19.4142C6.03914 19.7893 5.53043 20 5 20C4.46957 20 3.96086 19.7893 3.58579 19.4142C3.21071 19.0391 3 18.5304 3 18V11.2C3 10.5 3.5 9.99998 4.2 9.99998Z"
android:strokeColor="@color/white"
android:strokeWidth="1.5"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M15 9.69998H19C19.3192 9.71134 19.631 9.79898 19.9094 9.95556C20.1877 10.1121 20.4245 10.3331 20.6 10.6C20.7892 10.8547 20.9151 11.1508 20.9673 11.4638C21.0194 11.7768 20.9964 12.0977 20.9 12.4L18.5 19.6C18.2 20.5 18 21 16.6 21C14.6 21 12.4 20.3 10.5 19.7L9 19.3V9.49998C10.5947 7.82386 12.0029 5.97974 13.2 3.99998C13.3 3.59998 13.7 3.29998 14.1 3.09998H15.3C15.7 3.19998 16 3.49998 16.3 3.79998L16.5 5.09998L15 9.69998ZM4.2 9.99998H7V18C7 18.5304 6.78929 19.0391 6.41421 19.4142C6.03914 19.7893 5.53043 20 5 20C4.46957 20 3.96086 19.7893 3.58579 19.4142C3.21071 19.0391 3 18.5304 3 18V11.2C3 10.5 3.5 9.99998 4.2 9.99998Z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M260,540L700,540L700,509.23L260,509.23L260,540ZM260,415.38L700,415.38L700,384.62L260,384.62L260,415.38ZM260,290.77L700,290.77L700,260L260,260L260,290.77ZM840,803.08L716.92,680L175.38,680Q152.33,680 136.16,663.84Q120,647.67 120,624.62L120,175.38Q120,152.33 136.16,136.16Q152.33,120 175.38,120L784.62,120Q807.67,120 823.84,136.16Q840,152.33 840,175.38L840,803.08ZM175.38,649.23L730.62,649.23L809.23,730.77L809.23,175.38Q809.23,166.15 801.54,158.46Q793.85,150.77 784.62,150.77L175.38,150.77Q166.15,150.77 158.46,158.46Q150.77,166.15 150.77,175.38L150.77,624.62Q150.77,633.85 158.46,641.54Q166.15,649.23 175.38,649.23ZM150.77,649.23Q150.77,649.23 150.77,641.54Q150.77,633.85 150.77,624.62L150.77,175.38Q150.77,166.15 150.77,158.46Q150.77,150.77 150.77,150.77L150.77,150.77Q150.77,150.77 150.77,158.46Q150.77,166.15 150.77,175.38L150.77,649.23Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M592.69,651.08L623.46,651.08L623.46,589.15L655.23,589.15Q671.76,589.15 683.57,577.41Q695.38,565.66 695.38,549.23L695.38,411Q695.38,394.47 683.57,382.66Q671.76,370.85 655.23,370.85L563.08,370.85Q546.77,370.85 533.38,382.66Q520,394.47 520,411L520,549.23Q520,565.66 533.38,577.41Q546.77,589.15 563.08,589.15L592.69,589.15L592.69,651.08ZM264.62,589.15L295.38,589.15L295.38,504.77L409.23,504.77L409.23,589.15L440,589.15L440,370.85L409.23,370.85L409.23,474L295.38,474L295.38,370.85L264.62,370.85L264.62,589.15ZM563.08,558.38Q558.46,558.38 554.62,554.54Q550.77,550.69 550.77,546.08L550.77,413.92Q550.77,409.31 554.62,405.46Q558.46,401.62 563.08,401.62L652.31,401.62Q656.92,401.62 660.77,405.46Q664.62,409.31 664.62,413.92L664.62,546.08Q664.62,550.69 660.77,554.54Q656.92,558.38 652.31,558.38L563.08,558.38ZM175.38,760Q152.33,760 136.16,743.84Q120,727.67 120,704.62L120,255.38Q120,232.33 136.16,216.16Q152.33,200 175.38,200L784.62,200Q807.67,200 823.84,216.16Q840,232.33 840,255.38L840,704.62Q840,727.67 823.84,743.84Q807.67,760 784.62,760L175.38,760ZM175.38,729.23L784.62,729.23Q793.85,729.23 801.54,721.54Q809.23,713.85 809.23,704.62L809.23,255.38Q809.23,246.15 801.54,238.46Q793.85,230.77 784.62,230.77L175.38,230.77Q166.15,230.77 158.46,238.46Q150.77,246.15 150.77,255.38L150.77,704.62Q150.77,713.85 158.46,721.54Q166.15,729.23 175.38,729.23ZM150.77,729.23Q150.77,729.23 150.77,721.54Q150.77,713.85 150.77,704.62L150.77,255.38Q150.77,246.15 150.77,238.46Q150.77,230.77 150.77,230.77L150.77,230.77Q150.77,230.77 150.77,238.46Q150.77,246.15 150.77,255.38L150.77,704.62Q150.77,713.85 150.77,721.54Q150.77,729.23 150.77,729.23Z"/>
</vector>

View file

@ -5,5 +5,5 @@
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M405.85,617L619.69,478.77L405.85,341.31L405.85,617ZM175.38,760Q152.33,760 136.16,743.84Q120,727.67 120,704.62L120,255.38Q120,232.33 136.16,216.16Q152.33,200 175.38,200L784.62,200Q807.67,200 823.84,216.16Q840,232.33 840,255.38L840,704.62Q840,727.67 823.84,743.84Q807.67,760 784.62,760L175.38,760ZM175.38,729.23L784.62,729.23Q793.85,729.23 801.54,721.54Q809.23,713.85 809.23,704.62L809.23,255.38Q809.23,246.15 801.54,238.46Q793.85,230.77 784.62,230.77L175.38,230.77Q166.15,230.77 158.46,238.46Q150.77,246.15 150.77,255.38L150.77,704.62Q150.77,713.85 158.46,721.54Q166.15,729.23 175.38,729.23ZM150.77,729.23Q150.77,729.23 150.77,721.54Q150.77,713.85 150.77,704.62L150.77,255.38Q150.77,246.15 150.77,238.46Q150.77,230.77 150.77,230.77L150.77,230.77Q150.77,230.77 150.77,238.46Q150.77,246.15 150.77,255.38L150.77,704.62Q150.77,713.85 150.77,721.54Q150.77,729.23 150.77,729.23L150.77,729.23Z"/>
android:pathData="M405.85,617L619.69,478.77L405.85,341.31L405.85,617ZM175.38,760Q152.33,760 136.16,743.84Q120,727.67 120,704.62L120,255.38Q120,232.33 136.16,216.16Q152.33,200 175.38,200L784.62,200Q807.67,200 823.84,216.16Q840,232.33 840,255.38L840,704.62Q840,727.67 823.84,743.84Q807.67,760 784.62,760L175.38,760ZM175.38,729.23L784.62,729.23Q793.85,729.23 801.54,721.54Q809.23,713.85 809.23,704.62L809.23,255.38Q809.23,246.15 801.54,238.46Q793.85,230.77 784.62,230.77L175.38,230.77Q166.15,230.77 158.46,238.46Q150.77,246.15 150.77,255.38L150.77,704.62Q150.77,713.85 158.46,721.54Q166.15,729.23 175.38,729.23ZM150.77,729.23Q150.77,729.23 150.77,721.54Q150.77,713.85 150.77,704.62L150.77,255.38Q150.77,246.15 150.77,238.46Q150.77,230.77 150.77,230.77L150.77,230.77Q150.77,230.77 150.77,238.46Q150.77,246.15 150.77,255.38L150.77,704.62Q150.77,713.85 150.77,721.54Q150.77,729.23 150.77,729.23Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M405.85,617L619.69,478.77L405.85,341.31L405.85,617ZM175.38,760Q152.15,760 136.08,743.92Q120,727.85 120,704.62L120,255.38Q120,232.15 136.08,216.08Q152.15,200 175.38,200L784.62,200Q807.85,200 823.92,216.08Q840,232.15 840,255.38L840,704.62Q840,727.85 823.92,743.92Q807.85,760 784.62,760L175.38,760Z"/>
</vector>

View file

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M13.15,9.6H33.3V31.45L20.8,44L20.2,43.55Q19.9,43.3 19.75,42.95Q19.6,42.6 19.6,42.2V42L21.7,31.45H6.85Q5.75,31.45 4.9,30.6Q4.05,29.75 4.05,28.7V26.15Q4.05,25.9 4.05,25.575Q4.05,25.25 4.15,24.95L9.75,11.85Q10.1,10.9 11.1,10.25Q12.1,9.6 13.15,9.6ZM31.75,11.15H12.85Q12.4,11.15 11.975,11.375Q11.55,11.6 11.3,12.15L5.6,25.6V28.7Q5.6,29.2 5.95,29.55Q6.3,29.9 6.85,29.9H23.55L21.25,41.45L31.75,30.75ZM31.75,30.75V29.9Q31.75,29.9 31.75,29.55Q31.75,29.2 31.75,28.7V25.6V12.15Q31.75,11.6 31.75,11.375Q31.75,11.15 31.75,11.15ZM33.3,31.45V29.9H40.45V11.15H33.3V9.6H42V31.45Z"/>
android:pathData="M262.65,192.31L666,192.31L666,628.92L415.69,880L403.6,871.19Q398.38,866.31 395.38,859.23Q392.38,852.15 392.38,843.77L392.38,839.92L433.54,628.92L136.85,628.92Q115.46,628.92 98.46,611.92Q81.46,594.92 81.46,573.54L81.46,523.18Q81.46,517.62 81.12,511.27Q80.77,504.92 83,499.46L195.15,237.15Q202.49,218.21 222.44,205.26Q242.39,192.31 262.65,192.31ZM635.23,223.08L256.69,223.08Q248.23,223.08 239.38,227.69Q230.54,232.31 225.92,243.08L112.23,512.08L112.23,573.54Q112.23,583.54 119.15,590.85Q126.08,598.15 136.85,598.15L470.62,598.15L424.54,829.46L635.23,615.46L635.23,223.08ZM635.23,615.46L635.23,615.46L635.23,598.15L635.23,598.15Q635.23,598.15 635.23,590.85Q635.23,583.54 635.23,573.54L635.23,512.08L635.23,243.08Q635.23,232.31 635.23,227.69Q635.23,223.08 635.23,223.08L635.23,223.08L635.23,615.46ZM666,628.92L666,598.15L809,598.15L809,223.08L666,223.08L666,192.31L839.77,192.31L839.77,628.92L666,628.92Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M261.31,192.31L684.46,192.31L684.46,628.92L434.15,880L425,873.23Q417.08,866.77 413.96,859.46Q410.85,852.15 410.85,843.77L410.85,839.92L452,628.92L135.38,628.92Q113.92,628.92 96.96,611.96Q80,595 80,573.54L80,523.08Q80,515.15 81.73,505.65Q83.46,496.15 86.15,489.77L193.69,237.15Q201.92,218.46 221.27,205.38Q240.62,192.31 261.31,192.31ZM715.23,628.92L715.23,192.31L840,192.31L840,628.92L715.23,628.92Z"/>
</vector>

View file

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M34.85,40H14.7V18.15L27.15,5.6L27.8,6.05Q28.05,6.3 28.2,6.65Q28.35,7 28.35,7.4V7.6L26.25,18.15H41.15Q42.2,18.15 43.05,19Q43.9,19.85 43.9,20.95V23.45Q43.9,23.75 43.9,24.05Q43.9,24.35 43.8,24.65L38.2,37.75Q37.8,38.7 36.8,39.35Q35.8,40 34.85,40ZM16.2,38.45H35.15Q35.55,38.45 36,38.225Q36.45,38 36.7,37.45L42.35,24V20.95Q42.35,20.45 42,20.075Q41.65,19.7 41.15,19.7H24.45L26.7,8.15L16.2,18.85ZM16.2,18.85V19.7Q16.2,19.7 16.2,20.075Q16.2,20.45 16.2,20.95V24V37.45Q16.2,38 16.2,38.225Q16.2,38.45 16.2,38.45ZM14.7,18.15V19.7H7.55V38.45H14.7V40H6V18.15Z"/>
android:pathData="M696.77,800L293.54,800L293.54,363.38L543.08,112.31L555.84,121.12Q561.15,126 564.15,133.08Q567.15,140.15 567.15,147.77L567.15,152.38L525.23,363.38L822.69,363.38Q844.08,363.38 861.08,380.38Q878.08,397.38 878.08,418.77L878.08,469.13Q878.08,474.69 878.04,481.04Q878,487.38 875.77,492.85L764.38,755.15Q756,774.22 736.19,787.11Q716.38,800 696.77,800ZM324.31,769.23L702.85,769.23Q710.54,769.23 719.77,764.62Q729,760 733.62,749.23L847.31,480.23L847.31,418.77Q847.31,408.77 840,401.46Q832.69,394.15 822.69,394.15L488.92,394.15L534.23,162.85L324.31,376.85L324.31,769.23ZM324.31,376.85L324.31,376.85L324.31,394.15L324.31,394.15Q324.31,394.15 324.31,401.46Q324.31,408.77 324.31,418.77L324.31,480.23L324.31,749.23Q324.31,760 324.31,764.62Q324.31,769.23 324.31,769.23L324.31,769.23L324.31,376.85ZM293.54,363.38L293.54,394.15L150.54,394.15L150.54,769.23L293.54,769.23L293.54,800L119.77,800L119.77,363.38L293.54,363.38Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M698.69,800L275.54,800L275.54,363.38L525.85,112.31L535.77,119.08Q542.92,125.54 546.04,132.85Q549.15,140.15 549.15,147.77L549.15,152.38L508,363.38L824.62,363.38Q846.08,363.38 863.04,380.35Q880,397.31 880,418.77L880,469.23Q880,477.15 878.27,486.65Q876.54,496.15 873.85,502.54L766.31,755.15Q758.08,773.85 738.73,786.92Q719.38,800 698.69,800ZM244.77,363.38L244.77,800L120,800L120,363.38L244.77,363.38Z"/>
</vector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:drawable="@drawable/desktop_thumb_down_filled" />
<item android:drawable="@drawable/desktop_thumb_down" />
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:drawable="@drawable/desktop_thumb_up_filled" />
<item android:drawable="@drawable/desktop_thumb_up" />
</selector>

View file

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.motion.widget.MotionLayout
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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
tools:context=".activities.MainActivity"
android:background="@color/black"

View file

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="4dp">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />
<FrameLayout
android:id="@+id/short_view_loading_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#77000000"
android:elevation="4dp"
android:visibility="gone">
<ImageView
android:id="@+id/short_view_loader"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="center_vertical|center_horizontal"
android:alpha="0.7"
android:contentDescription="@string/loading"
app:srcCompat="@drawable/ic_loader_animated" />
</FrameLayout>
<!-- zero state -->
<LinearLayout
android:id="@+id/zero_state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginHorizontal="24dp"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:id="@+id/icon"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginBottom="10dp"
android:importantForAccessibility="no"
android:scaleType="fitCenter"
android:src="@drawable/ic_help" />
<TextView
android:id="@+id/text_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:fontFamily="@font/inter_bold"
android:gravity="center"
android:text="@string/no_results"
android:textColor="@color/white"
android:textSize="22dp" />
<TextView
android:id="@+id/text_centered"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="10dp"
android:fontFamily="@font/inter_regular"
android:gravity="center"
android:text="@string/no_results_shorts"
android:textColor="@color/gray_ac"
android:textSize="13dp" />
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/sources_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:buttonIcon="@drawable/ic_creators"
app:buttonSubText="Go to the sources tab"
app:buttonText="Sources" />
</LinearLayout>
<FrameLayout
android:id="@+id/shorts_quality_overview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="100dp"
android:visibility="gone" />
</FrameLayout>

View file

@ -0,0 +1,337 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/standard_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- Drag handle for accessibility -->
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
style="@style/Widget.Material3.BottomSheet.DragHandle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="0dp" />
<FrameLayout
android:id="@+id/content_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/videodetail_container_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical"
tools:ignore="SpeakableTextPresentCheck">
<LinearLayout
android:id="@+id/layout_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!--Title Segment-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!--Title + Meta-->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginEnd="14dp">
<TextView
android:id="@+id/videodetail_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:fontFamily="@font/inter_medium"
android:textColor="@color/white"
android:textSize="17sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/layout_title_right"
app:layout_constraintTop_toTopOf="parent"
tools:text="Some Text" />
<TextView
android:id="@+id/videodetail_meta"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:fontFamily="@font/inter_regular"
android:textColor="#ACACAC"
android:textSize="11sp"
app:layout_constraintLeft_toLeftOf="@id/videodetail_title"
app:layout_constraintRight_toLeftOf="@id/layout_title_right"
app:layout_constraintTop_toBottomOf="@id/videodetail_title"
tools:text="51K views - 3 years ago" />
<!--Source Button-->
<LinearLayout
android:id="@+id/layout_title_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="vertical"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.futo.platformplayer.views.platform.PlatformIndicator
android:id="@+id/videodetail_platform"
android:layout_width="25dp"
android:layout_height="25dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/layout_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/image_like_icon"
android:layout_width="14dp"
android:layout_height="14dp"
android:contentDescription="@string/cd_image_like_icon"
app:srcCompat="@drawable/ic_thumb_up"
app:tint="#ACACAC" />
<TextView
android:id="@+id/text_likes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:gravity="center_vertical"
android:textColor="#ACACAC"
android:textSize="11sp"
tools:text="500K" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/image_dislike_icon"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_marginTop="2dp"
android:contentDescription="@string/cd_image_dislike_icon"
app:srcCompat="@drawable/ic_thumb_down"
app:tint="#ACACAC" />
<TextView
android:id="@+id/text_dislikes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:gravity="center_vertical"
android:textColor="#ACACAC"
android:textSize="11sp"
tools:text="500K" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<!--Channel/Subscribe Segment-->
<LinearLayout
android:id="@+id/videodetail_channel_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="14dp"
android:layout_marginTop="17dp"
android:layout_marginRight="14dp"
android:layout_marginBottom="10dp"
android:orientation="horizontal">
<com.futo.platformplayer.views.others.CreatorThumbnail
android:id="@+id/creator_thumbnail"
android:layout_width="35dp"
android:layout_height="35dp"
android:contentDescription="@string/cd_creator_thumbnail" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/videodetail_channel_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="11sp"
tools:text="Channel Name" />
<TextView
android:id="@+id/videodetail_channel_meta"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:ellipsize="end"
android:maxLines="1"
android:textColor="#ACACAC"
android:textSize="11sp"
tools:text="" />
</LinearLayout>
</LinearLayout>
<!--Description-->
<LinearLayout
android:id="@+id/videodetail_description_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="14dp"
android:layout_marginRight="14dp"
android:animateLayoutChanges="true"
android:background="@drawable/background_videodetail_description"
android:gravity="center"
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingTop="3dp"
android:paddingRight="12dp"
android:paddingBottom="5dp">
<com.futo.platformplayer.views.behavior.NonScrollingTextView
android:id="@+id/videodetail_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:background="@color/transparent"
android:fontFamily="@font/inter_light"
android:isScrollContainer="false"
android:maxWidth="500dp"
android:maxLines="3"
android:textColor="@color/white"
android:textSize="11sp"
tools:text="@string/lorem_ipsum" />
<TextView
android:id="@+id/videodetail_description_view_more"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@color/transparent"
android:fontFamily="@font/inter_regular"
android:text="@string/click_to_read_more"
android:textAlignment="center"
android:textColor="#838181"
android:textSize="12sp" />
</LinearLayout>
<com.futo.platformplayer.views.MonetizationView
android:id="@+id/monetization"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:id="@+id/layout_change_bottom_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="14dp"
android:layout_marginTop="10dp"
android:layout_marginRight="14dp"
android:layout_marginBottom="10dp"
android:background="@drawable/background_videodetail_description"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/button_polycentric"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="90dp"
android:background="?android:attr/selectableItemBackground"
android:ellipsize="marquee"
android:lines="1"
android:maxWidth="100dp"
android:padding="10dp"
android:text="@string/polycentric"
android:textColor="#fff"
android:textSize="11sp" />
<Button
android:id="@+id/button_platform"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:ellipsize="marquee"
android:lines="1"
android:maxWidth="100dp"
android:padding="10dp"
android:text="@string/platform"
android:textColor="#fff"
android:textSize="11sp" />
</LinearLayout>
<com.futo.platformplayer.views.comments.AddCommentView
android:id="@+id/add_comment_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="28dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="28dp"
android:layout_marginBottom="12dp" />
</LinearLayout>
<com.futo.platformplayer.views.segments.CommentsList
android:id="@+id/comments_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="400dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_top" />
</LinearLayout>
<com.futo.platformplayer.views.overlays.DescriptionOverlay
android:id="@+id/videodetail_container_description"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<com.futo.platformplayer.views.overlays.WebviewOverlay
android:id="@+id/videodetail_container_webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<com.futo.platformplayer.views.overlays.RepliesOverlay
android:id="@+id/videodetail_container_replies"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<com.futo.platformplayer.views.overlays.SupportOverlay
android:id="@+id/videodetail_container_support"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>

View file

@ -16,7 +16,7 @@
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<ScrollView
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="15dp"
@ -37,6 +37,6 @@
android:textSize="13sp"
android:textIsSelectable="true"
tools:text="@string/lorem_ipsum" />
</ScrollView>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bottom_menu_border">
<com.futo.platformplayer.views.video.FutoShortPlayer
android:id="@+id/short_player"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include layout="@layout/view_short_overlay" />
<!-- make the overlay clickable to intercept touches -->
<FrameLayout
android:id="@+id/short_view_loading_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#77000000"
android:clickable="true"
android:elevation="4dp"
android:visibility="gone">
<ImageView
android:id="@+id/short_view_loader"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="center_vertical|center_horizontal"
android:alpha="0.7"
android:contentDescription="@string/loading"
app:srcCompat="@drawable/ic_loader_animated" />
</FrameLayout>
</merge>

View file

@ -0,0 +1,507 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Main content buttons and text -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Back button -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:visibility="gone"
android:id="@+id/back_button_container">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:importantForAccessibility="no"
android:src="@drawable/button_shadow"
app:layout_constraintBottom_toBottomOf="@id/back_button"
app:layout_constraintEnd_toEndOf="@id/back_button"
app:layout_constraintStart_toStartOf="@id/back_button"
app:layout_constraintTop_toTopOf="@id/back_button"
app:tint="@color/black"
tools:ignore="ImageContrastCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/back_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:contentDescription="@string/cd_button_back"
app:backgroundTint="@color/transparent"
app:icon="@drawable/ic_back_nav"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/ripple" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Title section -->
<LinearLayout
android:id="@+id/title_section"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="16dp"
android:orientation="vertical"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/buttons_section"
app:layout_constraintStart_toStartOf="parent">
<!-- Channel info row -->
<LinearLayout
android:id="@+id/channel_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minWidth="48dp"
android:orientation="horizontal"
android:paddingVertical="8dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.futo.platformplayer.views.others.CreatorThumbnail
android:id="@+id/creator_thumbnail"
android:layout_width="32dp"
android:layout_height="32dp"
android:contentDescription="@string/cd_creator_thumbnail"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.futo.platformplayer.views.platform.PlatformIndicator
android:id="@+id/short_platform_indicator"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="-12dp"
android:layout_marginBottom="-12dp"
android:background="@drawable/rounded_square_outline"
android:clipToOutline="true"
app:layout_constraintBottom_toTopOf="@id/creator_thumbnail"
app:layout_constraintStart_toEndOf="@id/creator_thumbnail" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/channel_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:paddingHorizontal="4dp"
android:shadowColor="@android:color/black"
android:shadowRadius="8"
android:text=""
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>
<!-- Video title -->
<TextView
android:id="@+id/video_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:ellipsize="end"
android:maxLines="1"
android:paddingHorizontal="4dp"
android:shadowColor="@android:color/black"
android:shadowRadius="8"
android:text=""
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>
<!-- Buttons section -->
<LinearLayout
android:id="@+id/buttons_section"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="16dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<!-- Like button -->
<FrameLayout
android:id="@+id/like_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="12dp"
android:visibility="gone">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:importantForAccessibility="no"
android:src="@drawable/button_shadow"
app:layout_constraintBottom_toBottomOf="@id/like_button"
app:layout_constraintEnd_toEndOf="@id/like_button"
app:layout_constraintStart_toStartOf="@id/like_button"
app:layout_constraintTop_toTopOf="@id/like_button"
app:tint="@color/black"
tools:ignore="ImageContrastCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/like_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:checkable="true"
android:contentDescription="@string/cd_image_like_icon"
app:backgroundTint="@color/transparent"
app:icon="@drawable/thumb_up_selector"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/ripple"
app:toggleCheckedStateOnClick="false" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/like_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:paddingHorizontal="4dp"
android:shadowColor="@android:color/black"
android:shadowRadius="8"
android:textColor="@android:color/white"
android:textSize="12sp" />
</FrameLayout>
<!-- Dislike button -->
<FrameLayout
android:id="@+id/dislike_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="12dp"
android:visibility="gone">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:importantForAccessibility="no"
android:src="@drawable/button_shadow"
app:layout_constraintBottom_toBottomOf="@id/dislike_button"
app:layout_constraintEnd_toEndOf="@id/dislike_button"
app:layout_constraintStart_toStartOf="@id/dislike_button"
app:layout_constraintTop_toTopOf="@id/dislike_button"
app:tint="@color/black"
tools:ignore="ImageContrastCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/dislike_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:checkable="true"
android:contentDescription="@string/cd_image_dislike_icon"
app:backgroundTint="@color/transparent"
app:icon="@drawable/thumb_down_selector"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/ripple"
app:toggleCheckedStateOnClick="false" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/dislike_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:paddingHorizontal="4dp"
android:shadowColor="@android:color/black"
android:shadowRadius="8"
android:textColor="@android:color/white"
android:textSize="12sp" />
</FrameLayout>
<!-- Comments button -->
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="12dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:importantForAccessibility="no"
android:src="@drawable/button_shadow"
app:layout_constraintBottom_toBottomOf="@id/comments_button"
app:layout_constraintEnd_toEndOf="@id/comments_button"
app:layout_constraintStart_toStartOf="@id/comments_button"
app:layout_constraintTop_toTopOf="@id/comments_button"
app:tint="@color/black"
tools:ignore="ImageContrastCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/comments_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:contentDescription="@string/comments"
app:icon="@drawable/desktop_comments"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/ripple" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:importantForAccessibility="no"
android:paddingHorizontal="4dp"
android:shadowColor="@android:color/black"
android:shadowRadius="8"
android:text="@string/comments"
android:textColor="@android:color/white"
android:textSize="12sp"
tools:ignore="TextContrastCheck" />
</FrameLayout>
<!-- Share button -->
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="12dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:importantForAccessibility="no"
android:src="@drawable/button_shadow"
app:layout_constraintBottom_toBottomOf="@id/share_button"
app:layout_constraintEnd_toEndOf="@id/share_button"
app:layout_constraintStart_toStartOf="@id/share_button"
app:layout_constraintTop_toTopOf="@id/share_button"
app:tint="@color/black"
tools:ignore="ImageContrastCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/share_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:contentDescription="@string/share"
app:icon="@drawable/desktop_share"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/ripple" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:importantForAccessibility="no"
android:paddingHorizontal="4dp"
android:shadowColor="@android:color/black"
android:shadowRadius="8"
android:text="@string/share"
android:textColor="@android:color/white"
android:textSize="12sp"
tools:ignore="TextContrastCheck" />
</FrameLayout>
<!-- Refresh button -->
<FrameLayout
android:id="@+id/refresh_button_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="12dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:importantForAccessibility="no"
android:src="@drawable/button_shadow"
app:layout_constraintBottom_toBottomOf="@id/refresh_button"
app:layout_constraintEnd_toEndOf="@id/refresh_button"
app:layout_constraintStart_toStartOf="@id/refresh_button"
app:layout_constraintTop_toTopOf="@id/refresh_button"
app:tint="@color/black"
tools:ignore="ImageContrastCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/refresh_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="8dp"
android:contentDescription="@string/refresh"
app:icon="@drawable/desktop_refresh"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/ripple" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:importantForAccessibility="no"
android:paddingHorizontal="4dp"
android:shadowColor="@android:color/black"
android:shadowRadius="8"
android:text="@string/refresh"
android:textColor="@android:color/white"
android:textSize="12sp"
tools:ignore="TextContrastCheck" />
</FrameLayout>
<!-- Quality/More button -->
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:importantForAccessibility="no"
android:src="@drawable/button_shadow"
app:layout_constraintBottom_toBottomOf="@id/quality_button"
app:layout_constraintEnd_toEndOf="@id/quality_button"
app:layout_constraintStart_toStartOf="@id/quality_button"
app:layout_constraintTop_toTopOf="@id/quality_button"
app:tint="@color/black"
tools:ignore="ImageContrastCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/quality_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:contentDescription="@string/quality"
app:icon="@drawable/desktop_gear"
app:iconSize="24dp"
app:iconTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/ripple" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:importantForAccessibility="no"
android:paddingHorizontal="4dp"
android:shadowColor="@android:color/black"
android:shadowRadius="8"
android:text="@string/quality"
android:textColor="@android:color/white"
android:textSize="12sp"
tools:ignore="TextContrastCheck" />
</FrameLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Play/Pause overlay -->
<FrameLayout
android:id="@+id/play_pause_overlay"
android:layout_width="94dp"
android:layout_height="94dp"
android:layout_gravity="center"
android:background="@drawable/rounded_outline"
android:backgroundTint="@color/play_pause_background"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/play_pause_icon"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:contentDescription="@string/play"
android:src="@drawable/ic_play"
app:tint="@android:color/white" />
</FrameLayout>
</FrameLayout>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.media3.ui.PlayerView
android:id="@+id/short_player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/short_player_progress_bar"
android:background="@color/black"
app:default_artwork="@drawable/placeholder_video_thumbnail"
app:resize_mode="fit"
app:show_buffering="when_playing"
app:use_artwork="true"
app:use_controller="false" />
<androidx.media3.ui.DefaultTimeBar
android:id="@+id/short_player_progress_bar"
android:layout_width="match_parent"
android:layout_height="6dp"
android:layout_alignParentBottom="true"
app:bar_height="6dp"
app:buffered_color="#DDEEEEEE"
app:played_color="@color/colorPrimary"
app:scrubber_disabled_size="0dp"
app:scrubber_dragged_size="0dp"
app:scrubber_enabled_size="0dp"
app:unplayed_color="#55EEEEEE" />
</merge>

View file

@ -38,4 +38,151 @@
<color name="overlay">#B3000000</color>
<color name="text_color_tinted">#ACACAC</color>
<color name="pastel_red">#C25353</color>
<!--shorts-->
<color name="play_pause_background">#33555555</color>
<color name="ripple">#33FFFFFF</color>
<!--material 3 colors-->
<color name="md_theme_primary">#8F4C38</color>
<color name="md_theme_onPrimary">#FFFFFF</color>
<color name="md_theme_primaryContainer">#FFDBD1</color>
<color name="md_theme_onPrimaryContainer">#723523</color>
<color name="md_theme_secondary">#77574E</color>
<color name="md_theme_onSecondary">#FFFFFF</color>
<color name="md_theme_secondaryContainer">#FFDBD1</color>
<color name="md_theme_onSecondaryContainer">#5D4037</color>
<color name="md_theme_tertiary">#6C5D2F</color>
<color name="md_theme_onTertiary">#FFFFFF</color>
<color name="md_theme_tertiaryContainer">#F5E1A7</color>
<color name="md_theme_onTertiaryContainer">#534619</color>
<color name="md_theme_error">#BA1A1A</color>
<color name="md_theme_onError">#FFFFFF</color>
<color name="md_theme_errorContainer">#FFDAD6</color>
<color name="md_theme_onErrorContainer">#93000A</color>
<color name="md_theme_background">#FFF8F6</color>
<color name="md_theme_onBackground">#231917</color>
<color name="md_theme_surface">#FFF8F6</color>
<color name="md_theme_onSurface">#231917</color>
<color name="md_theme_surfaceVariant">#F5DED8</color>
<color name="md_theme_onSurfaceVariant">#53433F</color>
<color name="md_theme_outline">#85736E</color>
<color name="md_theme_outlineVariant">#D8C2BC</color>
<color name="md_theme_scrim">#000000</color>
<color name="md_theme_inverseSurface">#392E2B</color>
<color name="md_theme_inverseOnSurface">#FFEDE8</color>
<color name="md_theme_inversePrimary">#FFB5A0</color>
<color name="md_theme_primaryFixed">#FFDBD1</color>
<color name="md_theme_onPrimaryFixed">#3A0B01</color>
<color name="md_theme_primaryFixedDim">#FFB5A0</color>
<color name="md_theme_onPrimaryFixedVariant">#723523</color>
<color name="md_theme_secondaryFixed">#FFDBD1</color>
<color name="md_theme_onSecondaryFixed">#2C150F</color>
<color name="md_theme_secondaryFixedDim">#E7BDB2</color>
<color name="md_theme_onSecondaryFixedVariant">#5D4037</color>
<color name="md_theme_tertiaryFixed">#F5E1A7</color>
<color name="md_theme_onTertiaryFixed">#231B00</color>
<color name="md_theme_tertiaryFixedDim">#D8C58D</color>
<color name="md_theme_onTertiaryFixedVariant">#534619</color>
<color name="md_theme_surfaceDim">#E8D6D2</color>
<color name="md_theme_surfaceBright">#FFF8F6</color>
<color name="md_theme_surfaceContainerLowest">#FFFFFF</color>
<color name="md_theme_surfaceContainerLow">#FFF1ED</color>
<color name="md_theme_surfaceContainer">#FCEAE5</color>
<color name="md_theme_surfaceContainerHigh">#F7E4E0</color>
<color name="md_theme_surfaceContainerHighest">#F1DFDA</color>
<color name="md_theme_primary_mediumContrast">#5D2514</color>
<color name="md_theme_onPrimary_mediumContrast">#FFFFFF</color>
<color name="md_theme_primaryContainer_mediumContrast">#A15A45</color>
<color name="md_theme_onPrimaryContainer_mediumContrast">#FFFFFF</color>
<color name="md_theme_secondary_mediumContrast">#4B2F28</color>
<color name="md_theme_onSecondary_mediumContrast">#FFFFFF</color>
<color name="md_theme_secondaryContainer_mediumContrast">#87655C</color>
<color name="md_theme_onSecondaryContainer_mediumContrast">#FFFFFF</color>
<color name="md_theme_tertiary_mediumContrast">#41350A</color>
<color name="md_theme_onTertiary_mediumContrast">#FFFFFF</color>
<color name="md_theme_tertiaryContainer_mediumContrast">#7B6C3C</color>
<color name="md_theme_onTertiaryContainer_mediumContrast">#FFFFFF</color>
<color name="md_theme_error_mediumContrast">#740006</color>
<color name="md_theme_onError_mediumContrast">#FFFFFF</color>
<color name="md_theme_errorContainer_mediumContrast">#CF2C27</color>
<color name="md_theme_onErrorContainer_mediumContrast">#FFFFFF</color>
<color name="md_theme_background_mediumContrast">#FFF8F6</color>
<color name="md_theme_onBackground_mediumContrast">#231917</color>
<color name="md_theme_surface_mediumContrast">#FFF8F6</color>
<color name="md_theme_onSurface_mediumContrast">#180F0D</color>
<color name="md_theme_surfaceVariant_mediumContrast">#F5DED8</color>
<color name="md_theme_onSurfaceVariant_mediumContrast">#41332F</color>
<color name="md_theme_outline_mediumContrast">#5F4F4A</color>
<color name="md_theme_outlineVariant_mediumContrast">#7B6964</color>
<color name="md_theme_scrim_mediumContrast">#000000</color>
<color name="md_theme_inverseSurface_mediumContrast">#392E2B</color>
<color name="md_theme_inverseOnSurface_mediumContrast">#FFEDE8</color>
<color name="md_theme_inversePrimary_mediumContrast">#FFB5A0</color>
<color name="md_theme_primaryFixed_mediumContrast">#A15A45</color>
<color name="md_theme_onPrimaryFixed_mediumContrast">#FFFFFF</color>
<color name="md_theme_primaryFixedDim_mediumContrast">#84422F</color>
<color name="md_theme_onPrimaryFixedVariant_mediumContrast">#FFFFFF</color>
<color name="md_theme_secondaryFixed_mediumContrast">#87655C</color>
<color name="md_theme_onSecondaryFixed_mediumContrast">#FFFFFF</color>
<color name="md_theme_secondaryFixedDim_mediumContrast">#6D4D45</color>
<color name="md_theme_onSecondaryFixedVariant_mediumContrast">#FFFFFF</color>
<color name="md_theme_tertiaryFixed_mediumContrast">#7B6C3C</color>
<color name="md_theme_onTertiaryFixed_mediumContrast">#FFFFFF</color>
<color name="md_theme_tertiaryFixedDim_mediumContrast">#615426</color>
<color name="md_theme_onTertiaryFixedVariant_mediumContrast">#FFFFFF</color>
<color name="md_theme_surfaceDim_mediumContrast">#D4C3BE</color>
<color name="md_theme_surfaceBright_mediumContrast">#FFF8F6</color>
<color name="md_theme_surfaceContainerLowest_mediumContrast">#FFFFFF</color>
<color name="md_theme_surfaceContainerLow_mediumContrast">#FFF1ED</color>
<color name="md_theme_surfaceContainer_mediumContrast">#F7E4E0</color>
<color name="md_theme_surfaceContainerHigh_mediumContrast">#EBD9D4</color>
<color name="md_theme_surfaceContainerHighest_mediumContrast">#DFCEC9</color>
<color name="md_theme_primary_highContrast">#501B0B</color>
<color name="md_theme_onPrimary_highContrast">#FFFFFF</color>
<color name="md_theme_primaryContainer_highContrast">#753725</color>
<color name="md_theme_onPrimaryContainer_highContrast">#FFFFFF</color>
<color name="md_theme_secondary_highContrast">#3F261E</color>
<color name="md_theme_onSecondary_highContrast">#FFFFFF</color>
<color name="md_theme_secondaryContainer_highContrast">#60423A</color>
<color name="md_theme_onSecondaryContainer_highContrast">#FFFFFF</color>
<color name="md_theme_tertiary_highContrast">#362B02</color>
<color name="md_theme_onTertiary_highContrast">#FFFFFF</color>
<color name="md_theme_tertiaryContainer_highContrast">#55481C</color>
<color name="md_theme_onTertiaryContainer_highContrast">#FFFFFF</color>
<color name="md_theme_error_highContrast">#600004</color>
<color name="md_theme_onError_highContrast">#FFFFFF</color>
<color name="md_theme_errorContainer_highContrast">#98000A</color>
<color name="md_theme_onErrorContainer_highContrast">#FFFFFF</color>
<color name="md_theme_background_highContrast">#FFF8F6</color>
<color name="md_theme_onBackground_highContrast">#231917</color>
<color name="md_theme_surface_highContrast">#FFF8F6</color>
<color name="md_theme_onSurface_highContrast">#000000</color>
<color name="md_theme_surfaceVariant_highContrast">#F5DED8</color>
<color name="md_theme_onSurfaceVariant_highContrast">#000000</color>
<color name="md_theme_outline_highContrast">#372925</color>
<color name="md_theme_outlineVariant_highContrast">#554641</color>
<color name="md_theme_scrim_highContrast">#000000</color>
<color name="md_theme_inverseSurface_highContrast">#392E2B</color>
<color name="md_theme_inverseOnSurface_highContrast">#FFFFFF</color>
<color name="md_theme_inversePrimary_highContrast">#FFB5A0</color>
<color name="md_theme_primaryFixed_highContrast">#753725</color>
<color name="md_theme_onPrimaryFixed_highContrast">#FFFFFF</color>
<color name="md_theme_primaryFixedDim_highContrast">#592111</color>
<color name="md_theme_onPrimaryFixedVariant_highContrast">#FFFFFF</color>
<color name="md_theme_secondaryFixed_highContrast">#60423A</color>
<color name="md_theme_onSecondaryFixed_highContrast">#FFFFFF</color>
<color name="md_theme_secondaryFixedDim_highContrast">#472C24</color>
<color name="md_theme_onSecondaryFixedVariant_highContrast">#FFFFFF</color>
<color name="md_theme_tertiaryFixed_highContrast">#55481C</color>
<color name="md_theme_onTertiaryFixed_highContrast">#FFFFFF</color>
<color name="md_theme_tertiaryFixedDim_highContrast">#3D3206</color>
<color name="md_theme_onTertiaryFixedVariant_highContrast">#FFFFFF</color>
<color name="md_theme_surfaceDim_highContrast">#C6B5B1</color>
<color name="md_theme_surfaceBright_highContrast">#FFF8F6</color>
<color name="md_theme_surfaceContainerLowest_highContrast">#FFFFFF</color>
<color name="md_theme_surfaceContainerLow_highContrast">#FFEDE8</color>
<color name="md_theme_surfaceContainer_highContrast">#F1DFDA</color>
<color name="md_theme_surfaceContainerHigh_highContrast">#E2D1CC</color>
<color name="md_theme_surfaceContainerHighest_highContrast">#D4C3BE</color>
</resources>

View file

@ -27,6 +27,7 @@
<string name="failed_to_retrieve_data_are_you_connected">Failed to retrieve data, are you connected?</string>
<string name="settings">Settings</string>
<string name="history">History</string>
<string name="shorts">Shorts</string>
<string name="sources">Sources</string>
<string name="buy">Buy</string>
<string name="faq">FAQ</string>
@ -37,6 +38,7 @@
<string name="preferred_quality">Preferred Quality</string>
<string name="preferred_quality_description">Default quality for watching a video</string>
<string name="update">Update</string>
<string name="refresh">Refresh Feed</string>
<string name="close">Close</string>
<string name="never">Never</string>
<string name="import_options">Select any of the following available import options.</string>
@ -85,6 +87,8 @@
<string name="these_creators_not_in_group">These creators are not in this group.</string>
<string name="disabled">Disabled</string>
<string name="watch_later">Watch Later</string>
<string name="no_results">Nothing to see here</string>
<string name="no_results_shorts">The enabled sources do not have any short video results.</string>
<string name="create">Create</string>
<string name="ok">OK</string>
<string name="yes">Yes</string>
@ -178,6 +182,7 @@
<string name="copy">Copy</string>
<string name="copy_your_identity_to_clipboard">Copy your identity to clipboard</string>
<string name="polycentric">Polycentric</string>
<string name="platform">Platform</string>
<string name="profile_name">Profile Name</string>
<string name="this_will_be_visible_to_other_users">This will be visible to other users</string>
<string name="create_profile">Create Profile</string>
@ -601,6 +606,7 @@
<string name="enable_where_this_plugins_content_are_visible">Enable where this plugin\'s content are visible</string>
<string name="show_content_in_home_tab">Show content in home tab</string>
<string name="show_content_in_search_results">Show content in search results</string>
<string name="show_content_in_shorts_tab">Show content in shorts tab</string>
<string name="no_valid_url_provided">No valid URL provided..</string>
<string name="invalid_config_format">Invalid Config Format</string>
<string name="failed_to_fetch_configuration">Failed to fetch configuration</string>

View file

@ -41,4 +41,18 @@
<item name="cornerSizeBottomLeft">4dp</item>
<item name="cornerSizeBottomRight">4dp</item>
</style>
<!--this section here attempts to emulate the look of the default material 3 bottom sheet i got the values from the material 3 theme-->
<style name="Custom.BottomSheet.ShapeAppearance" parent="">
<item name="cornerFamily">?attr/shapeCornerFamily</item>
<item name="cornerSize">28dp</item>
</style>
<style name="Custom.BottomSheet.Modal.Style" parent="Widget.MaterialComponents.BottomSheet.Modal">
<item name="shapeAppearanceOverlay">@style/Custom.BottomSheet.ShapeAppearance</item>
<!-- force black background while grayjay only has a dark theme TODO remove when adding support for light mode-->
<item name="backgroundTint">@color/black</item>
</style>
<style name="Custom.BottomSheetDialog.Theme" parent="@style/ThemeOverlay.Material3.BottomSheetDialog">
<item name="bottomSheetStyle">@style/Custom.BottomSheet.Modal.Style</item>
</style>
</resources>

View file

@ -49,6 +49,42 @@
<item name="android:listViewStyle">@style/Theme.FutoVideo.ListView</item>
<item name="android:textViewStyle">@style/Theme.FutoVideo.TextView</item>
<item name="android:checkedTextViewStyle">@style/Theme.FutoVideo.CheckedTextView</item>
<!-- Material3 attributes (needed if parent="Theme.MaterialComponents"). -->
<item name="colorPrimaryInverse">@color/md_theme_inversePrimary</item>
<item name="colorPrimaryContainer">@color/md_theme_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/md_theme_onPrimaryContainer</item>
<item name="colorSecondaryContainer">@color/md_theme_secondaryContainer</item>
<item name="colorOnSecondaryContainer">@color/md_theme_onSecondaryContainer</item>
<item name="colorTertiary">@color/md_theme_tertiary</item>
<item name="colorOnTertiary">@color/md_theme_onTertiary</item>
<item name="colorTertiaryContainer">@color/md_theme_tertiaryContainer</item>
<item name="colorOnTertiaryContainer">@color/md_theme_onTertiaryContainer</item>
<item name="colorSurfaceVariant">@color/md_theme_surfaceVariant</item>
<item name="colorOnSurfaceVariant">@color/md_theme_onSurfaceVariant</item>
<item name="colorSurfaceInverse">@color/md_theme_inverseSurface</item>
<item name="colorOnSurfaceInverse">@color/md_theme_inverseOnSurface</item>
<item name="colorOutline">@color/md_theme_outline</item>
<item name="colorErrorContainer">@color/md_theme_errorContainer</item>
<item name="colorOnErrorContainer">@color/md_theme_onErrorContainer</item>
<item name="textAppearanceDisplayLarge">@style/TextAppearance.Material3.DisplayLarge</item>
<item name="textAppearanceDisplayMedium">@style/TextAppearance.Material3.DisplayMedium</item>
<item name="textAppearanceDisplaySmall">@style/TextAppearance.Material3.DisplaySmall</item>
<item name="textAppearanceHeadlineLarge">@style/TextAppearance.Material3.HeadlineLarge</item>
<item name="textAppearanceHeadlineMedium">@style/TextAppearance.Material3.HeadlineMedium</item>
<item name="textAppearanceHeadlineSmall">@style/TextAppearance.Material3.HeadlineSmall</item>
<item name="textAppearanceTitleLarge">@style/TextAppearance.Material3.TitleLarge</item>
<item name="textAppearanceTitleMedium">@style/TextAppearance.Material3.TitleMedium</item>
<item name="textAppearanceTitleSmall">@style/TextAppearance.Material3.TitleSmall</item>
<item name="textAppearanceBodyLarge">@style/TextAppearance.Material3.BodyLarge</item>
<item name="textAppearanceBodyMedium">@style/TextAppearance.Material3.BodyMedium</item>
<item name="textAppearanceBodySmall">@style/TextAppearance.Material3.BodySmall</item>
<item name="textAppearanceLabelLarge">@style/TextAppearance.Material3.LabelLarge</item>
<item name="textAppearanceLabelMedium">@style/TextAppearance.Material3.LabelMedium</item>
<item name="textAppearanceLabelSmall">@style/TextAppearance.Material3.LabelSmall</item>
<item name="shapeAppearanceSmallComponent">@style/ShapeAppearance.Material3.SmallComponent</item>
<item name="shapeAppearanceMediumComponent">@style/ShapeAppearance.Material3.MediumComponent</item>
<item name="shapeAppearanceLargeComponent">@style/ShapeAppearance.Material3.LargeComponent</item>
</style>
<style name="Theme.FutoVideo.NoActionBar" parent="Theme.FutoVideo">