Fixed duration format. Added tutorial fragment. Added dialog that asks you if you want to see the tutorials on the first app boot. Made casting when clicking start now open connected dialog. Fixed crashes related to sliders in casting connected control. Fixed history tab title. Removed old FCast encryption.

This commit is contained in:
Koen 2023-12-19 11:38:22 +01:00
parent ef72561768
commit 0d5ad90ff9
20 changed files with 423 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

@ -36,7 +36,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
@ -92,6 +91,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;
@ -219,6 +219,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
//Main
_fragMainHome = HomeFragment.newInstance();
_fragMainTutorial = TutorialFragment.newInstance()
_fragMainSuggestions = SuggestionsFragment.newInstance();
_fragMainVideoSearchResults = ContentSearchResultsFragment.newInstance();
_fragMainCreatorSearchResults = CreatorSearchResultsFragment.newInstance();
@ -310,6 +311,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;
@ -325,7 +327,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragSubGroupList.topBar = _fragTopBarAdd;
_fragBrowser.topBar = _fragTopBarNavigation;
fragCurrent = _fragMainHome;
val defaultTab = Settings.instance.tabs.mapNotNull {
@ -407,6 +409,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()
}
}
@ -965,6 +977,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;
@ -181,10 +181,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;

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>