Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay

This commit is contained in:
Kelvin 2023-12-19 23:08:48 +01:00
commit a4422fdd56
20 changed files with 462 additions and 118 deletions

View file

@ -232,7 +232,11 @@ fun Long.formatDuration(): String {
val minutes = (this % 3600000) / 60000
val seconds = (this % 60000) / 1000
return String.format("%02d:%02d:%02d", hours, minutes, seconds)
return if (hours > 0) {
String.format("%02d:%02d:%02d", hours, minutes, seconds)
} else {
String.format("%02d:%02d", minutes, seconds)
}
}
fun String.fixHtmlLinks(): Spanned {

View file

@ -40,7 +40,6 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFrag
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
import com.futo.platformplayer.listeners.OrientationManager
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.states.*
import com.futo.platformplayer.stores.FragmentedStorage
@ -96,6 +95,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragMainSubscriptionsFeed: SubscriptionsFeedFragment;
lateinit var _fragMainChannel: ChannelFragment;
lateinit var _fragMainSources: SourcesFragment;
lateinit var _fragMainTutorial: TutorialFragment;
lateinit var _fragMainPlaylists: PlaylistsFragment;
lateinit var _fragMainPlaylist: PlaylistFragment;
lateinit var _fragWatchlist: WatchLaterFragment;
@ -223,6 +223,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
//Main
_fragMainHome = HomeFragment.newInstance();
_fragMainTutorial = TutorialFragment.newInstance()
_fragMainSuggestions = SuggestionsFragment.newInstance();
_fragMainVideoSearchResults = ContentSearchResultsFragment.newInstance();
_fragMainCreatorSearchResults = CreatorSearchResultsFragment.newInstance();
@ -314,6 +315,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainCreatorSearchResults.topBar = _fragTopBarSearch;
_fragMainPlaylistSearchResults.topBar = _fragTopBarSearch;
_fragMainChannel.topBar = _fragTopBarNavigation;
_fragMainTutorial.topBar = _fragTopBarNavigation;
_fragMainSubscriptionsFeed.topBar = _fragTopBarGeneral;
_fragMainSources.topBar = _fragTopBarAdd;
_fragMainPlaylists.topBar = _fragTopBarGeneral;
@ -328,7 +330,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragSubGroupList.topBar = _fragTopBarAdd;
_fragBrowser.topBar = _fragTopBarNavigation;
fragCurrent = _fragMainHome;
val defaultTab = Settings.instance.tabs.mapNotNull {
@ -410,6 +412,16 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
StateApp.instance.mainAppStartedWithExternalFiles(this);
//startActivity(Intent(this, TestActivity::class.java));
val sharedPreferences = getSharedPreferences("GrayjayFirstBoot", Context.MODE_PRIVATE)
val isFirstBoot = sharedPreferences.getBoolean("IsFirstBoot", true)
if (isFirstBoot) {
UIDialogs.showConfirmationDialog(this, getString(R.string.do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button), {
navigate(_fragMainTutorial)
})
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
}
}
@ -968,6 +980,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
inline fun <reified T : Fragment> getFragment() : T {
return when(T::class) {
HomeFragment::class -> _fragMainHome as T;
TutorialFragment::class -> _fragMainTutorial as T;
ContentSearchResultsFragment::class -> _fragMainVideoSearchResults as T;
CreatorSearchResultsFragment::class -> _fragMainCreatorSearchResults as T;
SuggestionsFragment::class -> _fragMainSuggestions as T;

View file

@ -6,11 +6,9 @@ import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow
class JSVideoSourceDescriptor: VideoMuxedSourceDescriptor {
class JSVideoSourceDescriptor : VideoMuxedSourceDescriptor {
protected val _obj: V8ValueObject;
override val isUnMuxed: Boolean;

View file

@ -420,7 +420,6 @@ class ChromecastCastingDevice : CastingDevice {
Logger.i(TAG, "Stopped connection loop.");
connectionState = CastConnectionState.DISCONNECTED;
_thread = null;
}.apply { start() };
//Start ping loop
@ -440,7 +439,6 @@ class ChromecastCastingDevice : CastingDevice {
}
Logger.i(TAG, "Stopped ping loop.");
_pingThread = null;
}.apply { start() };
} else {
Log.i(TAG, "Threads still alive, not restarted")

View file

@ -58,11 +58,8 @@ enum class Opcode(val value: Byte) {
PlaybackError(9),
SetSpeed(10),
Version(11),
KeyExchange(12),
Encrypted(13),
Ping(14),
Pong(15),
StartEncryption(16);
Ping(12),
Pong(13);
companion object {
private val _map = entries.associateBy { it.value }
@ -89,26 +86,18 @@ class FCastCastingDevice : CastingDevice {
private var _scopeIO: CoroutineScope? = null;
private var _started: Boolean = false;
private var _version: Long = 1;
private val _keyPair: KeyPair
private var _aesKey: SecretKeySpec? = null
private val _queuedEncryptedMessages = arrayListOf<FCastEncryptedMessage>()
private var _encryptionStarted = false
private var _thread: Thread? = null
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
this.name = name;
this.addresses = addresses;
this.port = port;
_keyPair = generateKeyPair()
}
constructor(deviceInfo: CastingDeviceInfo) : super() {
this.name = deviceInfo.name;
this.addresses = deviceInfo.addresses.map { a -> a.toInetAddress() }.filterNotNull().toTypedArray();
this.port = deviceInfo.port;
_keyPair = generateKeyPair()
}
override fun getAddresses(): List<InetAddress> {
@ -301,9 +290,6 @@ class FCastCastingDevice : CastingDevice {
localAddress = _socket?.localAddress;
connectionState = CastConnectionState.CONNECTED;
Logger.i(TAG, "Sending KeyExchange.")
send(Opcode.KeyExchange, getKeyExchangeMessage(_keyPair))
val buffer = ByteArray(4096);
Logger.i(TAG, "Started receiving.");
@ -362,7 +348,6 @@ class FCastCastingDevice : CastingDevice {
Logger.i(TAG, "Stopped connection loop.");
connectionState = CastConnectionState.DISCONNECTED;
_thread = null;
}.apply { start() };
} else {
Log.i(TAG, "Thread was still alive, not restarted")
@ -415,63 +400,12 @@ class FCastCastingDevice : CastingDevice {
_version = version.version;
Logger.i(TAG, "Remote version received: $version")
}
Opcode.KeyExchange -> {
if (json == null) {
Logger.w(TAG, "Got KeyExchange without JSON, ignoring.");
return;
}
val keyExchangeMessage: FCastKeyExchangeMessage = FCastCastingDevice.json.decodeFromString(json)
Logger.i(TAG, "Received public key: ${keyExchangeMessage.publicKey}")
_aesKey = computeSharedSecret(_keyPair.private, keyExchangeMessage)
synchronized(_queuedEncryptedMessages) {
for (queuedEncryptedMessages in _queuedEncryptedMessages) {
val decryptedMessage = decryptMessage(_aesKey!!, queuedEncryptedMessages)
val o = Opcode.find(decryptedMessage.opcode.toByte())
handleMessage(o, decryptedMessage.message)
}
_queuedEncryptedMessages.clear()
}
}
Opcode.Ping -> send(Opcode.Pong)
Opcode.Encrypted -> {
if (json == null) {
Logger.w(TAG, "Got Encrypted without JSON, ignoring.");
return;
}
val encryptedMessage: FCastEncryptedMessage = FCastCastingDevice.json.decodeFromString(json)
if (_aesKey != null) {
val decryptedMessage = decryptMessage(_aesKey!!, encryptedMessage)
val o = Opcode.find(decryptedMessage.opcode.toByte())
handleMessage(o, decryptedMessage.message)
} else {
synchronized(_queuedEncryptedMessages) {
if (_queuedEncryptedMessages.size == 15) {
_queuedEncryptedMessages.removeAt(0)
}
_queuedEncryptedMessages.add(encryptedMessage)
}
}
}
Opcode.StartEncryption -> {
_encryptionStarted = true
//TODO: Send decrypted messages waiting for encryption to be established
}
else -> { }
}
}
private fun send(opcode: Opcode, message: String? = null) {
val aesKey = _aesKey
if (_encryptionStarted && aesKey != null && opcode != Opcode.Encrypted && opcode != Opcode.KeyExchange && opcode != Opcode.StartEncryption) {
send(Opcode.Encrypted, encryptMessage(aesKey, FCastDecryptedMessage(opcode.value.toLong(), message)))
return
}
try {
val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
val size = 1 + data.size

View file

@ -1,37 +1,27 @@
package com.futo.platformplayer.dialogs
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Animatable
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.AddSourceActivity
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.QRCaptureActivity
import com.futo.platformplayer.casting.CastConnectionState
import com.futo.platformplayer.casting.CastingDevice
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.adapters.DeviceAdapter
import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.UUID
class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
private lateinit var _imageLoader: ImageView;
@ -80,6 +70,14 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
_textNoDevicesRemembered.visibility = if (_rememberedDevices.isEmpty()) View.VISIBLE else View.GONE;
_recyclerRememberedDevices.visibility = if (_rememberedDevices.isNotEmpty()) View.VISIBLE else View.GONE;
};
_rememberedAdapter.onConnect.subscribe { _ ->
dismiss()
UIDialogs.showCastingDialog(context)
}
_adapter.onConnect.subscribe { _ ->
dismiss()
UIDialogs.showCastingDialog(context)
}
_recyclerRememberedDevices.adapter = _rememberedAdapter;
_recyclerRememberedDevices.layoutManager = LinearLayoutManager(context);

View file

@ -133,17 +133,17 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
StateCasting.instance.onActiveDeviceVolumeChanged.remove(this);
StateCasting.instance.onActiveDeviceVolumeChanged.subscribe {
_sliderVolume.value = it.toFloat();
_sliderVolume.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
};
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
StateCasting.instance.onActiveDeviceTimeChanged.subscribe {
_sliderPosition.value = it.toFloat();
_sliderPosition.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderPosition.valueTo);
};
StateCasting.instance.onActiveDeviceDurationChanged.remove(this);
StateCasting.instance.onActiveDeviceDurationChanged.subscribe {
_sliderPosition.valueTo = it.toFloat();
_sliderPosition.valueTo = it.toFloat().coerceAtLeast(1.0f);
};
_device = StateCasting.instance.activeDevice;
@ -152,6 +152,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
setLoading(!isConnected);
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { setLoading(connectionState != CastConnectionState.CONNECTED); };
updateDevice();
};
updateDevice();
@ -181,10 +182,11 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
}
_textName.text = d.name;
_sliderVolume.value = d.volume.toFloat();
_sliderPosition.valueFrom = 0.0f;
_sliderPosition.valueTo = d.duration.toFloat();
_sliderPosition.value = d.time.toFloat();
_sliderVolume.valueFrom = 0.0f;
_sliderVolume.value = d.volume.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
_sliderPosition.valueTo = d.duration.toFloat().coerceAtLeast(1.0f);
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
if (d.canSetVolume) {
_layoutVolumeAdjustable.visibility = View.VISIBLE;
@ -193,6 +195,44 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
_layoutVolumeAdjustable.visibility = View.GONE;
_layoutVolumeFixed.visibility = View.VISIBLE;
}
val interactiveControls = listOf(
_sliderPosition,
_sliderVolume,
_buttonPrevious,
_buttonPlay,
_buttonPause,
_buttonStop,
_buttonNext
)
when (d.connectionState) {
CastConnectionState.CONNECTED -> {
enableControls(interactiveControls)
}
CastConnectionState.CONNECTING,
CastConnectionState.DISCONNECTED -> {
disableControls(interactiveControls)
}
}
}
private fun enableControls(views: List<View>) {
views.forEach { enableControl(it) }
}
private fun enableControl(view: View) {
view.alpha = 1.0f
view.isEnabled = true
}
private fun disableControls(views: List<View>) {
views.forEach { disableControl(it) }
}
private fun disableControl(view: View) {
view.alpha = 0.4f
view.isEnabled = false
}
private fun setLoading(isLoading: Boolean) {

View file

@ -349,6 +349,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
ButtonDefinition(6, 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_quiz, R.drawable.ic_quiz, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate<TutorialFragment>() }),
ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings, R.string.settings, canToggle = false, { false }, {
val c = it.context ?: return@ButtonDefinition;
Logger.i(TAG, "settings preventPictureInPicture()");

View file

@ -15,18 +15,17 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.structures.IAsyncPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.PlatformContentPager
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.others.TagsView
import com.futo.platformplayer.views.adapters.HistoryListViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.others.TagsView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -52,6 +51,7 @@ class HistoryFragment : MainFragment() {
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack)
_view?.setPager(StateHistory.instance.getHistoryPager());
(topBar as NavigationTopBarFragment?)?.onShown("History");
}
@SuppressLint("ViewConstructor")

View file

@ -0,0 +1,208 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.Thumbnail
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
import com.futo.platformplayer.views.pills.WidePillButton
import java.time.OffsetDateTime
class TutorialFragment : MainFragment() {
override val isMainView : Boolean = true;
override val isTab: Boolean = true;
override val hasBottomBar: Boolean get() = true;
private var _view: TutorialView? = null;
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
(topBar as NavigationTopBarFragment?)?.onShown(getString(R.string.tutorials));
}
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = TutorialView(this, inflater);
_view = view;
return view;
}
override fun onDestroyMainView() {
super.onDestroyMainView();
_view = null;
}
@SuppressLint("ViewConstructor")
class TutorialView : LinearLayout {
val fragment: TutorialFragment
constructor(fragment: TutorialFragment, inflater: LayoutInflater) : super(inflater.context) {
this.fragment = fragment
orientation = VERTICAL
addView(createHeader("Initial setup"))
initialSetupVideos.forEach {
addView(createTutorialPill(R.drawable.ic_movie, it.name).apply {
onClick.subscribe {
fragment.navigate<VideoDetailFragment>(it)
}
})
}
addView(createHeader("Features"))
featuresVideos.forEach {
addView(createTutorialPill(R.drawable.ic_movie, it.name).apply {
onClick.subscribe {
fragment.navigate<VideoDetailFragment>(it)
}
})
}
}
private fun createHeader(t: String): TextView {
return TextView(context).apply {
textSize = 24.0f
typeface = resources.getFont(R.font.inter_regular)
text = t
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
setMargins(15.dp(resources), 10.dp(resources), 15.dp(resources), 12.dp(resources))
}
}
}
private fun createTutorialPill(iconPrefix: Int, t: String): WidePillButton {
return WidePillButton(context).apply {
setIconPrefix(iconPrefix)
setText(t)
setIconSuffix(R.drawable.ic_play_notif)
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
setMargins(15.dp(resources), 0, 15.dp(resources), 12.dp(resources))
}
}
}
}
class TutorialVideoSourceDescriptor(url: String, duration: Long) : VideoUnMuxedSourceDescriptor() {
override val videoSources: Array<IVideoSource> = arrayOf(
VideoUrlSource("1080p", url, 1920, 1080, duration, "video/mp4")
)
override val audioSources: Array<IAudioSource> = arrayOf()
}
class TutorialVideo(
uuid: String,
override val name: String,
override val description: String,
thumbnailUrl: String,
videoUrl: String,
override val duration: Long
) : IPlatformVideoDetails {
override val id: PlatformID = PlatformID("tutorial", uuid)
override val contentType: ContentType = ContentType.MEDIA
override val preview: IVideoSourceDescriptor? = null
override val live: IVideoSource? = null
override val dash: IDashManifestSource? = null
override val hls: IHLSManifestSource? = null
override val subtitles: List<ISubtitleSource> = emptyList()
override val shareUrl: String = ""
override val url: String = ""
override val datetime: OffsetDateTime? = OffsetDateTime.parse("2023-12-18T00:00:00Z")
override val thumbnails: Thumbnails = Thumbnails(arrayOf(Thumbnail(thumbnailUrl)))
override val author: PlatformAuthorLink = PlatformAuthorLink(PlatformID("tutorial", "f422ced6-b551-4b62-818e-27a4f5f4918a"), "Grayjay", "", "https://releases.grayjay.app/tutorials/author.jpeg")
override val isLive: Boolean = false
override val rating: IRating = RatingLikes(-1)
override val viewCount: Long = -1
override val video: IVideoSourceDescriptor = TutorialVideoSourceDescriptor(videoUrl, duration)
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
return EmptyPager()
}
override fun getPlaybackTracker(): IPlaybackTracker? {
return null
}
}
companion object {
val TAG = "HomeFragment";
fun newInstance() = TutorialFragment().apply {}
val initialSetupVideos = listOf(
TutorialVideo(
uuid = "228be579-ec52-4d93-b9eb-ca74ec08c58a",
name = "How to install",
description = "Learn how to install Grayjay.",
thumbnailUrl = "https://releases.grayjay.app/tutorials/how-to-install.jpg",
videoUrl = "https://releases.grayjay.app/tutorials/how-to-install.mp4",
duration = 52
),
TutorialVideo(
uuid = "3b99ebfe-2640-4643-bfe0-a0cf04261fc5",
name = "Getting started",
description = "Learn how to get started with Grayjay.",
thumbnailUrl = "https://releases.grayjay.app/tutorials/getting-started.jpg",
videoUrl = "https://releases.grayjay.app/tutorials/getting-started.mp4",
duration = 50
),
TutorialVideo(
uuid = "793aa009-516c-4581-b82f-a8efdfef4c27",
name = "Is Grayjay free?",
description = "Learn how Grayjay is monetized.",
thumbnailUrl = "https://releases.grayjay.app/tutorials/pay.jpg",
videoUrl = "https://releases.grayjay.app/tutorials/pay.mp4",
duration = 52
)
)
val featuresVideos = listOf(
TutorialVideo(
uuid = "d2238d88-4252-4a91-a12d-b90c049bb7cf",
name = "Searching",
description = "Learn about searching in Grayjay.",
thumbnailUrl = "https://releases.grayjay.app/tutorials/search.jpg",
videoUrl = "https://releases.grayjay.app/tutorials/search.mp4",
duration = 39
),
TutorialVideo(
uuid = "d2238d88-4252-4a91-a12d-b90c049bb7cf",
name = "Comments",
description = "Learn about Polycentric comments in Grayjay.",
thumbnailUrl = "https://releases.grayjay.app/tutorials/polycentric.jpg",
videoUrl = "https://releases.grayjay.app/tutorials/polycentric.mp4",
duration = 64
),
TutorialVideo(
uuid = "94d36959-e3fc-4c24-a988-89147067a179",
name = "Casting",
description = "Learn about casting in Grayjay.",
thumbnailUrl = "https://releases.grayjay.app/tutorials/how-to-cast.jpg",
videoUrl = "https://releases.grayjay.app/tutorials/how-to-cast.mp4",
duration = 79
)
)
}
}

View file

@ -252,6 +252,7 @@ class VideoDetailView : ConstraintLayout {
private val _layoutRating: LinearLayout;
private val _imageDislikeIcon: ImageView;
private val _imageLikeIcon: ImageView;
private val _layoutToggleCommentSection: LinearLayout;
private val _monetization: MonetizationView;
@ -328,6 +329,7 @@ class VideoDetailView : ConstraintLayout {
_upNext = findViewById(R.id.up_next);
_textCommentType = findViewById(R.id.text_comment_type);
_toggleCommentType = findViewById(R.id.toggle_comment_type);
_layoutToggleCommentSection = findViewById(R.id.layout_toggle_comment_section);
_overlayContainer = findViewById(R.id.overlay_container);
_overlay_quality_container = findViewById(R.id.videodetail_quality_overview);
@ -434,7 +436,7 @@ class VideoDetailView : ConstraintLayout {
_buttonPins.alwaysShowLastButton = true;
var buttonMore: RoundButton? = null;
buttonMore = RoundButton(context, R.drawable.ic_menu, "More", TAG_MORE) {
buttonMore = RoundButton(context, R.drawable.ic_menu, context.getString(R.string.more), TAG_MORE) {
_slideUpOverlay = UISlideOverlays.showMoreButtonOverlay(_overlayContainer, _buttonPins, listOf(TAG_MORE)) {selected ->
_buttonPins.setButtons(*(selected + listOf(buttonMore!!)).toTypedArray());
_buttonPinStore.set(*selected.filter { it.tagRef is String }.map{ it.tagRef as String }.toTypedArray())
@ -444,7 +446,6 @@ class VideoDetailView : ConstraintLayout {
_buttonMore = buttonMore;
updateMoreButtons();
_channelButton.setOnClickListener {
(video?.author ?: _searchVideo?.author)?.let {
fragment.navigate<ChannelFragment>(it);
@ -1205,7 +1206,12 @@ class VideoDetailView : ConstraintLayout {
_player.setMetadata(video.name, video.author.name);
_toggleCommentType.setValue(!Settings.instance.other.polycentricEnabled || Settings.instance.comments.defaultCommentSection == 1, false);
if (video !is TutorialFragment.TutorialVideo) {
_toggleCommentType.setValue(false, false);
} else {
_toggleCommentType.setValue(!Settings.instance.other.polycentricEnabled || Settings.instance.comments.defaultCommentSection == 1, false);
}
updateCommentType(true);
//UI
@ -1392,6 +1398,20 @@ class VideoDetailView : ConstraintLayout {
_player.updateNextPrevious();
updateMoreButtons();
if (videoDetail is TutorialFragment.TutorialVideo) {
_buttonSubscribe.visibility = View.GONE
_buttonMore.visibility = View.GONE
_buttonPins.visibility = View.GONE
_layoutRating.visibility = View.GONE
_layoutToggleCommentSection.visibility = View.GONE
} else {
_buttonSubscribe.visibility = View.VISIBLE
_buttonMore.visibility = View.VISIBLE
_buttonPins.visibility = View.VISIBLE
_layoutRating.visibility = View.VISIBLE
_layoutToggleCommentSection.visibility = View.VISIBLE
}
}
fun loadLiveChat(video: IPlatformVideoDetails) {
_liveChat?.stop();

View file

@ -5,7 +5,6 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.casting.CastingDevice
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event1
class DeviceAdapter : RecyclerView.Adapter<DeviceViewHolder> {
@ -13,6 +12,7 @@ class DeviceAdapter : RecyclerView.Adapter<DeviceViewHolder> {
private val _isRememberedDevice: Boolean;
var onRemove = Event1<CastingDevice>();
var onConnect = Event1<CastingDevice>();
constructor(devices: ArrayList<CastingDevice>, isRememberedDevice: Boolean) : super() {
_devices = devices;
@ -26,6 +26,7 @@ class DeviceAdapter : RecyclerView.Adapter<DeviceViewHolder> {
val holder = DeviceViewHolder(view);
holder.setIsRememberedDevice(_isRememberedDevice);
holder.onRemove.subscribe { d -> onRemove.emit(d); };
holder.onConnect.subscribe { d -> onConnect.emit(d); }
return holder;
}

View file

@ -31,6 +31,7 @@ class DeviceViewHolder : ViewHolder {
private set
var onRemove = Event1<CastingDevice>();
val onConnect = Event1<CastingDevice>();
constructor(view: View) : super(view) {
_imageDevice = view.findViewById(R.id.image_device);
@ -56,7 +57,7 @@ class DeviceViewHolder : ViewHolder {
val dev = device ?: return@setOnClickListener;
StateCasting.instance.activeDevice?.stopCasting();
StateCasting.instance.connectDevice(dev);
updateButton();
onConnect.emit(dev);
};
_buttonRemove.setOnClickListener {
@ -64,6 +65,10 @@ class DeviceViewHolder : ViewHolder {
onRemove.emit(dev);
};
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, _ ->
updateButton();
}
setIsRememberedDevice(false);
}

View file

@ -0,0 +1,57 @@
package com.futo.platformplayer.views.pills
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0
class WidePillButton : LinearLayout {
private val _iconPrefix: ImageView
private val _iconSuffix: ImageView
private val _text: TextView
val onClick = Event0()
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
LayoutInflater.from(context).inflate(R.layout.view_wide_pill_button, this, true)
_iconPrefix = findViewById(R.id.image_prefix)
_iconSuffix = findViewById(R.id.image_suffix)
_text = findViewById(R.id.text)
val attrArr = context.obtainStyledAttributes(attrs, R.styleable.WidePillButton, 0, 0)
setIconPrefix(attrArr.getResourceId(R.styleable.WidePillButton_widePillIconPrefix, -1))
setIconSuffix(attrArr.getResourceId(R.styleable.WidePillButton_widePillIconSuffix, -1))
setText(attrArr.getText(R.styleable.PillButton_pillText) ?: "")
attrArr.recycle()
findViewById<LinearLayout>(R.id.root).setOnClickListener {
onClick.emit()
}
}
fun setIconPrefix(drawable: Int) {
if (drawable != -1) {
_iconPrefix.setImageResource(drawable)
_iconPrefix.visibility = View.VISIBLE
} else {
_iconPrefix.visibility = View.GONE
}
}
fun setIconSuffix(drawable: Int) {
if (drawable != -1) {
_iconSuffix.setImageResource(drawable)
_iconSuffix.visibility = View.VISIBLE
} else {
_iconSuffix.visibility = View.GONE
}
}
fun setText(t: CharSequence) {
_text.text = t
}
}

View file

@ -357,7 +357,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
UIDialogs.showCastingDialog(context);
};
videoControls.setProgressUpdateListener { position, bufferedPosition ->
val progressUpdateListener = { position: Long, bufferedPosition: Long ->
val currentTime = position.formatDuration()
val currentDuration = duration.formatDuration()
_control_time.text = currentTime;
@ -380,7 +380,10 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
updateChaptersLoop(++_currentChapterLoopId);
}
}
}
};
_videoControls_fullscreen.setProgressUpdateListener(progressUpdateListener);
videoControls.setProgressUpdateListener(progressUpdateListener);
StatePlayer.instance.onQueueChanged.subscribe(this) {
CoroutineScope(Dispatchers.Main).launch(Dispatchers.Main) {

View file

@ -488,22 +488,28 @@
android:layout_weight="1"
android:layout_height="match_parent" />
<TextView
android:id="@+id/text_comment_type"
<LinearLayout
android:id="@+id/layout_toggle_comment_section"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_extra_light"
android:textSize="14dp"
android:textColor="@color/white"
android:text="@string/polycentric"
android:layout_marginEnd="8dp" />
android:layout_height="wrap_content">
<com.futo.platformplayer.views.others.Toggle
android:id="@+id/toggle_comment_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:toggleEnabled="false"
android:layout_marginEnd="14dp" />
<TextView
android:id="@+id/text_comment_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_extra_light"
android:textSize="14dp"
android:textColor="@color/white"
android:text="@string/polycentric"
android:layout_marginEnd="8dp" />
<com.futo.platformplayer.views.others.Toggle
android:id="@+id/toggle_comment_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:toggleEnabled="false"
android:layout_marginEnd="14dp" />
</LinearLayout>
</LinearLayout>
<com.futo.platformplayer.views.comments.AddCommentView

View file

@ -150,7 +150,8 @@
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingStart="16dp"
android:paddingEnd="16dp">
android:paddingEnd="16dp"
android:layout_marginStart="12dp">
<TextView
android:id="@+id/pill_text"
android:layout_width="wrap_content"

View file

@ -0,0 +1,47 @@
<?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:layout_width="match_parent"
android:layout_height="50dp"
android:paddingTop="6dp"
android:paddingBottom="7dp"
android:paddingStart="7dp"
android:paddingEnd="12dp"
android:background="@drawable/background_pill"
android:id="@+id/root"
android:gravity="center_vertical">
<ImageView
android:id="@+id/image_prefix"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginRight="12dp"
android:layout_marginLeft="12dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_thumb_up" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="@color/white"
android:textSize="16sp"
android:gravity="center_vertical"
android:fontFamily="@font/inter_light"
tools:text="500K" />
<Space android:layout_height="match_parent"
android:layout_width="0dp"
android:layout_weight="1" />
<ImageView
android:id="@+id/image_suffix"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginRight="12dp"
android:layout_marginLeft="12dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_thumb_up" />
</LinearLayout>

View file

@ -722,6 +722,8 @@
<string name="polycentric_is_disabled">Polycentric is disabled</string>
<string name="play_pause">Play Pause</string>
<string name="position">Position</string>
<string name="tutorials">Tutorials</string>
<string name="do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button">Do you want to see the tutorials? You can find them at any time through the more button.</string>
<string-array name="home_screen_array">
<item>Recommendations</item>
<item>Subscriptions</item>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="WidePillButton">
<attr name="widePillIconPrefix" format="reference" />
<attr name="widePilllText" format="string" />
<attr name="widePillIconSuffix" format="reference" />
</declare-styleable>
</resources>