mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +00:00
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:
parent
ef72561768
commit
0d5ad90ff9
20 changed files with 423 additions and 118 deletions
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()");
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
47
app/src/main/res/layout/view_wide_pill_button.xml
Normal file
47
app/src/main/res/layout/view_wide_pill_button.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
8
app/src/main/res/values/wide_pill_button_attrs.xml
Normal file
8
app/src/main/res/values/wide_pill_button_attrs.xml
Normal 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>
|
Loading…
Add table
Reference in a new issue