From 0d5ad90ff98c483ca589a2850d4d678bae0d3660 Mon Sep 17 00:00:00 2001 From: Koen Date: Tue, 19 Dec 2023 11:38:22 +0100 Subject: [PATCH 1/2] 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. --- .../platformplayer/Extensions_Formatting.kt | 6 +- .../platformplayer/activities/MainActivity.kt | 17 +- .../models/sources/JSVideoSourceDescriptor.kt | 4 +- .../casting/ChomecastCastingDevice.kt | 2 - .../casting/FCastCastingDevice.kt | 70 +----- .../dialogs/ConnectCastingDialog.kt | 20 +- .../dialogs/ConnectedCastingDialog.kt | 13 +- .../bottombar/MenuBottomBarFragment.kt | 1 + .../mainactivity/main/HistoryFragment.kt | 6 +- .../mainactivity/main/TutorialFragment.kt | 208 ++++++++++++++++++ .../mainactivity/main/VideoDetailView.kt | 26 ++- .../views/adapters/DeviceAdapter.kt | 3 +- .../views/adapters/DeviceViewHolder.kt | 7 +- .../views/pills/WidePillButton.kt | 57 +++++ .../views/video/FutoVideoPlayer.kt | 7 +- .../main/res/layout/fragview_video_detail.xml | 34 +-- app/src/main/res/layout/list_comment.xml | 3 +- .../main/res/layout/view_wide_pill_button.xml | 47 ++++ app/src/main/res/values/strings.xml | 2 + .../res/values/wide_pill_button_attrs.xml | 8 + 20 files changed, 423 insertions(+), 118 deletions(-) create mode 100644 app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/TutorialFragment.kt create mode 100644 app/src/main/java/com/futo/platformplayer/views/pills/WidePillButton.kt create mode 100644 app/src/main/res/layout/view_wide_pill_button.xml create mode 100644 app/src/main/res/values/wide_pill_button_attrs.xml diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_Formatting.kt b/app/src/main/java/com/futo/platformplayer/Extensions_Formatting.kt index d4617fdc..5776d381 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_Formatting.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_Formatting.kt @@ -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 { diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index 6dfe8472..bd1a24d2 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -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 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; diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt index 131a5794..4b983ef7 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/sources/JSVideoSourceDescriptor.kt @@ -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; diff --git a/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt index 7885da41..eeee0dd3 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt @@ -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") diff --git a/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt index 0003cf4e..a75f0c7c 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt @@ -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() - private var _encryptionStarted = false private var _thread: Thread? = null constructor(name: String, addresses: Array, 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 { @@ -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 diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt index 8f13545c..9291424b 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt @@ -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); diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt index 846d4857..b640bc0e 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt @@ -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; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt index 979a679a..9f160d6c 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt @@ -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() }), ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate() }), ButtonDefinition(9, R.drawable.ic_subscriptions, R.drawable.ic_subscriptions_filled, R.string.subscription_group_menu, canToggle = true, { it.currentMain is SubscriptionGroupListFragment }, { it.navigate() }), + ButtonDefinition(10, R.drawable.ic_quiz, R.drawable.ic_quiz, R.string.tutorials, canToggle = true, { it.currentMain is TutorialFragment }, { it.navigate() }), 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()"); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HistoryFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HistoryFragment.kt index 3a5cdc50..fcbec07c 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HistoryFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HistoryFragment.kt @@ -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") diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/TutorialFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/TutorialFragment.kt new file mode 100644 index 00000000..aab3986e --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/TutorialFragment.kt @@ -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(it) + } + }) + } + + addView(createHeader("Features")) + featuresVideos.forEach { + addView(createTutorialPill(R.drawable.ic_movie, it.name).apply { + onClick.subscribe { + fragment.navigate(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 = arrayOf( + VideoUrlSource("1080p", url, 1920, 1080, duration, "video/mp4") + ) + override val audioSources: Array = 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 = 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 { + 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 + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 30c827ec..8cdaeab4 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -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(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(); diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceAdapter.kt index e318c3a7..711a1675 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceAdapter.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceAdapter.kt @@ -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 { @@ -13,6 +12,7 @@ class DeviceAdapter : RecyclerView.Adapter { private val _isRememberedDevice: Boolean; var onRemove = Event1(); + var onConnect = Event1(); constructor(devices: ArrayList, isRememberedDevice: Boolean) : super() { _devices = devices; @@ -26,6 +26,7 @@ class DeviceAdapter : RecyclerView.Adapter { val holder = DeviceViewHolder(view); holder.setIsRememberedDevice(_isRememberedDevice); holder.onRemove.subscribe { d -> onRemove.emit(d); }; + holder.onConnect.subscribe { d -> onConnect.emit(d); } return holder; } diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceViewHolder.kt index 3c17617d..3fa42219 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceViewHolder.kt @@ -31,6 +31,7 @@ class DeviceViewHolder : ViewHolder { private set var onRemove = Event1(); + val onConnect = Event1(); 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); } diff --git a/app/src/main/java/com/futo/platformplayer/views/pills/WidePillButton.kt b/app/src/main/java/com/futo/platformplayer/views/pills/WidePillButton.kt new file mode 100644 index 00000000..3be38af6 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/pills/WidePillButton.kt @@ -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(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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt index 154db811..6af7e0b4 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt @@ -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) { diff --git a/app/src/main/res/layout/fragview_video_detail.xml b/app/src/main/res/layout/fragview_video_detail.xml index 7652198e..38aa534d 100644 --- a/app/src/main/res/layout/fragview_video_detail.xml +++ b/app/src/main/res/layout/fragview_video_detail.xml @@ -488,22 +488,28 @@ android:layout_weight="1" android:layout_height="match_parent" /> - + android:layout_height="wrap_content"> - + + + + + android:paddingEnd="16dp" + android:layout_marginStart="12dp"> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9b50c552..15658694 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -722,6 +722,8 @@ Polycentric is disabled Play Pause Position + Tutorials + Do you want to see the tutorials? You can find them at any time through the more button. Recommendations Subscriptions diff --git a/app/src/main/res/values/wide_pill_button_attrs.xml b/app/src/main/res/values/wide_pill_button_attrs.xml new file mode 100644 index 00000000..785a4593 --- /dev/null +++ b/app/src/main/res/values/wide_pill_button_attrs.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file From 65174ffc9727d8e28a86bd8066cfeef512bc4cba Mon Sep 17 00:00:00 2001 From: Koen Date: Tue, 19 Dec 2023 11:46:40 +0100 Subject: [PATCH 2/2] Show connected controls as disabled when still connecting. --- .../dialogs/ConnectedCastingDialog.kt | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt index b640bc0e..300b0a66 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt @@ -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(); @@ -194,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) { + views.forEach { enableControl(it) } + } + + private fun enableControl(view: View) { + view.alpha = 1.0f + view.isEnabled = true + } + + private fun disableControls(views: List) { + views.forEach { disableControl(it) } + } + + private fun disableControl(view: View) { + view.alpha = 0.4f + view.isEnabled = false } private fun setLoading(isLoading: Boolean) {