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 2e1f2109..294c811f 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -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 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..300b0a66 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; @@ -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) { + 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) { 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