initial POC shorts tab

Changelog: added
This commit is contained in:
Kai 2025-03-07 14:27:18 -06:00
parent c83a9924e2
commit f63f9dd6db
No known key found for this signature in database
17 changed files with 1235 additions and 15 deletions

View file

@ -143,6 +143,10 @@ android {
}
buildFeatures {
buildConfig true
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.2"
}
sourceSets {
main {
@ -215,6 +219,7 @@ dependencies {
//Database
implementation("androidx.room:room-runtime:2.6.1")
annotationProcessor("androidx.room:room-compiler:2.6.1")
debugImplementation 'androidx.compose.ui:ui-tooling:1.7.8'
ksp("androidx.room:room-compiler:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
@ -228,4 +233,10 @@ dependencies {
testImplementation "org.mockito:mockito-core:5.4.0"
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
//Compose
def composeBom = platform('androidx.compose:compose-bom:2025.02.00')
implementation composeBom
androidTestImplementation composeBom
implementation 'androidx.compose.material3:material3'
}

View file

@ -1,13 +1,11 @@
package com.futo.platformplayer.activities
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.media.AudioManager
import android.net.Uri
import android.os.Bundle
import android.os.StrictMode
@ -57,6 +55,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
@ -74,7 +73,6 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.receivers.MediaButtonReceiver
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
@ -161,6 +159,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;
@ -315,6 +314,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragPostDetail = PostDetailFragment.newInstance();
_fragWatchlist = WatchLaterFragment.newInstance();
_fragHistory = HistoryFragment.newInstance();
_fragShorts = ShortsFragment.newInstance();
_fragSourceDetail = SourceDetailFragment.newInstance();
_fragDownloads = DownloadsFragment();
_fragImportSubscriptions = ImportSubscriptionsFragment.newInstance();
@ -1088,6 +1088,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if (segment.isMainView) {
var transaction = supportFragmentManager.beginTransaction();
transaction.setReorderingAllowed(true)
if (segment.topBar != null) {
if (segment.topBar != fragCurrent.topBar) {
transaction = transaction
@ -1188,6 +1189,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
PostDetailFragment::class -> _fragPostDetail 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

@ -386,16 +386,17 @@ class MenuBottomBarFragment : MainActivityFragment() {
it.navigate<HomeFragment>()
}
}),
ButtonDefinition(1, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate<SubscriptionsFeedFragment>() }),
ButtonDefinition(2, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>() }),
ButtonDefinition(3, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>() }),
ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate<PlaylistsFragment>() }),
ButtonDefinition(5, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate<HistoryFragment>() }),
ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>() }),
ButtonDefinition(1, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.shorts, canToggle = true, { it.currentMain is ShortsFragment }, { it.navigate<ShortsFragment>() }),
ButtonDefinition(2, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscriptions, canToggle = true, { it.currentMain is SubscriptionsFeedFragment }, { it.navigate<SubscriptionsFeedFragment>() }),
ButtonDefinition(3, R.drawable.ic_creators, R.drawable.ic_creators_filled, R.string.creators, canToggle = false, { it.currentMain is CreatorsFragment }, { it.navigate<CreatorsFragment>() }),
ButtonDefinition(4, R.drawable.ic_sources, R.drawable.ic_sources_filled, R.string.sources, canToggle = false, { it.currentMain is SourcesFragment }, { it.navigate<SourcesFragment>() }),
ButtonDefinition(5, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate<PlaylistsFragment>() }),
ButtonDefinition(6, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate<HistoryFragment>() }),
ButtonDefinition(7, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>() }),
ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>() }),
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>() }),
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>() }),
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, {
ButtonDefinition(11, R.drawable.ic_settings, R.drawable.ic_settings_filled, R.string.settings, canToggle = false, { false }, {
val c = it.context ?: return@ButtonDefinition;
Logger.i(TAG, "settings preventPictureInPicture()");
it.requireFragment<VideoDetailFragment>().preventPictureInPicture();

View file

@ -0,0 +1,427 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.app.Dialog
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Animatable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.SoundEffectConstants
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ThumbUp
import androidx.compose.material.icons.outlined.ThumbUp
import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.LocalRippleConfiguration
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RippleConfiguration
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.engine.exceptions.ScriptAgeException
import com.futo.platformplayer.engine.exceptions.ScriptException
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.exceptions.UnsupportedCastException
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.views.video.FutoShortPlayer
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.OffsetDateTime
import kotlin.coroutines.cancellation.CancellationException
@OptIn(ExperimentalMaterial3Api::class)
@UnstableApi
class ShortView : ConstraintLayout {
private var mainFragment: MainFragment? = null
private val player: FutoShortPlayer
private val overlayLoading: FrameLayout
private val overlayLoadingSpinner: ImageView
private var url: String? = null
private var video: IPlatformVideo? = null
private var videoDetails: IPlatformVideoDetails? = null
private var playWhenReady = false
private var _lastVideoSource: IVideoSource? = null
private var _lastAudioSource: IAudioSource? = null
private var _lastSubtitleSource: ISubtitleSource? = null
private var loadVideoJob: Job? = null
private val bottomSheet: ModalBottomSheet = ModalBottomSheet()
// Required constructor for XML inflation
constructor(context: Context) : super(context) {
inflate(context, R.layout.view_short, this)
player = findViewById(R.id.short_player)
overlayLoading = findViewById(R.id.short_view_loading_overlay)
overlayLoadingSpinner = findViewById(R.id.short_view_loader)
setupComposeView()
}
// Required constructor for XML inflation with attributes
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
inflate(context, R.layout.view_short, this)
player = findViewById(R.id.short_player)
overlayLoading = findViewById(R.id.short_view_loading_overlay)
overlayLoadingSpinner = findViewById(R.id.short_view_loader)
setupComposeView()
}
// Required constructor for XML inflation with attributes and style
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
inflate(context, R.layout.view_short, this)
player = findViewById(R.id.short_player)
overlayLoading = findViewById(R.id.short_view_loading_overlay)
overlayLoadingSpinner = findViewById(R.id.short_view_loader)
setupComposeView()
}
constructor(inflater: LayoutInflater, fragment: MainFragment) : super(inflater.context) {
this.mainFragment = fragment
inflater.inflate(R.layout.view_short, this, true)
player = findViewById(R.id.short_player)
overlayLoading = findViewById(R.id.short_view_loading_overlay)
overlayLoadingSpinner = findViewById(R.id.short_view_loader)
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT
)
}
private fun setupComposeView () {
val composeView: ComposeView = findViewById(R.id.compose_view_test_button)
composeView.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
// In Compose world
MaterialTheme {
var checked by remember { mutableStateOf(false) }
val tint = Color.White
val alpha = 0.2f
val rippleConfiguration =
RippleConfiguration(color = tint, rippleAlpha = RippleAlpha(alpha, alpha, alpha, alpha))
val view = LocalView.current
CompositionLocalProvider(LocalRippleConfiguration provides rippleConfiguration) {
IconToggleButton(
checked = checked,
onCheckedChange = {
checked = it
view.playSoundEffect(SoundEffectConstants.CLICK)
},
) {
if (checked) {
Icon(
Icons.Filled.ThumbUp, contentDescription = "Liked", tint = tint,
)
} else {
Icon(
Icons.Outlined.ThumbUp, contentDescription = "Not Liked", tint = tint,
)
}
}
}
}
}
}
}
fun setMainFragment(fragment: MainFragment) {
this.mainFragment = fragment
}
fun setVideo(url: String) {
if (url == this.url) {
return
}
loadVideo(url)
}
fun setVideo(video: IPlatformVideo) {
if (url == video.url) {
return
}
this.video = video
loadVideo(video.url)
}
fun setVideo(videoDetails: IPlatformVideoDetails) {
if (url == videoDetails.url) {
return
}
this.videoDetails = videoDetails
}
fun play() {
player.attach()
playVideo()
}
fun stop() {
playWhenReady = false
player.clear()
player.detach()
}
fun detach() {
loadVideoJob?.cancel()
}
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()
}
}
private fun loadVideo(url: String) {
loadVideoJob?.cancel()
loadVideoJob = CoroutineScope(Dispatchers.Main).launch {
setLoading(true)
_lastVideoSource = null
_lastAudioSource = null
_lastSubtitleSource = null
val result = try {
withContext(StateApp.instance.scope.coroutineContext) {
StatePlatform.instance.getContentDetails(url).await()
}
} catch (e: CancellationException) {
return@launch
} catch (e: NoPlatformClientException) {
Logger.w(TAG, "exception<NoPlatformClientException>", e)
UIDialogs.showDialog(
context, R.drawable.ic_sources, "No source enabled to support this video\n(${url})", null, null, 0, UIDialogs.Action(
"Close", { }, UIDialogs.ActionStyle.PRIMARY
)
)
return@launch
} catch (e: ScriptLoginRequiredException) {
Logger.w(TAG, "exception<ScriptLoginRequiredException>", e)
UIDialogs.showDialog(context, R.drawable.ic_security, "Authentication", e.message, null, 0, UIDialogs.Action("Cancel", {}), UIDialogs.Action("Login", {
val id = e.config.let { if (it is SourcePluginConfig) it.id else null }
val didLogin =
if (id == null) false else StatePlugins.instance.loginPlugin(context, id) {
loadVideo(url)
}
if (!didLogin) UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, "Failed to login")
}, UIDialogs.ActionStyle.PRIMARY)
)
return@launch
} catch (e: ContentNotAvailableYetException) {
Logger.w(TAG, "exception<ContentNotAvailableYetException>", e)
UIDialogs.showSingleButtonDialog(context, R.drawable.ic_schedule, "Video is available in ${e.availableWhen}.", "Close") { }
return@launch
} catch (e: ScriptImplementationException) {
Logger.w(TAG, "exception<ScriptImplementationException>", e)
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), e, { loadVideo(url) }, null, mainFragment)
return@launch
} catch (e: ScriptAgeException) {
Logger.w(TAG, "exception<ScriptAgeException>", e)
UIDialogs.showDialog(
context, R.drawable.ic_lock, "Age restricted video", e.message, null, 0, UIDialogs.Action("Close", { }, UIDialogs.ActionStyle.PRIMARY)
)
return@launch
} catch (e: ScriptUnavailableException) {
Logger.w(TAG, "exception<ScriptUnavailableException>", e)
if (video?.datetime == null || video?.datetime!! < OffsetDateTime.now()
.minusHours(1)
) {
UIDialogs.showDialog(
context, R.drawable.ic_lock, context.getString(R.string.unavailable_video), context.getString(R.string.this_video_is_unavailable), null, 0, UIDialogs.Action(context.getString(R.string.close), { }, UIDialogs.ActionStyle.PRIMARY)
)
}
video?.let { StatePlatform.instance.clearContentDetailCache(it.url) }
return@launch
} catch (e: ScriptException) {
Logger.w(TAG, "exception<ScriptException>", e)
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), e, { loadVideo(url) }, null, mainFragment)
return@launch
} catch (e: Throwable) {
Logger.w(ChannelFragment.TAG, "Failed to load video.", e)
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), e, { loadVideo(url) }, null, mainFragment)
return@launch
}
if (result !is IPlatformVideoDetails) {
Logger.w(
TAG, "Wrong content type", IllegalStateException("Expected media content, found ${result.contentType}")
)
return@launch
}
// if it's been canceled then don't set the video details
if (!isActive) {
return@launch
}
videoDetails = result
video = result
setLoading(false)
if (playWhenReady) playVideo()
}
}
private fun playVideo(resumePositionMs: Long = 0) {
val video = videoDetails
if (video === null) {
playWhenReady = true
return
}
bottomSheet.show(mainFragment!!.childFragmentManager, ModalBottomSheet.TAG)
try {
val videoSource = _lastVideoSource
?: player.getPreferredVideoSource(video, Settings.instance.playback.getCurrentPreferredQualityPixelCount())
val audioSource = _lastAudioSource
?: player.getPreferredAudioSource(video, Settings.instance.playback.getPrimaryLanguage(context))
val subtitleSource = _lastSubtitleSource
?: (if (video is VideoLocal) video.subtitlesSources.firstOrNull() else null)
Logger.i(TAG, "loadCurrentVideo(videoSource=$videoSource, audioSource=$audioSource, subtitleSource=$subtitleSource, resumePositionMs=$resumePositionMs)")
if (videoSource == null && audioSource == null) {
UIDialogs.showDialog(
context, R.drawable.ic_lock, context.getString(R.string.unavailable_video), context.getString(R.string.this_video_is_unavailable), null, 0, UIDialogs.Action(context.getString(R.string.close), { }, UIDialogs.ActionStyle.PRIMARY)
)
StatePlatform.instance.clearContentDetailCache(video.url)
return
}
val thumbnail = video.thumbnails.getHQThumbnail()
if (videoSource == null && !thumbnail.isNullOrBlank()) Glide.with(context).asBitmap()
.load(thumbnail).into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
player.setArtwork(BitmapDrawable(resources, resource))
}
override fun onLoadCleared(placeholder: Drawable?) {
player.setArtwork(null)
}
})
else player.setArtwork(null)
player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0)
if (subtitleSource != null) player.swapSubtitles(mainFragment!!.lifecycleScope, subtitleSource)
player.seekTo(resumePositionMs)
_lastVideoSource = videoSource
_lastAudioSource = audioSource
_lastSubtitleSource = subtitleSource
} catch (ex: UnsupportedCastException) {
Logger.e(TAG, "Failed to load cast media", ex)
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.unsupported_cast_format), ex)
} catch (ex: Throwable) {
Logger.e(TAG, "Failed to load media", ex)
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_load_media), ex)
}
}
companion object {
const val TAG = "VideoDetailView"
}
class ModalBottomSheet : BottomSheetDialogFragment() {
override fun onCreateDialog(
savedInstanceState: Bundle?,
): Dialog {
val bottomSheetDialog = BottomSheetDialog(
requireContext()
)
bottomSheetDialog.setContentView(R.layout.modal_comments)
val composeView = bottomSheetDialog.findViewById<ComposeView>(R.id.compose_view)
composeView?.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
// In Compose world
MaterialTheme {
val view = LocalView.current
IconButton(onClick = {
view.playSoundEffect(SoundEffectConstants.CLICK)
}) {
Icon(
Icons.Outlined.ThumbUp, contentDescription = "Close Bottom Sheet"
)
}
}
}
}
return bottomSheetDialog
}
companion object {
const val TAG = "ModalBottomSheet"
}
}
}

View file

@ -0,0 +1,96 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.core.view.get
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.futo.platformplayer.R
@UnstableApi
class ShortsFragment : MainFragment() {
override val isMainView: Boolean = true
override val isTab: Boolean = true
override val hasBottomBar: Boolean get() = true
private var previousShownView: ShortView? = null
private lateinit var viewPager: ViewPager2
private lateinit var customViewAdapter: CustomViewAdapter
private val urls = listOf(
"https://youtube.com/shorts/fHU6dfPHT-o?si=TVCYnt_mvAxWYACZ", "https://youtube.com/shorts/j9LQ0c4MyGk?si=FVlr90UD42y1ZIO0", "https://youtube.com/shorts/Q8LndW9YZvQ?si=mDrSsm-3Uq7IEXAT", "https://youtube.com/shorts/OIS5qHDOOzs?si=RGYeaAH9M-TRuZSr", "https://youtube.com/shorts/1Cp6EbLWVnI?si=N4QqytC48CTnfJra", "https://youtube.com/shorts/fHU6dfPHT-o?si=TVCYnt_mvAxWYACZ", "https://youtube.com/shorts/j9LQ0c4MyGk?si=FVlr90UD42y1ZIO0", "https://youtube.com/shorts/Q8LndW9YZvQ?si=mDrSsm-3Uq7IEXAT", "https://youtube.com/shorts/OIS5qHDOOzs?si=RGYeaAH9M-TRuZSr", "https://youtube.com/shorts/1Cp6EbLWVnI?si=N4QqytC48CTnfJra"
)
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.viewPager)
customViewAdapter = CustomViewAdapter(urls, layoutInflater, this)
viewPager.adapter = customViewAdapter
// TODO something is laggy sometimes when swiping between videos
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
@OptIn(UnstableApi::class)
override fun onPageSelected(position: Int) {
previousShownView?.stop()
val focusedView =
((viewPager[0] as RecyclerView).findViewHolderForAdapterPosition(position) as CustomViewHolder).shortView
focusedView.play()
previousShownView = focusedView
}
})
}
override fun onPause() {
super.onPause()
previousShownView?.stop()
}
companion object {
private const val TAG = "ShortsFragment"
fun newInstance() = ShortsFragment()
}
class CustomViewAdapter(
private val urls: List<String>, private val inflater: LayoutInflater, private val fragment: MainFragment
) : RecyclerView.Adapter<CustomViewHolder>() {
@OptIn(UnstableApi::class)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
val shortView = ShortView(inflater, fragment)
return CustomViewHolder(shortView)
}
@OptIn(UnstableApi::class)
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
holder.shortView.setVideo(urls[position])
}
@OptIn(UnstableApi::class)
override fun onViewRecycled(holder: CustomViewHolder) {
super.onViewRecycled(holder)
holder.shortView.detach()
}
override fun getItemCount(): Int = urls.size
}
@OptIn(UnstableApi::class)
class CustomViewHolder(val shortView: ShortView) : RecyclerView.ViewHolder(shortView)
}

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

@ -0,0 +1,179 @@
package com.futo.platformplayer.views.video
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
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.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerView
import androidx.media3.ui.TimeBar
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.states.StatePlayer
@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 val loadArtwork = object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
setArtwork(BitmapDrawable(resources, resource))
}
override fun onLoadCleared(placeholder: Drawable?) {
setArtwork(null)
}
}
private val player = StatePlayer.instance.getShortPlayerOrCreate(context)
private var progressAnimator: ValueAnimator = createProgressBarAnimator()
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
)
) {
if (player.duration >= 0) {
progressAnimator.duration = player.duration
setProgressBarDuration(player.duration)
progressAnimator.currentPlayTime = player.currentPosition
}
if (player.isPlaying) {
if (!progressAnimator.isStarted) {
progressAnimator.start()
}
} else {
if (progressAnimator.isRunning) {
progressAnimator.cancel()
}
}
}
}
}
init {
LayoutInflater.from(context).inflate(R.layout.view_short_player, this, true)
videoView = findViewById(R.id.video_player)
progressBar = findViewById(R.id.video_player_progress_bar)
progressBar.addListener(object : TimeBar.OnScrubListener {
override fun onScrubStart(timeBar: TimeBar, position: Long) {
if (progressAnimator.isRunning) {
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.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 ->
val progress = animation.animatedValue as Float
val duration = animation.duration
progressBar.setPosition((progress * duration).toLong())
}
}
}
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()
}
fun setPreview(video: IPlatformVideoDetails) {
if (video.live != null) {
setSource(video.live, null, play = true, keepSubtitles = false)
} else {
val videoSource =
VideoHelper.selectBestVideoSource(video.video, Settings.instance.playback.getPreferredPreviewQualityPixelCount(), PREFERED_VIDEO_CONTAINERS)
val audioSource =
VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context))
if (videoSource == null && audioSource != null) {
val thumbnail = video.thumbnails.getHQThumbnail()
if (!thumbnail.isNullOrBlank()) {
Glide.with(videoView).asBitmap().load(thumbnail).into(loadArtwork)
} else {
Glide.with(videoView).clear(loadArtwork)
setArtwork(null)
}
} else {
Glide.with(videoView).clear(loadArtwork)
}
setSource(videoSource, audioSource, play = true, keepSubtitles = false)
}
}
@OptIn(UnstableApi::class)
fun setArtwork(drawable: Drawable?) {
if (drawable != null) {
videoView.defaultArtwork = drawable
videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_FILL
} else {
videoView.defaultArtwork = null
videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_OFF
}
}
}

View file

@ -5,6 +5,7 @@ import android.net.Uri
import android.util.AttributeSet
import android.widget.RelativeLayout
import androidx.annotation.OptIn
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.media3.common.C
@ -68,7 +69,7 @@ import java.io.ByteArrayInputStream
import java.io.File
import kotlin.math.abs
abstract class FutoVideoPlayerBase : RelativeLayout {
abstract class FutoVideoPlayerBase : ConstraintLayout {
private val TAG = "FutoVideoPlayerBase"
private val TEMP_DIRECTORY = StateApp.instance.getTempDirectory();

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"
@ -96,4 +97,4 @@
app:layout_constraintRight_toRightOf="@id/fragment_main"
app:layout_constraintBottom_toBottomOf="@id/fragment_main" />
</androidx.constraintlayout.motion.widget.MotionLayout>
</androidx.constraintlayout.motion.widget.MotionLayout>

View file

@ -38,7 +38,7 @@
<LinearLayout
android:id="@+id/bottom_bar_buttons"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_height="48dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.viewpager2.widget.ViewPager2 xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />

View file

@ -0,0 +1,231 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
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" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textview.MaterialTextView
style="@style/Widget.Material3.CheckedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Bottom Sheet Content"
android:textSize="18sp"
android:textStyle="bold" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View file

@ -0,0 +1,43 @@
<?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" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view_test_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="1dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<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>
</merge>

View file

@ -0,0 +1,31 @@
<?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/video_player"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/black"
app:default_artwork="@drawable/placeholder_video_thumbnail"
app:layout_constraintBottom_toTopOf="@id/video_player_progress_bar"
app:layout_constraintTop_toTopOf="parent"
app:resize_mode="fit"
app:show_buffering="when_playing"
app:use_artwork="true"
app:use_controller="false" />
<androidx.media3.ui.DefaultTimeBar
android:id="@+id/video_player_progress_bar"
android:layout_width="match_parent"
android:layout_height="6dp"
app:bar_height="6dp"
app:buffered_color="#DDEEEEEE"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/video_player"
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,147 @@
<color name="overlay">#B3000000</color>
<color name="text_color_tinted">#ACACAC</color>
<color name="pastel_red">#C25353</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

@ -25,6 +25,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>

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">