mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +00:00
Merge
This commit is contained in:
commit
3b253ad2b6
101 changed files with 3767 additions and 956 deletions
|
@ -61,6 +61,14 @@
|
|||
|
||||
<data android:scheme="grayjay" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="fcast" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
@ -210,5 +218,9 @@
|
|||
android:name=".activities.QRCaptureActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.FCastGuideActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
</application>
|
||||
</manifest>
|
|
@ -22,6 +22,7 @@ import com.futo.platformplayer.views.fields.FormField
|
|||
import com.futo.platformplayer.views.fields.FieldForm
|
||||
import com.futo.platformplayer.views.fields.FormFieldButton
|
||||
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -29,7 +30,6 @@ import kotlinx.serialization.*
|
|||
import kotlinx.serialization.json.*
|
||||
import java.io.File
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.Locale
|
||||
|
||||
@Serializable
|
||||
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);
|
||||
|
@ -44,19 +44,23 @@ class Settings : FragmentedStorageFileJson() {
|
|||
@Transient
|
||||
val onTabsChanged = Event0();
|
||||
|
||||
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -5)
|
||||
@FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -6)
|
||||
@FormFieldButton(R.drawable.ic_person)
|
||||
fun managePolycentricIdentity() {
|
||||
SettingsActivity.getActivity()?.let {
|
||||
if (StatePolycentric.instance.processHandle != null) {
|
||||
it.startActivity(Intent(it, PolycentricProfileActivity::class.java));
|
||||
if (StatePolycentric.instance.enabled) {
|
||||
if (StatePolycentric.instance.processHandle != null) {
|
||||
it.startActivity(Intent(it, PolycentricProfileActivity::class.java));
|
||||
} else {
|
||||
it.startActivity(Intent(it, PolycentricHomeActivity::class.java));
|
||||
}
|
||||
} else {
|
||||
it.startActivity(Intent(it, PolycentricHomeActivity::class.java));
|
||||
UIDialogs.toast(it, "Polycentric is disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -4)
|
||||
@FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -5)
|
||||
@FormFieldButton(R.drawable.ic_quiz)
|
||||
fun openFAQ() {
|
||||
try {
|
||||
|
@ -66,7 +70,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||
//Ignored
|
||||
}
|
||||
}
|
||||
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -3)
|
||||
@FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -4)
|
||||
@FormFieldButton(R.drawable.ic_data_alert)
|
||||
fun openIssues() {
|
||||
try {
|
||||
|
@ -98,7 +102,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||
}
|
||||
}*/
|
||||
|
||||
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -2)
|
||||
@FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -3)
|
||||
@FormFieldButton(R.drawable.ic_tabs)
|
||||
fun manageTabs() {
|
||||
try {
|
||||
|
@ -112,6 +116,25 @@ class Settings : FragmentedStorageFileJson() {
|
|||
|
||||
|
||||
|
||||
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -2)
|
||||
@FormFieldButton(R.drawable.ic_move_up)
|
||||
fun import() {
|
||||
val act = SettingsActivity.getActivity() ?: return;
|
||||
val intent = MainActivity.getImportOptionsIntent(act);
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK;
|
||||
act.startActivity(intent);
|
||||
}
|
||||
|
||||
@FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -1)
|
||||
@FormFieldButton(R.drawable.ic_link)
|
||||
fun manageLinks() {
|
||||
try {
|
||||
SettingsActivity.getActivity()?.let { UIDialogs.showUrlHandlingPrompt(it) }
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to show url handling prompt", e)
|
||||
}
|
||||
}
|
||||
|
||||
@FormField(R.string.language, "group", -1, 0)
|
||||
var language = LanguageSettings();
|
||||
@Serializable
|
||||
|
@ -376,6 +399,14 @@ class Settings : FragmentedStorageFileJson() {
|
|||
|
||||
@FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 10)
|
||||
var backgroundSwitchToAudio: Boolean = true;
|
||||
|
||||
@FormField(R.string.restart_after_audio_focus_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_audio_focus_after_a_loss, 11)
|
||||
@DropdownFieldOptionsId(R.array.restart_playback_after_loss)
|
||||
var restartPlaybackAfterLoss: Int = 1;
|
||||
|
||||
@FormField(R.string.restart_after_connectivity_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_connectivity_after_a_loss, 12)
|
||||
@DropdownFieldOptionsId(R.array.restart_playback_after_loss)
|
||||
var restartPlaybackAfterConnectivityLoss: Int = 1;
|
||||
}
|
||||
|
||||
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||
|
@ -385,6 +416,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||
@FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
|
||||
@DropdownFieldOptionsId(R.array.comment_sections)
|
||||
var defaultCommentSection: Int = 0;
|
||||
|
||||
@FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0)
|
||||
var badReputationCommentsFading: Boolean = true;
|
||||
}
|
||||
|
||||
@FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 7)
|
||||
|
@ -692,25 +726,16 @@ class Settings : FragmentedStorageFileJson() {
|
|||
|
||||
@FormField(R.string.export_data, FieldForm.BUTTON, R.string.creates_a_zip_file_with_your_data_which_can_be_imported_by_opening_it_with_grayjay, 3)
|
||||
fun export() {
|
||||
StateBackup.startExternalBackup();
|
||||
val activity = SettingsActivity.getActivity() ?: return;
|
||||
UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
|
||||
SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", null, {
|
||||
StateBackup.shareExternalBackup();
|
||||
}),
|
||||
SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", null, {
|
||||
StateBackup.saveExternalBackup(activity);
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
@FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, 4)
|
||||
fun import() {
|
||||
val act = SettingsActivity.getActivity() ?: return;
|
||||
StateApp.instance.requestFileReadAccess(act, null) {
|
||||
if(it != null && it.exists()) {
|
||||
val name = it.name;
|
||||
val contents = it.readBytes(act);
|
||||
if(contents != null) {
|
||||
if(name != null && name.endsWith(".zip", true))
|
||||
StateBackup.importZipBytes(act, act.lifecycleScope, contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
@FormField(R.string.payment, FieldForm.GROUP, -1, 17)
|
||||
|
@ -737,6 +762,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||
@FormField(R.string.bypass_rotation_prevention, FieldForm.TOGGLE, R.string.bypass_rotation_prevention_description, 1)
|
||||
@FormFieldWarning(R.string.bypass_rotation_prevention_warning)
|
||||
var bypassRotationPrevention: Boolean = false;
|
||||
|
||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 1)
|
||||
var polycentricEnabled: Boolean = true;
|
||||
}
|
||||
|
||||
@FormField(R.string.info, FieldForm.GROUP, -1, 19)
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package com.futo.platformplayer
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
|
@ -10,12 +13,12 @@ import android.view.View
|
|||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.dialogs.*
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
|
@ -91,6 +94,50 @@ class UIDialogs {
|
|||
}.toTypedArray());
|
||||
}
|
||||
|
||||
fun showUrlHandlingPrompt(context: Context, onYes: (() -> Unit)? = null) {
|
||||
val builder = AlertDialog.Builder(context)
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_url_handling, null)
|
||||
builder.setView(view)
|
||||
|
||||
val dialog = builder.create()
|
||||
registerDialogOpened(dialog)
|
||||
|
||||
view.findViewById<TextView>(R.id.button_no).apply {
|
||||
this.setOnClickListener {
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
view.findViewById<LinearLayout>(R.id.button_yes).apply {
|
||||
this.setOnClickListener {
|
||||
if (BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||
dialog.dismiss()
|
||||
showDialogOk(context, R.drawable.ic_error_pred, context.getString(R.string.play_store_version_does_not_support_default_url_handling)) {
|
||||
onYes?.invoke()
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
val intent =
|
||||
Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
val uri = Uri.fromParts("package", context.packageName, null)
|
||||
intent.data = uri
|
||||
context.startActivity(intent)
|
||||
} catch (e: Throwable) {
|
||||
toast(context, context.getString(R.string.failed_to_show_settings))
|
||||
}
|
||||
|
||||
onYes?.invoke()
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialog.setOnDismissListener {
|
||||
registerDialogClosed(dialog)
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
fun showAutomaticBackupDialog(context: Context, skipRestoreCheck: Boolean = false, onClosed: (()->Unit)? = null) {
|
||||
val dialogAction: ()->Unit = {
|
||||
|
@ -107,7 +154,8 @@ class UIDialogs {
|
|||
}, UIDialogs.ActionStyle.DANGEROUS),
|
||||
UIDialogs.Action(context.getString(R.string.restore), {
|
||||
UIDialogs.showAutomaticRestoreDialog(context, StateApp.instance.scope);
|
||||
}, UIDialogs.ActionStyle.PRIMARY));
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
else {
|
||||
dialogAction();
|
||||
}
|
||||
|
@ -142,8 +190,10 @@ class UIDialogs {
|
|||
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
||||
if(code == null)
|
||||
this.visibility = View.GONE;
|
||||
else
|
||||
else {
|
||||
this.text = code;
|
||||
this.visibility = View.VISIBLE;
|
||||
}
|
||||
};
|
||||
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
|
||||
val buttons = actions.map<Action, TextView> { act ->
|
||||
|
@ -279,6 +329,12 @@ class UIDialogs {
|
|||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
}
|
||||
fun showImportOptionsDialog(context: MainActivity) {
|
||||
val dialog = ImportOptionsDialog(context);
|
||||
registerDialogOpened(dialog);
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
|
||||
fun showCastingDialog(context: Context) {
|
||||
|
@ -291,11 +347,22 @@ class UIDialogs {
|
|||
} else {
|
||||
val dialog = ConnectCastingDialog(context);
|
||||
registerDialogOpened(dialog);
|
||||
val c = context
|
||||
if (c is Activity) {
|
||||
dialog.setOwnerActivity(c);
|
||||
}
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
fun showCastingTutorialDialog(context: Context) {
|
||||
val dialog = CastingHelpDialog(context);
|
||||
registerDialogOpened(dialog);
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
fun showCastingAddDialog(context: Context) {
|
||||
val dialog = CastingAddDialog(context);
|
||||
registerDialogOpened(dialog);
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
package com.futo.platformplayer
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.graphics.Color
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.SubtitleRawSource
|
||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
|
@ -24,8 +21,9 @@ import com.futo.platformplayer.helpers.VideoHelper
|
|||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Playlist
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.parsers.HLS
|
||||
import com.futo.platformplayer.states.*
|
||||
import com.futo.platformplayer.views.Loader
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||
|
@ -127,6 +125,101 @@ class UISlideOverlays {
|
|||
}
|
||||
}
|
||||
|
||||
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
|
||||
val items = arrayListOf<View>(LoaderView(container.context))
|
||||
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
val masterPlaylistResponse = ManagedHttpClient().get(sourceUrl)
|
||||
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
||||
|
||||
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||
?: throw Exception("Master playlist content is empty")
|
||||
|
||||
val videoButtons = arrayListOf<SlideUpMenuItem>()
|
||||
val audioButtons = arrayListOf<SlideUpMenuItem>()
|
||||
//TODO: Implement subtitles
|
||||
//val subtitleButtons = arrayListOf<SlideUpMenuItem>()
|
||||
|
||||
var selectedVideoVariant: HLSVariantVideoUrlSource? = null
|
||||
var selectedAudioVariant: HLSVariantAudioUrlSource? = null
|
||||
//TODO: Implement subtitles
|
||||
//var selectedSubtitleVariant: HLSVariantSubtitleUrlSource? = null
|
||||
|
||||
val masterPlaylist: HLS.MasterPlaylist
|
||||
try {
|
||||
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
|
||||
|
||||
masterPlaylist.getAudioSources().forEach { it ->
|
||||
audioButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
|
||||
selectedAudioVariant = it
|
||||
slideUpMenuOverlay.selectOption(audioButtons, it)
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
}, false))
|
||||
}
|
||||
|
||||
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
||||
subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
|
||||
selectedSubtitleVariant = it
|
||||
slideUpMenuOverlay.selectOption(subtitleButtons, it)
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
}, false))
|
||||
}*/
|
||||
|
||||
masterPlaylist.getVideoSources().forEach {
|
||||
videoButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
||||
selectedVideoVariant = it
|
||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
}, false))
|
||||
}
|
||||
|
||||
val newItems = arrayListOf<View>()
|
||||
if (videoButtons.isNotEmpty()) {
|
||||
newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoButtons, videoButtons))
|
||||
}
|
||||
if (audioButtons.isNotEmpty()) {
|
||||
newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.audio), audioButtons, audioButtons))
|
||||
}
|
||||
//TODO: Implement subtitles
|
||||
/*if (subtitleButtons.isNotEmpty()) {
|
||||
newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleButtons, subtitleButtons))
|
||||
}*/
|
||||
|
||||
slideUpMenuOverlay.onOK.subscribe {
|
||||
//TODO: Fix SubtitleRawSource issue
|
||||
StateDownloads.instance.download(video, selectedVideoVariant, selectedAudioVariant, null);
|
||||
slideUpMenuOverlay.hide()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
slideUpMenuOverlay.setItems(newItems)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (source is IHLSManifestSource) {
|
||||
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, sourceUrl), null, null)
|
||||
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
||||
slideUpMenuOverlay.hide()
|
||||
} else if (source is IHLSManifestAudioSource) {
|
||||
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, sourceUrl), null)
|
||||
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
||||
slideUpMenuOverlay.hide()
|
||||
} else {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return slideUpMenuOverlay.apply { show() }
|
||||
|
||||
}
|
||||
|
||||
fun showDownloadVideoOverlay(video: IPlatformVideoDetails, container: ViewGroup, contentResolver: ContentResolver? = null): SlideUpMenuOverlay? {
|
||||
val items = arrayListOf<View>();
|
||||
var menu: SlideUpMenuOverlay? = null;
|
||||
|
@ -166,30 +259,49 @@ class UISlideOverlays {
|
|||
videoSources
|
||||
.filter { it.isDownloadable() }
|
||||
.map {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
||||
selectedVideo = it as IVideoUrlSource;
|
||||
menu?.selectOption(videoSources, it);
|
||||
if(selectedAudio != null || !requiresAudio)
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
}, false)
|
||||
if (it is IVideoUrlSource) {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
|
||||
selectedVideo = it
|
||||
menu?.selectOption(videoSources, it);
|
||||
if(selectedAudio != null || !requiresAudio)
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
}, false)
|
||||
} else if (it is IHLSManifestSource) {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS", it, {
|
||||
showHlsPicker(video, it, it.url, container)
|
||||
}, false)
|
||||
} else {
|
||||
throw Exception("Unhandled source type")
|
||||
}
|
||||
}).flatten().toList()
|
||||
));
|
||||
|
||||
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.size > 0)
|
||||
selectedVideo = VideoHelper.selectBestVideoSource(videoSources.filter { it.isDownloadable() }.asIterable(),
|
||||
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.size > 0) {
|
||||
//TODO: Add HLS support here
|
||||
selectedVideo = VideoHelper.selectBestVideoSource(
|
||||
videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
|
||||
Settings.instance.downloads.getDefaultVideoQualityPixels(),
|
||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) as IVideoUrlSource;
|
||||
|
||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
||||
) as IVideoUrlSource;
|
||||
}
|
||||
|
||||
audioSources?.let { audioSources ->
|
||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.audio), audioSources, audioSources
|
||||
.filter { VideoHelper.isDownloadable(it) }
|
||||
.map {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
||||
selectedAudio = it as IAudioUrlSource;
|
||||
menu?.selectOption(audioSources, it);
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
}, false);
|
||||
if (it is IAudioUrlSource) {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
|
||||
selectedAudio = it
|
||||
menu?.selectOption(audioSources, it);
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
}, false);
|
||||
} else if (it is IHLSManifestAudioSource) {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS Audio", it, {
|
||||
showHlsPicker(video, it, it.url, container)
|
||||
}, false)
|
||||
} else {
|
||||
throw Exception("Unhandled source type")
|
||||
}
|
||||
}));
|
||||
val asources = audioSources;
|
||||
val preferredAudioSource = VideoHelper.selectBestAudioSource(asources.asIterable(),
|
||||
|
@ -198,15 +310,15 @@ class UISlideOverlays {
|
|||
if(Settings.instance.downloads.isHighBitrateDefault()) 99999999 else 1);
|
||||
menu?.selectOption(asources, preferredAudioSource);
|
||||
|
||||
|
||||
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it.isDownloadable() }.asIterable(),
|
||||
//TODO: Add HLS support here
|
||||
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource && it.isDownloadable() }.asIterable(),
|
||||
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
||||
Settings.instance.playback.getPrimaryLanguage(container.context),
|
||||
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
|
||||
}
|
||||
|
||||
//ContentResolver is required for subtitles..
|
||||
if(contentResolver != null) {
|
||||
if(contentResolver != null && subtitleSources.isNotEmpty()) {
|
||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources
|
||||
.map {
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
|
||||
|
@ -378,7 +490,7 @@ class UISlideOverlays {
|
|||
val dp70 = 70.dp(container.context.resources);
|
||||
val dp15 = 15.dp(container.context.resources);
|
||||
val overlay = SlideUpMenuOverlay(container.context, container, text, null, true, listOf(
|
||||
Loader(container.context, true, dp70).apply {
|
||||
LoaderView(container.context, true, dp70).apply {
|
||||
this.setPadding(0, dp15, 0, dp15);
|
||||
}
|
||||
), true);
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
package com.futo.platformplayer.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.dialogs.CastingHelpDialog
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
|
||||
class FCastGuideActivity : AppCompatActivity() {
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_fcast_guide);
|
||||
setNavigationBarColorAndIcons();
|
||||
|
||||
findViewById<TextView>(R.id.text_explanation).apply {
|
||||
val guideText = """
|
||||
<h3>1. Install FCast Receiver:</h3>
|
||||
<p>- Open Play Store, FireStore, or FCast website on your TV/desktop.<br>
|
||||
- Search for "FCast Receiver", install and open it.</p>
|
||||
<br>
|
||||
|
||||
<h3>2. Prepare the Grayjay App:</h3>
|
||||
<p>- Ensure it's connected to the same network as the FCast Receiver.</p>
|
||||
<br>
|
||||
|
||||
<h3>3. Initiate Casting from Grayjay:</h3>
|
||||
<p>- Click the cast button in Grayjay.</p>
|
||||
<br>
|
||||
|
||||
<h3>4. Connect to FCast Receiver:</h3>
|
||||
<p>- Wait for your device to show in the list or add it manually with its IP address.</p>
|
||||
<br>
|
||||
|
||||
<h3>5. Confirm Connection:</h3>
|
||||
<p>- Click "OK" to confirm your device selection.</p>
|
||||
<br>
|
||||
|
||||
<h3>6. Start Casting:</h3>
|
||||
<p>- Press "start" next to the device you've added.</p>
|
||||
<br>
|
||||
|
||||
<h3>7. Play Your Video:</h3>
|
||||
<p>- Start any video in Grayjay to cast.</p>
|
||||
<br>
|
||||
|
||||
<h3>Finding Your IP Address:</h3>
|
||||
<p><b>On FCast Receiver (Android):</b> Displayed on the main screen.<br>
|
||||
<b>On Windows:</b> Use 'ipconfig' in Command Prompt.<br>
|
||||
<b>On Linux:</b> Use 'hostname -I' or 'ip addr' in Terminal.<br>
|
||||
<b>On MacOS:</b> System Preferences > Network.</p>
|
||||
""".trimIndent()
|
||||
|
||||
text = Html.fromHtml(guideText, Html.FROM_HTML_MODE_COMPACT)
|
||||
}
|
||||
|
||||
findViewById<ImageButton>(R.id.button_back).setOnClickListener {
|
||||
UIDialogs.showCastingTutorialDialog(this)
|
||||
finish()
|
||||
}
|
||||
|
||||
findViewById<BigButton>(R.id.button_close).onClick.subscribe {
|
||||
UIDialogs.showCastingTutorialDialog(this)
|
||||
finish()
|
||||
}
|
||||
|
||||
findViewById<BigButton>(R.id.button_website).onClick.subscribe {
|
||||
try {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://fcast.org/"))
|
||||
startActivity(browserIntent);
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to open browser.", e)
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<BigButton>(R.id.button_technical).onClick.subscribe {
|
||||
try {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1"))
|
||||
startActivity(browserIntent);
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to open browser.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
UIDialogs.showCastingTutorialDialog(this)
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FCastGuideActivity";
|
||||
}
|
||||
}
|
|
@ -7,7 +7,6 @@ import android.content.pm.ActivityInfo
|
|||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
|
@ -25,11 +24,9 @@ import androidx.fragment.app.FragmentContainerView
|
|||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event3
|
||||
import com.futo.platformplayer.dialogs.ConnectCastingDialog
|
||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.*
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
|
||||
|
@ -45,6 +42,7 @@ import com.futo.platformplayer.stores.FragmentedStorage
|
|||
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import com.google.gson.JsonParser
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
@ -90,6 +88,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
|
||||
lateinit var _fragMainSuggestions: SuggestionsFragment;
|
||||
lateinit var _fragMainSubscriptions: CreatorsFragment;
|
||||
lateinit var _fragMainComments: CommentsFragment;
|
||||
lateinit var _fragMainSubscriptionsFeed: SubscriptionsFeedFragment;
|
||||
lateinit var _fragMainChannel: ChannelFragment;
|
||||
lateinit var _fragMainSources: SourcesFragment;
|
||||
|
@ -123,6 +122,24 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
private var _isVisible = true;
|
||||
private var _wasStopped = false;
|
||||
|
||||
private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
|
||||
scanResult?.let {
|
||||
val content = it.contents
|
||||
if (content == null) {
|
||||
UIDialogs.toast(this, getString(R.string.failed_to_scan_qr_code))
|
||||
return@let
|
||||
}
|
||||
|
||||
try {
|
||||
handleUrlAll(content)
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to handle URL.", e)
|
||||
UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor() : super() {
|
||||
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
|
||||
val writer = StringWriter();
|
||||
|
@ -205,6 +222,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
_fragMainCreatorSearchResults = CreatorSearchResultsFragment.newInstance();
|
||||
_fragMainPlaylistSearchResults = PlaylistSearchResultsFragment.newInstance();
|
||||
_fragMainSubscriptions = CreatorsFragment.newInstance();
|
||||
_fragMainComments = CommentsFragment.newInstance();
|
||||
_fragMainChannel = ChannelFragment.newInstance();
|
||||
_fragMainSubscriptionsFeed = SubscriptionsFeedFragment.newInstance();
|
||||
_fragMainSources = SourcesFragment.newInstance();
|
||||
|
@ -282,6 +300,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
//Set top bars
|
||||
_fragMainHome.topBar = _fragTopBarGeneral;
|
||||
_fragMainSubscriptions.topBar = _fragTopBarGeneral;
|
||||
_fragMainComments.topBar = _fragTopBarGeneral;
|
||||
_fragMainSuggestions.topBar = _fragTopBarSearch;
|
||||
_fragMainVideoSearchResults.topBar = _fragTopBarSearch;
|
||||
_fragMainCreatorSearchResults.topBar = _fragTopBarSearch;
|
||||
|
@ -406,6 +425,23 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
UIDialogs.toast(this, "No external file permissions\nExporting and auto backups will not work");
|
||||
}*/
|
||||
|
||||
fun showUrlQrCodeScanner() {
|
||||
try {
|
||||
val integrator = IntentIntegrator(this)
|
||||
integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||
integrator.setPrompt(getString(R.string.scan_a_qr_code))
|
||||
integrator.setOrientationLocked(true);
|
||||
integrator.setCameraId(0)
|
||||
integrator.setBeepEnabled(false)
|
||||
integrator.setBarcodeImageEnabled(true)
|
||||
integrator.captureActivity = QRCaptureActivity::class.java
|
||||
_urlQrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to handle show QR scanner.", e)
|
||||
UIDialogs.toast(this, "Failed to show QR scanner: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume();
|
||||
Logger.v(TAG, "onResume")
|
||||
|
@ -479,6 +515,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
val url = intent.getStringExtra("VIDEO");
|
||||
navigate(_fragVideoDetail, url);
|
||||
}
|
||||
"IMPORT_OPTIONS" -> {
|
||||
UIDialogs.showImportOptionsDialog(this);
|
||||
}
|
||||
"TAB" -> {
|
||||
when(intent.getStringExtra("TAB")){
|
||||
"Sources" -> {
|
||||
|
@ -493,76 +532,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
|
||||
try {
|
||||
if (targetData != null) {
|
||||
when(intent.scheme) {
|
||||
"grayjay" -> {
|
||||
if(targetData.startsWith("grayjay://license/")) {
|
||||
if(StatePayment.instance.setPaymentLicenseUrl(targetData))
|
||||
{
|
||||
UIDialogs.showDialogOk(this, R.drawable.ic_check, getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required));
|
||||
|
||||
if(fragCurrent is BuyFragment)
|
||||
closeSegment(fragCurrent);
|
||||
}
|
||||
else
|
||||
UIDialogs.toast(getString(R.string.invalid_license_format));
|
||||
|
||||
}
|
||||
else if(targetData.startsWith("grayjay://plugin/")) {
|
||||
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
||||
data = Uri.parse(targetData.substring("grayjay://plugin/".length));
|
||||
};
|
||||
startActivity(intent);
|
||||
}
|
||||
else if(targetData.startsWith("grayjay://video/")) {
|
||||
val videoUrl = targetData.substring("grayjay://video/".length);
|
||||
navigate(_fragVideoDetail, videoUrl);
|
||||
}
|
||||
else if(targetData.startsWith("grayjay://channel/")) {
|
||||
val channelUrl = targetData.substring("grayjay://channel/".length);
|
||||
navigate(_fragMainChannel, channelUrl);
|
||||
}
|
||||
}
|
||||
"content" -> {
|
||||
if(!handleContent(targetData, intent.type)) {
|
||||
UIDialogs.showSingleButtonDialog(
|
||||
this,
|
||||
R.drawable.ic_play,
|
||||
getString(R.string.unknown_content_format) + " [${targetData}]",
|
||||
"Ok",
|
||||
{ });
|
||||
}
|
||||
}
|
||||
"file" -> {
|
||||
if(!handleFile(targetData)) {
|
||||
UIDialogs.showSingleButtonDialog(
|
||||
this,
|
||||
R.drawable.ic_play,
|
||||
getString(R.string.unknown_file_format) + " [${targetData}]",
|
||||
"Ok",
|
||||
{ });
|
||||
}
|
||||
}
|
||||
"polycentric" -> {
|
||||
if(!handlePolycentric(targetData)) {
|
||||
UIDialogs.showSingleButtonDialog(
|
||||
this,
|
||||
R.drawable.ic_play,
|
||||
getString(R.string.unknown_polycentric_format) + " [${targetData}]",
|
||||
"Ok",
|
||||
{ });
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (!handleUrl(targetData)) {
|
||||
UIDialogs.showSingleButtonDialog(
|
||||
this,
|
||||
R.drawable.ic_play,
|
||||
getString(R.string.unknown_url_format) + " [${targetData}]",
|
||||
"Ok",
|
||||
{ });
|
||||
}
|
||||
}
|
||||
}
|
||||
handleUrlAll(targetData)
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
|
@ -570,6 +540,90 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
}
|
||||
}
|
||||
|
||||
fun handleUrlAll(url: String) {
|
||||
val uri = Uri.parse(url)
|
||||
when (uri.scheme) {
|
||||
"grayjay" -> {
|
||||
if(url.startsWith("grayjay://license/")) {
|
||||
if(StatePayment.instance.setPaymentLicenseUrl(url))
|
||||
{
|
||||
UIDialogs.showDialogOk(this, R.drawable.ic_check, getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required));
|
||||
|
||||
if(fragCurrent is BuyFragment)
|
||||
closeSegment(fragCurrent);
|
||||
}
|
||||
else
|
||||
UIDialogs.toast(getString(R.string.invalid_license_format));
|
||||
|
||||
}
|
||||
else if(url.startsWith("grayjay://plugin/")) {
|
||||
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
||||
data = Uri.parse(url.substring("grayjay://plugin/".length));
|
||||
};
|
||||
startActivity(intent);
|
||||
}
|
||||
else if(url.startsWith("grayjay://video/")) {
|
||||
val videoUrl = url.substring("grayjay://video/".length);
|
||||
navigate(_fragVideoDetail, videoUrl);
|
||||
}
|
||||
else if(url.startsWith("grayjay://channel/")) {
|
||||
val channelUrl = url.substring("grayjay://channel/".length);
|
||||
navigate(_fragMainChannel, channelUrl);
|
||||
}
|
||||
}
|
||||
"content" -> {
|
||||
if(!handleContent(url, intent.type)) {
|
||||
UIDialogs.showSingleButtonDialog(
|
||||
this,
|
||||
R.drawable.ic_play,
|
||||
getString(R.string.unknown_content_format) + " [${url}]",
|
||||
"Ok",
|
||||
{ });
|
||||
}
|
||||
}
|
||||
"file" -> {
|
||||
if(!handleFile(url)) {
|
||||
UIDialogs.showSingleButtonDialog(
|
||||
this,
|
||||
R.drawable.ic_play,
|
||||
getString(R.string.unknown_file_format) + " [${url}]",
|
||||
"Ok",
|
||||
{ });
|
||||
}
|
||||
}
|
||||
"polycentric" -> {
|
||||
if(!handlePolycentric(url)) {
|
||||
UIDialogs.showSingleButtonDialog(
|
||||
this,
|
||||
R.drawable.ic_play,
|
||||
getString(R.string.unknown_polycentric_format) + " [${url}]",
|
||||
"Ok",
|
||||
{ });
|
||||
}
|
||||
}
|
||||
"fcast" -> {
|
||||
if(!handleFCast(url)) {
|
||||
UIDialogs.showSingleButtonDialog(
|
||||
this,
|
||||
R.drawable.ic_cast,
|
||||
"Unknown FCast format [${url}]",
|
||||
"Ok",
|
||||
{ });
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (!handleUrl(url)) {
|
||||
UIDialogs.showSingleButtonDialog(
|
||||
this,
|
||||
R.drawable.ic_play,
|
||||
getString(R.string.unknown_url_format) + " [${url}]",
|
||||
"Ok",
|
||||
{ });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleUrl(url: String): Boolean {
|
||||
Logger.i(TAG, "handleUrl(url=$url)")
|
||||
|
||||
|
@ -679,18 +733,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
if (!newPipeSubsParsed.has("subscriptions") || !newPipeSubsParsed["subscriptions"].isJsonArray)
|
||||
return false;//throw IllegalArgumentException("Invalid NewPipe json structure found");
|
||||
|
||||
val jsonSubs = newPipeSubsParsed["subscriptions"]
|
||||
val jsonSubsArray = jsonSubs.asJsonArray;
|
||||
val jsonSubsArrayItt = jsonSubsArray.iterator();
|
||||
val subs = mutableListOf<String>()
|
||||
while(jsonSubsArrayItt.hasNext()) {
|
||||
val jsonSubObj = jsonSubsArrayItt.next().asJsonObject;
|
||||
|
||||
if(jsonSubObj.has("url"))
|
||||
subs.add(jsonSubObj["url"].asString);
|
||||
}
|
||||
|
||||
navigate(_fragImportSubscriptions, subs);
|
||||
StateBackup.importNewPipeSubs(this, newPipeSubsParsed);
|
||||
}
|
||||
catch(ex: Exception) {
|
||||
Logger.e(TAG, ex.message, ex);
|
||||
|
@ -716,6 +759,20 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
startActivity(Intent(this, PolycentricImportProfileActivity::class.java).apply { putExtra("url", url) })
|
||||
return true;
|
||||
}
|
||||
|
||||
fun handleFCast(url: String): Boolean {
|
||||
Logger.i(TAG, "handleFCast");
|
||||
|
||||
try {
|
||||
StateCasting.instance.handleUrl(this, url)
|
||||
return true;
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun readSharedContent(contentPath: String): ByteArray {
|
||||
return contentResolver.openInputStream(Uri.parse(contentPath))?.use {
|
||||
return it.readBytes();
|
||||
|
@ -916,6 +973,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
GeneralTopBarFragment::class -> _fragTopBarGeneral as T;
|
||||
SearchTopBarFragment::class -> _fragTopBarSearch as T;
|
||||
CreatorsFragment::class -> _fragMainSubscriptions as T;
|
||||
CommentsFragment::class -> _fragMainComments as T;
|
||||
SubscriptionsFeedFragment::class -> _fragMainSubscriptionsFeed as T;
|
||||
PlaylistSearchResultsFragment::class -> _fragMainPlaylistSearchResults as T;
|
||||
ChannelFragment::class -> _fragMainChannel as T;
|
||||
|
@ -988,5 +1046,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
return sourcesIntent;
|
||||
}
|
||||
|
||||
fun getImportOptionsIntent(context: Context): Intent {
|
||||
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||
sourcesIntent.action = "IMPORT_OPTIONS";
|
||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
return sourcesIntent;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import androidx.activity.result.ActivityResult
|
||||
|
@ -15,7 +16,7 @@ import androidx.lifecycle.lifecycleScope
|
|||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.Loader
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
import com.futo.platformplayer.views.fields.FieldForm
|
||||
import com.futo.platformplayer.views.fields.ReadOnlyTextField
|
||||
import com.google.android.material.button.MaterialButton
|
||||
|
@ -23,13 +24,15 @@ import com.google.android.material.button.MaterialButton
|
|||
class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
||||
private lateinit var _form: FieldForm;
|
||||
private lateinit var _buttonBack: ImageButton;
|
||||
private lateinit var _loader: Loader;
|
||||
private lateinit var _loaderView: LoaderView;
|
||||
|
||||
private lateinit var _devSets: LinearLayout;
|
||||
private lateinit var _buttonDev: MaterialButton;
|
||||
|
||||
private var _isFinished = false;
|
||||
|
||||
lateinit var overlay: FrameLayout;
|
||||
|
||||
override fun attachBaseContext(newBase: Context?) {
|
||||
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
|
||||
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
|
||||
|
@ -43,7 +46,8 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||
_buttonBack = findViewById(R.id.button_back);
|
||||
_buttonDev = findViewById(R.id.button_dev);
|
||||
_devSets = findViewById(R.id.dev_settings);
|
||||
_loader = findViewById(R.id.loader);
|
||||
_loaderView = findViewById(R.id.loader);
|
||||
overlay = findViewById(R.id.overlay_container);
|
||||
|
||||
_form.onChanged.subscribe { field, value ->
|
||||
Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
|
||||
|
@ -70,9 +74,9 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
|
|||
|
||||
fun reloadSettings() {
|
||||
_form.setSearchVisible(false);
|
||||
_loader.start();
|
||||
_loaderView.start();
|
||||
_form.fromObject(lifecycleScope, Settings.instance) {
|
||||
_loader.stop();
|
||||
_loaderView.stop();
|
||||
_form.setSearchVisible(true);
|
||||
|
||||
var devCounter = 0;
|
||||
|
|
|
@ -4,10 +4,7 @@ import com.futo.platformplayer.api.media.IPlatformClient
|
|||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.polycentric.core.Pointer
|
||||
import com.futo.polycentric.core.SignedEvent
|
||||
import userpackage.Protocol.Reference
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
|
@ -20,16 +17,18 @@ class PolycentricPlatformComment : IPlatformComment {
|
|||
|
||||
override val replyCount: Int?;
|
||||
|
||||
val eventPointer: Pointer;
|
||||
val reference: Reference;
|
||||
|
||||
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, reference: Reference, replyCount: Int? = null) {
|
||||
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, replyCount: Int? = null) {
|
||||
this.contextUrl = contextUrl;
|
||||
this.author = author;
|
||||
this.message = msg;
|
||||
this.rating = rating;
|
||||
this.date = date;
|
||||
this.replyCount = replyCount;
|
||||
this.reference = reference;
|
||||
this.eventPointer = eventPointer;
|
||||
this.reference = eventPointer.toReference();
|
||||
}
|
||||
|
||||
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
|
||||
|
@ -37,7 +36,7 @@ class PolycentricPlatformComment : IPlatformComment {
|
|||
}
|
||||
|
||||
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
|
||||
return PolycentricPlatformComment(contextUrl, author, message, rating, date, reference, replyCount);
|
||||
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, replyCount);
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
import android.net.Uri
|
||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
|
||||
class HLSVariantVideoUrlSource(
|
||||
override val name: String,
|
||||
override val width: Int,
|
||||
override val height: Int,
|
||||
override val container: String,
|
||||
override val codec: String,
|
||||
override val bitrate: Int?,
|
||||
override val duration: Long,
|
||||
override val priority: Boolean,
|
||||
val url: String
|
||||
) : IVideoUrlSource {
|
||||
override fun getVideoUrl(): String {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
class HLSVariantAudioUrlSource(
|
||||
override val name: String,
|
||||
override val bitrate: Int,
|
||||
override val container: String,
|
||||
override val codec: String,
|
||||
override val language: String,
|
||||
override val duration: Long?,
|
||||
override val priority: Boolean,
|
||||
val url: String
|
||||
) : IAudioUrlSource {
|
||||
override fun getAudioUrl(): String {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
class HLSVariantSubtitleUrlSource(
|
||||
override val name: String,
|
||||
override val url: String,
|
||||
override val format: String,
|
||||
) : ISubtitleSource {
|
||||
override val hasFetch: Boolean = false
|
||||
|
||||
override fun getSubtitles(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun getSubtitlesURI(): Uri? {
|
||||
return Uri.parse(url)
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ package com.futo.platformplayer.casting
|
|||
import android.os.Looper
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.casting.models.FastCastSetVolumeMessage
|
||||
import com.futo.platformplayer.getConnectedSocket
|
||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||
import com.futo.platformplayer.toInetAddress
|
||||
|
@ -49,7 +48,7 @@ class AirPlayCastingDevice : CastingDevice {
|
|||
return;
|
||||
}
|
||||
|
||||
Logger.i(FastCastCastingDevice.TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration)");
|
||||
Logger.i(FCastCastingDevice.TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration)");
|
||||
|
||||
time = resumePosition;
|
||||
if (resumePosition > 0.0) {
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
package com.futo.platformplayer.casting
|
||||
|
||||
import android.content.Context
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.getNowDiffMiliseconds
|
||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import java.net.InetAddress
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
|
@ -14,10 +19,27 @@ enum class CastConnectionState {
|
|||
CONNECTED
|
||||
}
|
||||
|
||||
@Serializable(with = CastProtocolType.CastProtocolTypeSerializer::class)
|
||||
enum class CastProtocolType {
|
||||
CHROMECAST,
|
||||
AIRPLAY,
|
||||
FASTCAST
|
||||
FCAST;
|
||||
|
||||
object CastProtocolTypeSerializer : KSerializer<CastProtocolType> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CastProtocolType", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: CastProtocolType) {
|
||||
encoder.encodeString(value.name)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): CastProtocolType {
|
||||
val name = decoder.decodeString()
|
||||
return when (name) {
|
||||
"FASTCAST" -> FCAST // Handle the renamed case
|
||||
else -> CastProtocolType.valueOf(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CastingDevice {
|
||||
|
|
|
@ -2,18 +2,16 @@ package com.futo.platformplayer.casting
|
|||
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.futo.platformplayer.casting.models.FastCastSetVolumeMessage
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.getConnectedSocket
|
||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||
import com.futo.platformplayer.protos.DeviceAuthMessageOuterClass
|
||||
import com.futo.platformplayer.protos.ChromeCast
|
||||
import com.futo.platformplayer.toHexString
|
||||
import com.futo.platformplayer.toInetAddress
|
||||
import kotlinx.coroutines.*
|
||||
import org.json.JSONObject
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.SSLContext
|
||||
|
@ -376,7 +374,7 @@ class ChromecastCastingDevice : CastingDevice {
|
|||
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
|
||||
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
|
||||
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
||||
val message = DeviceAuthMessageOuterClass.CastMessage.parseFrom(messageBytes);
|
||||
val message = ChromeCast.CastMessage.parseFrom(messageBytes);
|
||||
if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
|
||||
Logger.i(TAG, "Received message: $message");
|
||||
}
|
||||
|
@ -429,12 +427,12 @@ class ChromecastCastingDevice : CastingDevice {
|
|||
|
||||
private fun sendChannelMessage(sourceId: String, destinationId: String, namespace: String, json: String) {
|
||||
try {
|
||||
val castMessage = DeviceAuthMessageOuterClass.CastMessage.newBuilder()
|
||||
.setProtocolVersion(DeviceAuthMessageOuterClass.CastMessage.ProtocolVersion.CASTV2_1_0)
|
||||
val castMessage = ChromeCast.CastMessage.newBuilder()
|
||||
.setProtocolVersion(ChromeCast.CastMessage.ProtocolVersion.CASTV2_1_0)
|
||||
.setSourceId(sourceId)
|
||||
.setDestinationId(destinationId)
|
||||
.setNamespace(namespace)
|
||||
.setPayloadType(DeviceAuthMessageOuterClass.CastMessage.PayloadType.STRING)
|
||||
.setPayloadType(ChromeCast.CastMessage.PayloadType.STRING)
|
||||
.setPayloadUtf8(json)
|
||||
.build();
|
||||
|
||||
|
@ -448,8 +446,8 @@ class ChromecastCastingDevice : CastingDevice {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleMessage(message: DeviceAuthMessageOuterClass.CastMessage) {
|
||||
if (message.payloadType == DeviceAuthMessageOuterClass.CastMessage.PayloadType.STRING) {
|
||||
private fun handleMessage(message: ChromeCast.CastMessage) {
|
||||
if (message.payloadType == ChromeCast.CastMessage.PayloadType.STRING) {
|
||||
val jsonObject = JSONObject(message.payloadUtf8);
|
||||
val type = jsonObject.getString("type");
|
||||
if (type == "RECEIVER_STATUS") {
|
||||
|
|
|
@ -30,10 +30,10 @@ enum class Opcode(val value: Byte) {
|
|||
SET_VOLUME(8)
|
||||
}
|
||||
|
||||
class FastCastCastingDevice : CastingDevice {
|
||||
class FCastCastingDevice : CastingDevice {
|
||||
//See for more info: TODO
|
||||
|
||||
override val protocol: CastProtocolType get() = CastProtocolType.FASTCAST;
|
||||
override val protocol: CastProtocolType get() = CastProtocolType.FCAST;
|
||||
override val isReady: Boolean get() = name != null && addresses != null && addresses?.isNotEmpty() == true && port != 0;
|
||||
override var usedRemoteAddress: InetAddress? = null;
|
||||
override var localAddress: InetAddress? = null;
|
||||
|
@ -72,7 +72,7 @@ class FastCastCastingDevice : CastingDevice {
|
|||
Logger.i(TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration)");
|
||||
|
||||
time = resumePosition;
|
||||
sendMessage(Opcode.PLAY, FastCastPlayMessage(
|
||||
sendMessage(Opcode.PLAY, FCastPlayMessage(
|
||||
container = contentType,
|
||||
url = contentId,
|
||||
time = resumePosition.toInt()
|
||||
|
@ -87,7 +87,7 @@ class FastCastCastingDevice : CastingDevice {
|
|||
Logger.i(TAG, "Start streaming content (contentType: $contentType, resumePosition: $resumePosition, duration: $duration)");
|
||||
|
||||
time = resumePosition;
|
||||
sendMessage(Opcode.PLAY, FastCastPlayMessage(
|
||||
sendMessage(Opcode.PLAY, FCastPlayMessage(
|
||||
container = contentType,
|
||||
content = content,
|
||||
time = resumePosition.toInt()
|
||||
|
@ -100,7 +100,7 @@ class FastCastCastingDevice : CastingDevice {
|
|||
}
|
||||
|
||||
this.volume = volume
|
||||
sendMessage(Opcode.SET_VOLUME, FastCastSetVolumeMessage(volume))
|
||||
sendMessage(Opcode.SET_VOLUME, FCastSetVolumeMessage(volume))
|
||||
}
|
||||
|
||||
override fun seekVideo(timeSeconds: Double) {
|
||||
|
@ -108,7 +108,7 @@ class FastCastCastingDevice : CastingDevice {
|
|||
return;
|
||||
}
|
||||
|
||||
sendMessage(Opcode.SEEK, FastCastSeekMessage(
|
||||
sendMessage(Opcode.SEEK, FCastSeekMessage(
|
||||
time = timeSeconds.toInt()
|
||||
));
|
||||
}
|
||||
|
@ -282,7 +282,7 @@ class FastCastCastingDevice : CastingDevice {
|
|||
return;
|
||||
}
|
||||
|
||||
val playbackUpdate = Json.decodeFromString<FastCastPlaybackUpdateMessage>(json);
|
||||
val playbackUpdate = Json.decodeFromString<FCastPlaybackUpdateMessage>(json);
|
||||
time = playbackUpdate.time.toDouble();
|
||||
isPlaying = when (playbackUpdate.state) {
|
||||
1 -> true
|
||||
|
@ -295,7 +295,7 @@ class FastCastCastingDevice : CastingDevice {
|
|||
return;
|
||||
}
|
||||
|
||||
val volumeUpdate = Json.decodeFromString<FastCastVolumeUpdateMessage>(json);
|
||||
val volumeUpdate = Json.decodeFromString<FCastVolumeUpdateMessage>(json);
|
||||
volume = volumeUpdate.volume;
|
||||
}
|
||||
else -> { }
|
||||
|
@ -398,7 +398,7 @@ class FastCastCastingDevice : CastingDevice {
|
|||
}
|
||||
|
||||
override fun getDeviceInfo(): CastingDeviceInfo {
|
||||
return CastingDeviceInfo(name!!, CastProtocolType.FASTCAST, addresses!!.filter { a -> a.hostAddress != null }.map { a -> a.hostAddress!! }.toTypedArray(), port);
|
||||
return CastingDeviceInfo(name!!, CastProtocolType.FCAST, addresses!!.filter { a -> a.hostAddress != null }.map { a -> a.hostAddress!! }.toTypedArray(), port);
|
||||
}
|
||||
|
||||
companion object {
|
|
@ -2,8 +2,11 @@ package com.futo.platformplayer.casting
|
|||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Looper
|
||||
import android.util.Base64
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
||||
import com.futo.platformplayer.api.http.server.handlers.*
|
||||
|
@ -27,6 +30,9 @@ import javax.jmdns.ServiceListener
|
|||
import kotlin.collections.HashMap
|
||||
import com.futo.platformplayer.stores.CastingDeviceInfoStorage
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import javax.jmdns.ServiceTypeListener
|
||||
|
||||
class StateCasting {
|
||||
|
@ -147,6 +153,32 @@ class StateCasting {
|
|||
}
|
||||
}
|
||||
|
||||
fun handleUrl(context: Context, url: String) {
|
||||
val uri = Uri.parse(url)
|
||||
if (uri.scheme != "fcast") {
|
||||
throw Exception("Expected scheme to be FCast")
|
||||
}
|
||||
|
||||
val type = uri.host
|
||||
if (type != "r") {
|
||||
throw Exception("Expected type r")
|
||||
}
|
||||
|
||||
val connectionInfo = uri.pathSegments[0]
|
||||
val json = Base64.decode(connectionInfo, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP).toString(Charsets.UTF_8)
|
||||
val networkConfig = Json.decodeFromString<FCastNetworkConfig>(json)
|
||||
val tcpService = networkConfig.services.first { v -> v.type == 0 }
|
||||
|
||||
addRememberedDevice(CastingDeviceInfo(
|
||||
name = networkConfig.name,
|
||||
type = CastProtocolType.FCAST,
|
||||
addresses = networkConfig.addresses.toTypedArray(),
|
||||
port = tcpService.port
|
||||
))
|
||||
|
||||
UIDialogs.toast(context,"FCast device '${networkConfig.name}' added")
|
||||
}
|
||||
|
||||
fun onStop() {
|
||||
val ad = activeDevice ?: return;
|
||||
Logger.i(TAG, "Stopping active device because of onStop.");
|
||||
|
@ -345,7 +377,7 @@ class StateCasting {
|
|||
} else {
|
||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
if (ad is FastCastCastingDevice) {
|
||||
if (ad is FCastCastingDevice) {
|
||||
Logger.i(TAG, "Casting as DASH direct");
|
||||
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition);
|
||||
} else if (ad is AirPlayCastingDevice) {
|
||||
|
@ -961,7 +993,7 @@ class StateCasting {
|
|||
|
||||
private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double) : List<String> {
|
||||
val ad = activeDevice ?: return listOf();
|
||||
val proxyStreams = ad !is FastCastCastingDevice;
|
||||
val proxyStreams = ad !is FCastCastingDevice;
|
||||
|
||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
||||
val id = UUID.randomUUID();
|
||||
|
@ -1042,8 +1074,8 @@ class StateCasting {
|
|||
CastProtocolType.AIRPLAY -> {
|
||||
AirPlayCastingDevice(deviceInfo);
|
||||
}
|
||||
CastProtocolType.FASTCAST -> {
|
||||
FastCastCastingDevice(deviceInfo);
|
||||
CastProtocolType.FCAST -> {
|
||||
FCastCastingDevice(deviceInfo);
|
||||
}
|
||||
else -> throw Exception("${deviceInfo.type} is not a valid casting protocol")
|
||||
}
|
||||
|
@ -1090,8 +1122,8 @@ class StateCasting {
|
|||
}
|
||||
|
||||
private fun addOrUpdateFastCastDevice(name: String, addresses: Array<InetAddress>, port: Int) {
|
||||
return addOrUpdateCastDevice<FastCastCastingDevice>(name,
|
||||
deviceFactory = { FastCastCastingDevice(name, addresses, port) },
|
||||
return addOrUpdateCastDevice<FCastCastingDevice>(name,
|
||||
deviceFactory = { FCastCastingDevice(name, addresses, port) },
|
||||
deviceUpdater = { d ->
|
||||
if (d.isReady) {
|
||||
return@addOrUpdateCastDevice false;
|
||||
|
@ -1167,6 +1199,19 @@ class StateCasting {
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class FCastNetworkConfig(
|
||||
val name: String,
|
||||
val addresses: List<String>,
|
||||
val services: List<FCastService>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class FCastService(
|
||||
val port: Int,
|
||||
val type: Int
|
||||
)
|
||||
|
||||
companion object {
|
||||
val instance: StateCasting = StateCasting();
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ package com.futo.platformplayer.casting.models
|
|||
import kotlinx.serialization.Serializable
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class FastCastPlayMessage(
|
||||
data class FCastPlayMessage(
|
||||
val container: String,
|
||||
val url: String? = null,
|
||||
val content: String? = null,
|
||||
|
@ -11,23 +11,23 @@ data class FastCastPlayMessage(
|
|||
) { }
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class FastCastSeekMessage(
|
||||
data class FCastSeekMessage(
|
||||
val time: Int
|
||||
) { }
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class FastCastPlaybackUpdateMessage(
|
||||
data class FCastPlaybackUpdateMessage(
|
||||
val time: Int,
|
||||
val state: Int
|
||||
) { }
|
||||
|
||||
|
||||
@Serializable
|
||||
data class FastCastVolumeUpdateMessage(
|
||||
data class FCastVolumeUpdateMessage(
|
||||
val volume: Double
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class FastCastSetVolumeMessage(
|
||||
data class FCastSetVolumeMessage(
|
||||
val volume: Double
|
||||
)
|
|
@ -12,10 +12,7 @@ import com.futo.platformplayer.UIDialogs
|
|||
import com.futo.platformplayer.casting.CastProtocolType
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.toInetAddress
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
||||
|
@ -26,6 +23,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
|||
private lateinit var _textError: TextView;
|
||||
private lateinit var _buttonCancel: Button;
|
||||
private lateinit var _buttonConfirm: LinearLayout;
|
||||
private lateinit var _buttonTutorial: TextView;
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
@ -38,6 +36,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
|||
_textError = findViewById(R.id.text_error);
|
||||
_buttonCancel = findViewById(R.id.button_cancel);
|
||||
_buttonConfirm = findViewById(R.id.button_confirm);
|
||||
_buttonTutorial = findViewById(R.id.button_tutorial)
|
||||
|
||||
ArrayAdapter.createFromResource(context, R.array.casting_device_type_array, R.layout.spinner_item_simple).also { adapter ->
|
||||
adapter.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
||||
|
@ -62,7 +61,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
|||
|
||||
_buttonConfirm.setOnClickListener {
|
||||
val castProtocolType: CastProtocolType = when (_spinnerType.selectedItemPosition) {
|
||||
0 -> CastProtocolType.FASTCAST
|
||||
0 -> CastProtocolType.FCAST
|
||||
1 -> CastProtocolType.CHROMECAST
|
||||
2 -> CastProtocolType.AIRPLAY
|
||||
else -> {
|
||||
|
@ -105,6 +104,11 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
|||
StateCasting.instance.addRememberedDevice(castingDeviceInfo);
|
||||
performDismiss();
|
||||
};
|
||||
|
||||
_buttonTutorial.setOnClickListener {
|
||||
UIDialogs.showCastingTutorialDialog(context)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun show() {
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package com.futo.platformplayer.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.FCastGuideActivity
|
||||
import com.futo.platformplayer.activities.PolycentricWhyActivity
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
|
||||
|
||||
class CastingHelpDialog(context: Context?) : AlertDialog(context) {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(LayoutInflater.from(context).inflate(R.layout.dialog_casting_help, null));
|
||||
|
||||
findViewById<BigButton>(R.id.button_guide).onClick.subscribe {
|
||||
context.startActivity(Intent(context, FCastGuideActivity::class.java))
|
||||
}
|
||||
|
||||
findViewById<BigButton>(R.id.button_video).onClick.subscribe {
|
||||
try {
|
||||
//TODO: Replace the URL with the casting video URL
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://fcast.org/"))
|
||||
context.startActivity(browserIntent);
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to open browser.", e)
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<BigButton>(R.id.button_website).onClick.subscribe {
|
||||
try {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://fcast.org/"))
|
||||
context.startActivity(browserIntent);
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to open browser.", e)
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<BigButton>(R.id.button_technical).onClick.subscribe {
|
||||
try {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1"))
|
||||
context.startActivity(browserIntent);
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to open browser.", e)
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<BigButton>(R.id.button_close).onClick.subscribe {
|
||||
dismiss()
|
||||
UIDialogs.showCastingAddDialog(context)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "CastingTutorialDialog";
|
||||
}
|
||||
}
|
|
@ -118,7 +118,7 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||
msg = comment,
|
||||
rating = RatingLikeDislikes(0, 0),
|
||||
date = OffsetDateTime.now(),
|
||||
reference = eventPointer.toReference()
|
||||
eventPointer = eventPointer
|
||||
));
|
||||
|
||||
dismiss();
|
||||
|
|
|
@ -1,24 +1,33 @@
|
|||
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.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
|
||||
|
@ -28,6 +37,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
private lateinit var _imageLoader: ImageView;
|
||||
private lateinit var _buttonClose: Button;
|
||||
private lateinit var _buttonAdd: Button;
|
||||
private lateinit var _buttonScanQR: Button;
|
||||
private lateinit var _textNoDevicesFound: TextView;
|
||||
private lateinit var _textNoDevicesRemembered: TextView;
|
||||
private lateinit var _recyclerDevices: RecyclerView;
|
||||
|
@ -44,6 +54,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
_imageLoader = findViewById(R.id.image_loader);
|
||||
_buttonClose = findViewById(R.id.button_close);
|
||||
_buttonAdd = findViewById(R.id.button_add);
|
||||
_buttonScanQR = findViewById(R.id.button_scan_qr);
|
||||
_recyclerDevices = findViewById(R.id.recycler_devices);
|
||||
_recyclerRememberedDevices = findViewById(R.id.recycler_remembered_devices);
|
||||
_textNoDevicesFound = findViewById(R.id.text_no_devices_found);
|
||||
|
@ -77,6 +88,17 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
UIDialogs.showCastingAddDialog(context);
|
||||
dismiss();
|
||||
};
|
||||
|
||||
val c = ownerActivity
|
||||
if (c is MainActivity) {
|
||||
_buttonScanQR.visibility = View.VISIBLE
|
||||
_buttonScanQR.setOnClickListener {
|
||||
c.showUrlQrCodeScanner()
|
||||
dismiss()
|
||||
};
|
||||
} else {
|
||||
_buttonScanQR.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun show() {
|
||||
|
|
|
@ -16,9 +16,7 @@ import com.futo.platformplayer.casting.*
|
|||
import com.futo.platformplayer.states.StateApp
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.android.material.slider.Slider.OnChangeListener
|
||||
import com.google.android.material.slider.Slider.OnSliderTouchListener
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
|
@ -105,7 +103,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
} else if (d is AirPlayCastingDevice) {
|
||||
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
||||
_textType.text = "AirPlay";
|
||||
} else if (d is FastCastCastingDevice) {
|
||||
} else if (d is FCastCastingDevice) {
|
||||
_imageDevice.setImageResource(R.drawable.ic_fc);
|
||||
_textType.text = "FastCast";
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
package com.futo.platformplayer.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Button
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.readBytes
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
|
||||
class ImportOptionsDialog: AlertDialog {
|
||||
private val _context: MainActivity;
|
||||
|
||||
private lateinit var _button_import_zip: BigButton;
|
||||
private lateinit var _button_import_ezip: BigButton;
|
||||
private lateinit var _button_import_txt: BigButton;
|
||||
private lateinit var _button_import_newpipe_subs: BigButton;
|
||||
private lateinit var _button_close: Button;
|
||||
|
||||
|
||||
constructor(context: MainActivity): super(context) {
|
||||
_context = context;
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(LayoutInflater.from(context).inflate(R.layout.dialog_import_options, null));
|
||||
_button_import_zip = findViewById(R.id.button_import_zip);
|
||||
_button_import_ezip = findViewById(R.id.button_import_ezip);
|
||||
_button_import_txt = findViewById(R.id.button_import_txt);
|
||||
_button_import_newpipe_subs = findViewById(R.id.button_import_newpipe_subs);
|
||||
_button_close = findViewById(R.id.button_cancel);
|
||||
|
||||
_button_import_zip.onClick.subscribe {
|
||||
dismiss();
|
||||
StateApp.instance.requestFileReadAccess(_context, null, "application/zip") {
|
||||
val zipBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
|
||||
StateBackup.importZipBytes(_context, StateApp.instance.scope, zipBytes);
|
||||
};
|
||||
}
|
||||
_button_import_ezip.setOnClickListener {
|
||||
|
||||
}
|
||||
_button_import_txt.onClick.subscribe {
|
||||
dismiss();
|
||||
StateApp.instance.requestFileReadAccess(_context, null, "text/plain") {
|
||||
val txtBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
|
||||
val txt = String(txtBytes);
|
||||
StateBackup.importTxt(_context, txt);
|
||||
};
|
||||
}
|
||||
_button_import_newpipe_subs.onClick.subscribe {
|
||||
dismiss();
|
||||
StateApp.instance.requestFileReadAccess(_context, null, "application/json") {
|
||||
val jsonBytes = it?.readBytes(context) ?: return@requestFileReadAccess;
|
||||
val json = String(jsonBytes);
|
||||
StateBackup.importNewPipeSubs(_context, json);
|
||||
};
|
||||
};
|
||||
_button_close.setOnClickListener {
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
super.dismiss();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +1,17 @@
|
|||
package com.futo.platformplayer.downloads
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.arthenica.ffmpegkit.FFmpegKit
|
||||
import com.arthenica.ffmpegkit.ReturnCode
|
||||
import com.arthenica.ffmpegkit.StatisticsCallback
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
|
@ -18,22 +24,28 @@ import com.futo.platformplayer.hasAnySource
|
|||
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||
import com.futo.platformplayer.helpers.VideoHelper
|
||||
import com.futo.platformplayer.isDownloadable
|
||||
import com.futo.platformplayer.parsers.HLS
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||
import com.futo.platformplayer.toHumanBitrate
|
||||
import com.futo.platformplayer.toHumanBytesSpeed
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
import java.util.concurrent.ForkJoinTask
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class VideoDownload {
|
||||
|
@ -137,7 +149,7 @@ class VideoDownload {
|
|||
return items.joinToString(" • ");
|
||||
}
|
||||
|
||||
suspend fun prepare() {
|
||||
suspend fun prepare(client: ManagedHttpClient) {
|
||||
Logger.i(TAG, "VideoDownload Prepare [${name}]");
|
||||
if(video == null && videoDetails == null)
|
||||
throw IllegalStateException("Missing information for download to complete");
|
||||
|
@ -157,24 +169,65 @@ class VideoDownload {
|
|||
|
||||
videoDetails = SerializedPlatformVideoDetails.fromVideo(original, if (subtitleSource != null) listOf(subtitleSource!!) else listOf());
|
||||
if(videoSource == null && targetPixelCount != null) {
|
||||
val vsource = VideoHelper.selectBestVideoSource(videoDetails!!.video, targetPixelCount!!.toInt(), arrayOf())
|
||||
val videoSources = arrayListOf<IVideoSource>()
|
||||
for (source in original.video.videoSources) {
|
||||
if (source is IHLSManifestSource) {
|
||||
try {
|
||||
val playlistResponse = client.get(source.url)
|
||||
if (playlistResponse.isOk) {
|
||||
val playlistContent = playlistResponse.body?.string()
|
||||
if (playlistContent != null) {
|
||||
videoSources.addAll(HLS.parseAndGetVideoSources(source, playlistContent, source.url))
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.i(TAG, "Failed to get HLS video sources", e)
|
||||
}
|
||||
} else {
|
||||
videoSources.add(source)
|
||||
}
|
||||
}
|
||||
|
||||
val vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
|
||||
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
||||
if(vsource != null) {
|
||||
if (vsource is IVideoUrlSource)
|
||||
videoSource = VideoUrlSource.fromUrlSource(vsource);
|
||||
videoSource = VideoUrlSource.fromUrlSource(vsource)
|
||||
else
|
||||
throw DownloadException("Video source is not supported for downloading (yet)", false);
|
||||
}
|
||||
}
|
||||
|
||||
if(audioSource == null && targetBitrate != null) {
|
||||
val asource = VideoHelper.selectBestAudioSource(videoDetails!!.video, arrayOf(), null, targetPixelCount)
|
||||
val audioSources = arrayListOf<IAudioSource>()
|
||||
val video = original.video
|
||||
if (video is VideoUnMuxedSourceDescriptor) {
|
||||
for (source in video.audioSources) {
|
||||
if (source is IHLSManifestSource) {
|
||||
try {
|
||||
val playlistResponse = client.get(source.url)
|
||||
if (playlistResponse.isOk) {
|
||||
val playlistContent = playlistResponse.body?.string()
|
||||
if (playlistContent != null) {
|
||||
audioSources.addAll(HLS.parseAndGetAudioSources(source, playlistContent, source.url))
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.i(TAG, "Failed to get HLS audio sources", e)
|
||||
}
|
||||
} else {
|
||||
audioSources.add(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
||||
?: if(videoSource != null ) null
|
||||
else throw DownloadException("Could not find a valid video or audio source for download")
|
||||
if(asource == null)
|
||||
audioSource = null;
|
||||
else if(asource is IAudioUrlSource)
|
||||
audioSource = AudioUrlSource.fromUrlSource(asource);
|
||||
audioSource = AudioUrlSource.fromUrlSource(asource)
|
||||
else
|
||||
throw DownloadException("Audio source is not supported for downloading (yet)", false);
|
||||
}
|
||||
|
@ -183,7 +236,8 @@ class VideoDownload {
|
|||
throw DownloadException("No valid sources found for video/audio");
|
||||
}
|
||||
}
|
||||
suspend fun download(client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
|
||||
|
||||
suspend fun download(context: Context, client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
|
||||
Logger.i(TAG, "VideoDownload Download [${name}]");
|
||||
if(videoDetails == null || (videoSource == null && audioSource == null))
|
||||
throw IllegalStateException("Missing information for download to complete");
|
||||
|
@ -199,7 +253,7 @@ class VideoDownload {
|
|||
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
||||
}
|
||||
if(audioSource != null) {
|
||||
audioFileName = "${videoDetails!!.id.value!!} [${audioSource!!.bitrate}].${audioContainerToExtension(audioSource!!.container)}".sanitizeFileName();
|
||||
audioFileName = "${videoDetails!!.id.value!!} [${audioSource!!.language}-${audioSource!!.bitrate}].${audioContainerToExtension(audioSource!!.container)}".sanitizeFileName();
|
||||
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
||||
}
|
||||
if(subtitleSource != null) {
|
||||
|
@ -217,7 +271,8 @@ class VideoDownload {
|
|||
if(videoSource != null) {
|
||||
sourcesToDownload.add(async {
|
||||
Logger.i(TAG, "Started downloading video");
|
||||
videoFileSize = downloadSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!)) { length, totalRead, speed ->
|
||||
|
||||
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
|
||||
synchronized(progressLock) {
|
||||
lastVideoLength = length;
|
||||
lastVideoRead = totalRead;
|
||||
|
@ -235,12 +290,18 @@ class VideoDownload {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
videoFileSize = when (videoSource!!.container) {
|
||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||
else -> downloadFileSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||
}
|
||||
});
|
||||
}
|
||||
if(audioSource != null) {
|
||||
sourcesToDownload.add(async {
|
||||
Logger.i(TAG, "Started downloading audio");
|
||||
audioFileSize = downloadSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!)) { length, totalRead, speed ->
|
||||
|
||||
val progressCallback = { length: Long, totalRead: Long, speed: Long ->
|
||||
synchronized(progressLock) {
|
||||
lastAudioLength = length;
|
||||
lastAudioRead = totalRead;
|
||||
|
@ -258,6 +319,11 @@ class VideoDownload {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
audioFileSize = when (audioSource!!.container) {
|
||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||
else -> downloadFileSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||
}
|
||||
});
|
||||
}
|
||||
if (subtitleSource != null) {
|
||||
|
@ -279,7 +345,105 @@ class VideoDownload {
|
|||
throw ex;
|
||||
}
|
||||
}
|
||||
private fun downloadSource(name: String, client: ManagedHttpClient, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||
|
||||
private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||
if(targetFile.exists())
|
||||
targetFile.delete();
|
||||
|
||||
var downloadedTotalLength = 0L
|
||||
|
||||
val segmentFiles = arrayListOf<File>()
|
||||
try {
|
||||
val response = client.get(hlsUrl)
|
||||
check(response.isOk) { "Failed to get variant playlist: ${response.code}" }
|
||||
|
||||
val vpContent = response.body?.string()
|
||||
?: throw Exception("Variant playlist content is empty")
|
||||
|
||||
val variantPlaylist = HLS.parseVariantPlaylist(vpContent, hlsUrl)
|
||||
variantPlaylist.segments.forEachIndexed { index, segment ->
|
||||
if (segment !is HLS.MediaSegment) {
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Download '$name' segment $index Sequential");
|
||||
val segmentFile = File(context.cacheDir, "segment-${UUID.randomUUID()}")
|
||||
segmentFiles.add(segmentFile)
|
||||
|
||||
val segmentLength = downloadSource_Sequential(client, segmentFile.outputStream(), segment.uri) { segmentLength, totalRead, lastSpeed ->
|
||||
val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
|
||||
val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
|
||||
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
|
||||
}
|
||||
|
||||
downloadedTotalLength += segmentLength
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Combining segments into $targetFile");
|
||||
combineSegments(context, segmentFiles, targetFile)
|
||||
|
||||
Logger.i(TAG, "${name} downloadSource Finished");
|
||||
}
|
||||
catch(ioex: IOException) {
|
||||
if(targetFile.exists() ?: false)
|
||||
targetFile.delete();
|
||||
if(ioex.message?.contains("ENOSPC") ?: false)
|
||||
throw Exception("Not enough space on device", ioex);
|
||||
else
|
||||
throw ioex;
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
if(targetFile.exists() ?: false)
|
||||
targetFile.delete();
|
||||
throw ex;
|
||||
}
|
||||
finally {
|
||||
for (segmentFile in segmentFiles) {
|
||||
segmentFile.delete()
|
||||
}
|
||||
}
|
||||
return downloadedTotalLength;
|
||||
}
|
||||
|
||||
private suspend fun combineSegments(context: Context, segmentFiles: List<File>, targetFile: File) = withContext(Dispatchers.IO) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val fileList = File(context.cacheDir, "fileList-${UUID.randomUUID()}.txt")
|
||||
fileList.writeText(segmentFiles.joinToString("\n") { "file '${it.absolutePath}'" })
|
||||
|
||||
val cmd = "-f concat -safe 0 -i \"${fileList.absolutePath}\" -c copy \"${targetFile.absolutePath}\""
|
||||
|
||||
val statisticsCallback = StatisticsCallback { statistics ->
|
||||
//TODO: Show progress?
|
||||
}
|
||||
|
||||
val executorService = Executors.newSingleThreadExecutor()
|
||||
val session = FFmpegKit.executeAsync(cmd,
|
||||
{ session ->
|
||||
if (ReturnCode.isSuccess(session.returnCode)) {
|
||||
fileList.delete()
|
||||
continuation.resumeWith(Result.success(Unit))
|
||||
} else {
|
||||
val errorMessage = if (ReturnCode.isCancel(session.returnCode)) {
|
||||
"Command cancelled"
|
||||
} else {
|
||||
"Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}"
|
||||
}
|
||||
fileList.delete()
|
||||
continuation.resumeWithException(RuntimeException(errorMessage))
|
||||
}
|
||||
},
|
||||
{ Logger.v(TAG, it.message) },
|
||||
statisticsCallback,
|
||||
executorService
|
||||
)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
session.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadFileSource(name: String, client: ManagedHttpClient, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||
if(targetFile.exists())
|
||||
targetFile.delete();
|
||||
|
||||
|
@ -472,8 +636,10 @@ class VideoDownload {
|
|||
val expectedFile = File(videoFilePath!!);
|
||||
if(!expectedFile.exists())
|
||||
throw IllegalStateException("Video file missing after download");
|
||||
if(expectedFile.length() != videoFileSize)
|
||||
throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
|
||||
if (videoSource?.container != "application/vnd.apple.mpegurl") {
|
||||
if (expectedFile.length() != videoFileSize)
|
||||
throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
|
||||
}
|
||||
}
|
||||
if(audioSource != null) {
|
||||
if(audioFilePath == null)
|
||||
|
@ -481,8 +647,10 @@ class VideoDownload {
|
|||
val expectedFile = File(audioFilePath!!);
|
||||
if(!expectedFile.exists())
|
||||
throw IllegalStateException("Audio file missing after download");
|
||||
if(expectedFile.length() != audioFileSize)
|
||||
throw IllegalStateException("Expected size [${audioFileSize}], but found ${expectedFile.length()}");
|
||||
if (audioSource?.container != "application/vnd.apple.mpegurl") {
|
||||
if (expectedFile.length() != audioFileSize)
|
||||
throw IllegalStateException("Expected size [${audioFileSize}], but found ${expectedFile.length()}");
|
||||
}
|
||||
}
|
||||
if(subtitleSource != null) {
|
||||
if(subtitleFilePath == null)
|
||||
|
@ -560,7 +728,7 @@ class VideoDownload {
|
|||
const val GROUP_PLAYLIST = "Playlist";
|
||||
|
||||
fun videoContainerToExtension(container: String): String? {
|
||||
if (container.contains("video/mp4"))
|
||||
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
||||
return "mp4";
|
||||
else if (container.contains("application/x-mpegURL"))
|
||||
return "m3u8";
|
||||
|
@ -585,6 +753,8 @@ class VideoDownload {
|
|||
return "mp3";
|
||||
else if (container.contains("audio/webm"))
|
||||
return "webma";
|
||||
else if (container == "application/vnd.apple.mpegurl")
|
||||
return "mp4";
|
||||
else
|
||||
return "audio";
|
||||
}
|
||||
|
|
|
@ -351,6 +351,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||
ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate<PlaylistsFragment>() }),
|
||||
ButtonDefinition(5, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate<HistoryFragment>() }),
|
||||
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(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()");
|
||||
|
|
|
@ -35,6 +35,11 @@ class BuyFragment : MainFragment() {
|
|||
return view;
|
||||
}
|
||||
|
||||
override fun onDestroyMainView() {
|
||||
super.onDestroyMainView()
|
||||
_view = null
|
||||
}
|
||||
|
||||
class BuyView: LinearLayout {
|
||||
private val _fragment: BuyFragment;
|
||||
|
||||
|
|
|
@ -437,11 +437,12 @@ class ChannelFragment : MainFragment() {
|
|||
}
|
||||
|
||||
private fun setPolycentricProfileOr(url: String, or: () -> Unit) {
|
||||
setPolycentricProfile(null, animate = false);
|
||||
|
||||
val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(it.url) };
|
||||
if (cachedProfile != null) {
|
||||
setPolycentricProfile(cachedProfile, animate = false);
|
||||
} else {
|
||||
setPolycentricProfile(null, animate = false);
|
||||
or();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,322 @@
|
|||
package com.futo.platformplayer.fragment.mainactivity.main
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.PolycentricHomeActivity
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.views.adapters.CommentWithReferenceViewHolder
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||
import com.futo.polycentric.core.PublicKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.UnknownHostException
|
||||
import java.util.IdentityHashMap
|
||||
|
||||
class CommentsFragment : MainFragment() {
|
||||
override val isMainView : Boolean = true
|
||||
override val isTab: Boolean = true
|
||||
override val hasBottomBar: Boolean get() = true
|
||||
|
||||
private var _view: CommentsView? = null
|
||||
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
super.onShownWithView(parameter, isBack)
|
||||
_view?.onShown()
|
||||
}
|
||||
|
||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val view = CommentsView(this, inflater)
|
||||
_view = view
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onDestroyMainView() {
|
||||
super.onDestroyMainView()
|
||||
_view = null
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
return _view?.onBackPressed() ?: false
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
_view?.onShown()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance() = CommentsFragment().apply {}
|
||||
private const val TAG = "CommentsFragment"
|
||||
}
|
||||
|
||||
class CommentsView : FrameLayout {
|
||||
private val _fragment: CommentsFragment
|
||||
private val _recyclerComments: RecyclerView;
|
||||
private val _adapterComments: InsertedViewAdapterWithLoader<CommentWithReferenceViewHolder>;
|
||||
private val _textCommentCount: TextView
|
||||
private val _comments: ArrayList<IPlatformComment> = arrayListOf();
|
||||
private val _llmReplies: LinearLayoutManager;
|
||||
private val _spinnerSortBy: Spinner;
|
||||
private val _layoutNotLoggedIn: LinearLayout;
|
||||
private val _layoutPolycentricNotEnabled: LinearLayout;
|
||||
private val _buttonLogin: LinearLayout;
|
||||
private var _loading = false;
|
||||
private val _repliesOverlay: RepliesOverlay;
|
||||
private var _repliesAnimator: ViewPropertyAnimator? = null;
|
||||
private val _cache: IdentityHashMap<IPlatformComment, StatePolycentric.LikesDislikesReplies> = IdentityHashMap()
|
||||
|
||||
private val _taskLoadComments = if(!isInEditMode) TaskHandler<PublicKey, List<IPlatformComment>>(
|
||||
StateApp.instance.scopeGetter, { StatePolycentric.instance.getSystemComments(context, it) })
|
||||
.success { pager -> onCommentsLoaded(pager); }
|
||||
.exception<UnknownHostException> {
|
||||
UIDialogs.toast("Failed to load comments");
|
||||
setLoading(false);
|
||||
}
|
||||
.exception<Throwable> {
|
||||
Logger.e(TAG, "Failed to load comments.", it);
|
||||
UIDialogs.toast(context, context.getString(R.string.failed_to_load_comments) + "\n" + (it.message ?: ""));
|
||||
setLoading(false);
|
||||
} else TaskHandler(IPlatformVideoDetails::class.java, StateApp.instance.scopeGetter);
|
||||
|
||||
constructor(fragment: CommentsFragment, inflater: LayoutInflater) : super(inflater.context) {
|
||||
_fragment = fragment
|
||||
inflater.inflate(R.layout.fragment_comments, this)
|
||||
|
||||
val commentHeader = findViewById<LinearLayout>(R.id.layout_header)
|
||||
(commentHeader.parent as ViewGroup).removeView(commentHeader)
|
||||
_textCommentCount = commentHeader.findViewById(R.id.text_comment_count)
|
||||
|
||||
_recyclerComments = findViewById(R.id.recycler_comments)
|
||||
_adapterComments = InsertedViewAdapterWithLoader(context, arrayListOf(commentHeader), arrayListOf(),
|
||||
childCountGetter = { _comments.size },
|
||||
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(_comments[position]); },
|
||||
childViewHolderFactory = { viewGroup, _ ->
|
||||
val holder = CommentWithReferenceViewHolder(viewGroup, _cache);
|
||||
holder.onDelete.subscribe(::onDelete);
|
||||
holder.onRepliesClick.subscribe(::onRepliesClick);
|
||||
return@InsertedViewAdapterWithLoader holder;
|
||||
}
|
||||
);
|
||||
|
||||
_spinnerSortBy = commentHeader.findViewById(R.id.spinner_sortby);
|
||||
_spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.comments_sortby_array)).also {
|
||||
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
||||
};
|
||||
_spinnerSortBy.setSelection(0);
|
||||
_spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
|
||||
if (_spinnerSortBy.selectedItemPosition == 0) {
|
||||
_comments.sortByDescending { it.date!! }
|
||||
} else if (_spinnerSortBy.selectedItemPosition == 1) {
|
||||
_comments.sortBy { it.date!! }
|
||||
}
|
||||
|
||||
_adapterComments.notifyDataSetChanged()
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||
}
|
||||
|
||||
_llmReplies = LinearLayoutManager(context);
|
||||
_recyclerComments.layoutManager = _llmReplies;
|
||||
_recyclerComments.adapter = _adapterComments;
|
||||
updateCommentCountString();
|
||||
|
||||
_layoutNotLoggedIn = findViewById(R.id.layout_not_logged_in)
|
||||
_layoutNotLoggedIn.visibility = View.GONE
|
||||
|
||||
_layoutPolycentricNotEnabled = findViewById(R.id.layout_polycentric_disabled)
|
||||
_layoutPolycentricNotEnabled.visibility = if (!StatePolycentric.instance.enabled) View.VISIBLE else View.GONE
|
||||
|
||||
_buttonLogin = findViewById(R.id.button_login)
|
||||
_buttonLogin.setOnClickListener {
|
||||
context.startActivity(Intent(context, PolycentricHomeActivity::class.java));
|
||||
}
|
||||
|
||||
_repliesOverlay = findViewById(R.id.replies_overlay);
|
||||
_repliesOverlay.onClose.subscribe { setRepliesOverlayVisible(isVisible = false, animate = true); };
|
||||
}
|
||||
|
||||
private fun onDelete(comment: IPlatformComment) {
|
||||
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete this comment?", {
|
||||
val processHandle = StatePolycentric.instance.processHandle ?: return@showConfirmationDialog
|
||||
if (comment !is PolycentricPlatformComment) {
|
||||
return@showConfirmationDialog
|
||||
}
|
||||
|
||||
val index = _comments.indexOf(comment)
|
||||
if (index != -1) {
|
||||
_comments.removeAt(index)
|
||||
_adapterComments.notifyItemRemoved(_adapterComments.childToParentPosition(index))
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
processHandle.delete(comment.eventPointer.process, comment.eventPointer.logicalClock)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to delete event.", e);
|
||||
return@launch
|
||||
}
|
||||
|
||||
try {
|
||||
Logger.i(TAG, "Started backfill");
|
||||
processHandle.fullyBackfillServersAnnounceExceptions();
|
||||
Logger.i(TAG, "Finished backfill");
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to fully backfill servers.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun onBackPressed(): Boolean {
|
||||
if (_repliesOverlay.visibility == View.VISIBLE) {
|
||||
setRepliesOverlayVisible(isVisible = false, animate = true);
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun onRepliesClick(c: IPlatformComment) {
|
||||
val replyCount = c.replyCount ?: 0;
|
||||
var metadata = "";
|
||||
if (replyCount > 0) {
|
||||
metadata += "$replyCount " + context.getString(R.string.replies);
|
||||
}
|
||||
|
||||
if (c is PolycentricPlatformComment) {
|
||||
_repliesOverlay.load(false, metadata, c.contextUrl, c.reference, c,
|
||||
{ StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) },
|
||||
{ newComment ->
|
||||
synchronized(_cache) {
|
||||
_cache.remove(c)
|
||||
}
|
||||
|
||||
val newCommentIndex = if (_spinnerSortBy.selectedItemPosition == 0) {
|
||||
_comments.indexOfFirst { it.date!! < newComment.date!! }.takeIf { it != -1 } ?: _comments.size
|
||||
} else {
|
||||
_comments.indexOfFirst { it.date!! > newComment.date!! }.takeIf { it != -1 } ?: _comments.size
|
||||
}
|
||||
|
||||
_comments.add(newCommentIndex, newComment)
|
||||
_adapterComments.notifyItemInserted(_adapterComments.childToParentPosition(newCommentIndex))
|
||||
});
|
||||
} else {
|
||||
_repliesOverlay.load(true, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
|
||||
}
|
||||
|
||||
setRepliesOverlayVisible(isVisible = true, animate = true);
|
||||
}
|
||||
|
||||
private fun setRepliesOverlayVisible(isVisible: Boolean, animate: Boolean) {
|
||||
val desiredVisibility = if (isVisible) View.VISIBLE else View.GONE
|
||||
if (_repliesOverlay.visibility == desiredVisibility) {
|
||||
return;
|
||||
}
|
||||
|
||||
_repliesAnimator?.cancel();
|
||||
|
||||
if (isVisible) {
|
||||
_repliesOverlay.visibility = View.VISIBLE;
|
||||
|
||||
if (animate) {
|
||||
_repliesOverlay.translationY = _repliesOverlay.height.toFloat();
|
||||
|
||||
_repliesAnimator = _repliesOverlay.animate()
|
||||
.setDuration(300)
|
||||
.translationY(0f)
|
||||
.withEndAction {
|
||||
_repliesAnimator = null;
|
||||
}.apply { start() };
|
||||
}
|
||||
} else {
|
||||
if (animate) {
|
||||
_repliesOverlay.translationY = 0f;
|
||||
|
||||
_repliesAnimator = _repliesOverlay.animate()
|
||||
.setDuration(300)
|
||||
.translationY(_repliesOverlay.height.toFloat())
|
||||
.withEndAction {
|
||||
_repliesOverlay.visibility = GONE;
|
||||
_repliesAnimator = null;
|
||||
}.apply { start(); }
|
||||
} else {
|
||||
_repliesOverlay.visibility = View.GONE;
|
||||
_repliesOverlay.translationY = _repliesOverlay.height.toFloat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCommentCountString() {
|
||||
_textCommentCount.text = context.getString(R.string.these_are_all_commentcount_comments_you_have_made_in_grayjay).replace("{commentCount}", _comments.size.toString())
|
||||
}
|
||||
|
||||
private fun setLoading(loading: Boolean) {
|
||||
if (_loading == loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
_loading = loading;
|
||||
_adapterComments.setLoading(loading);
|
||||
}
|
||||
|
||||
private fun fetchComments() {
|
||||
val system = StatePolycentric.instance.processHandle?.system ?: return
|
||||
_comments.clear()
|
||||
_adapterComments.notifyDataSetChanged()
|
||||
setLoading(true)
|
||||
_taskLoadComments.run(system)
|
||||
}
|
||||
|
||||
private fun onCommentsLoaded(comments: List<IPlatformComment>) {
|
||||
setLoading(false)
|
||||
_comments.addAll(comments)
|
||||
|
||||
if (_spinnerSortBy.selectedItemPosition == 0) {
|
||||
_comments.sortByDescending { it.date!! }
|
||||
} else if (_spinnerSortBy.selectedItemPosition == 1) {
|
||||
_comments.sortBy { it.date!! }
|
||||
}
|
||||
|
||||
_adapterComments.notifyDataSetChanged()
|
||||
updateCommentCountString()
|
||||
}
|
||||
|
||||
fun onShown() {
|
||||
_layoutPolycentricNotEnabled.visibility = if (!StatePolycentric.instance.enabled) View.VISIBLE else View.GONE
|
||||
|
||||
val processHandle = StatePolycentric.instance.processHandle
|
||||
if (processHandle != null) {
|
||||
_layoutNotLoggedIn.visibility = View.GONE
|
||||
_recyclerComments.visibility = View.VISIBLE
|
||||
fetchComments()
|
||||
} else {
|
||||
_layoutNotLoggedIn.visibility = View.VISIBLE
|
||||
_recyclerComments.visibility= View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,8 +6,10 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Spinner
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
|
@ -21,9 +23,13 @@ class CreatorsFragment : MainFragment() {
|
|||
|
||||
private var _spinnerSortBy: Spinner? = null;
|
||||
private var _overlayContainer: FrameLayout? = null;
|
||||
private var _containerSearch: FrameLayout? = null;
|
||||
private var _editSearch: EditText? = null;
|
||||
|
||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val view = inflater.inflate(R.layout.fragment_creators, container, false);
|
||||
_containerSearch = view.findViewById(R.id.container_search);
|
||||
_editSearch = view.findViewById(R.id.edit_search);
|
||||
|
||||
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
|
||||
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
|
||||
|
@ -44,6 +50,10 @@ class CreatorsFragment : MainFragment() {
|
|||
|
||||
_spinnerSortBy = spinnerSortBy;
|
||||
|
||||
_editSearch?.addTextChangedListener {
|
||||
adapter.query = it.toString();
|
||||
}
|
||||
|
||||
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_subscriptions);
|
||||
recyclerView.adapter = adapter;
|
||||
recyclerView.layoutManager = LinearLayoutManager(view.context);
|
||||
|
@ -54,6 +64,8 @@ class CreatorsFragment : MainFragment() {
|
|||
super.onDestroyMainView();
|
||||
_spinnerSortBy = null;
|
||||
_overlayContainer = null;
|
||||
_editSearch = null;
|
||||
_containerSearch = null;
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -13,7 +13,6 @@ import androidx.recyclerview.widget.RecyclerView.LayoutManager
|
|||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPager
|
||||
import com.futo.platformplayer.api.media.structures.*
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
|
@ -21,7 +20,6 @@ import com.futo.platformplayer.constructs.TaskHandler
|
|||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||
import com.futo.platformplayer.views.others.ProgressBar
|
||||
import com.futo.platformplayer.views.others.TagsView
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
|
@ -64,6 +62,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||
val fragment: TFragment;
|
||||
|
||||
private val _scrollListener: RecyclerView.OnScrollListener;
|
||||
private var _automaticNextPageCounter = 0;
|
||||
|
||||
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>? = null) : super(inflater.context) {
|
||||
this.fragment = fragment;
|
||||
|
@ -122,7 +121,6 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||
|
||||
_toolbarContentView = findViewById(R.id.container_toolbar_content);
|
||||
|
||||
var filteredNextPageCounter = 0;
|
||||
_nextPageHandler = TaskHandler<TPager, List<TResult>>({fragment.lifecycleScope}, {
|
||||
if (it is IAsyncPager<*>)
|
||||
it.nextPageAsync();
|
||||
|
@ -142,15 +140,8 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||
val filteredResults = filterResults(it);
|
||||
recyclerData.results.addAll(filteredResults);
|
||||
recyclerData.resultsUnfiltered.addAll(it);
|
||||
if(filteredResults.isEmpty()) {
|
||||
filteredNextPageCounter++
|
||||
if(filteredNextPageCounter <= 4)
|
||||
loadNextPage()
|
||||
}
|
||||
else {
|
||||
filteredNextPageCounter = 0;
|
||||
recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size);
|
||||
}
|
||||
recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size);
|
||||
ensureEnoughContentVisible(filteredResults)
|
||||
}.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load next page.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
||||
|
@ -170,8 +161,10 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||
|
||||
val visibleItemCount = _recyclerResults.childCount;
|
||||
val firstVisibleItem = recyclerData.layoutManager.findFirstVisibleItemPosition();
|
||||
//Logger.i(TAG, "onScrolled loadNextPage visibleItemCount=$visibleItemCount firstVisibleItem=$visibleItemCount")
|
||||
|
||||
if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= recyclerData.results.size && firstVisibleItem > 0) {
|
||||
//Logger.i(TAG, "loadNextPage(): firstVisibleItem=$firstVisibleItem visibleItemCount=$visibleItemCount visibleThreshold=$visibleThreshold _results.size=${_results.size}")
|
||||
//Logger.i(TAG, "onScrolled loadNextPage(): firstVisibleItem=$firstVisibleItem visibleItemCount=$visibleItemCount visibleThreshold=$visibleThreshold recyclerData.results.size=${recyclerData.results.size}")
|
||||
loadNextPage();
|
||||
}
|
||||
}
|
||||
|
@ -180,6 +173,33 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||
_recyclerResults.addOnScrollListener(_scrollListener);
|
||||
}
|
||||
|
||||
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
|
||||
val canScroll = if (recyclerData.results.isEmpty()) false else {
|
||||
val layoutManager = recyclerData.layoutManager
|
||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||
|
||||
if (firstVisibleItemPosition != RecyclerView.NO_POSITION) {
|
||||
val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition)
|
||||
val itemHeight = firstVisibleView?.height ?: 0
|
||||
val occupiedSpace = recyclerData.results.size * itemHeight
|
||||
val recyclerViewHeight = _recyclerResults.height
|
||||
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage occupiedSpace=$occupiedSpace recyclerViewHeight=$recyclerViewHeight")
|
||||
occupiedSpace >= recyclerViewHeight
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
}
|
||||
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter")
|
||||
if (!canScroll || filteredResults.isEmpty()) {
|
||||
_automaticNextPageCounter++
|
||||
if(_automaticNextPageCounter <= 4)
|
||||
loadNextPage()
|
||||
} else {
|
||||
_automaticNextPageCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected fun setTextCentered(text: String?) {
|
||||
_textCentered.text = text;
|
||||
}
|
||||
|
@ -370,6 +390,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||
recyclerData.resultsUnfiltered.addAll(toAdd);
|
||||
recyclerData.adapter.notifyDataSetChanged();
|
||||
recyclerData.loadedFeedStyle = feedStyle;
|
||||
ensureEnoughContentVisible(filteredResults)
|
||||
}
|
||||
|
||||
private fun detachPagerEvents() {
|
||||
|
|
|
@ -224,7 +224,7 @@ class PostDetailFragment : MainFragment {
|
|||
updateCommentType(false);
|
||||
};
|
||||
|
||||
_commentsList.onClick.subscribe { c ->
|
||||
_commentsList.onRepliesClick.subscribe { c ->
|
||||
val replyCount = c.replyCount ?: 0;
|
||||
var metadata = "";
|
||||
if (replyCount > 0) {
|
||||
|
@ -233,7 +233,7 @@ class PostDetailFragment : MainFragment {
|
|||
|
||||
if (c is PolycentricPlatformComment) {
|
||||
var parentComment: PolycentricPlatformComment = c;
|
||||
_repliesOverlay.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference,
|
||||
_repliesOverlay.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference, c,
|
||||
{ StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) },
|
||||
{
|
||||
val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1);
|
||||
|
@ -241,7 +241,7 @@ class PostDetailFragment : MainFragment {
|
|||
parentComment = newComment;
|
||||
});
|
||||
} else {
|
||||
_repliesOverlay.load(_toggleCommentType.value, metadata, null, null, { StatePlatform.instance.getSubComments(c) });
|
||||
_repliesOverlay.load(_toggleCommentType.value, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
|
||||
}
|
||||
|
||||
setRepliesOverlayVisible(isVisible = true, animate = true);
|
||||
|
|
|
@ -37,7 +37,6 @@ import com.futo.platformplayer.api.media.LiveChatManager
|
|||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
|
||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
|
||||
import com.futo.platformplayer.api.media.models.chapters.ChapterType
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
|
@ -52,7 +51,6 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
|||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.casting.CastConnectionState
|
||||
|
@ -60,7 +58,6 @@ import com.futo.platformplayer.casting.StateCasting
|
|||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||
import com.futo.platformplayer.downloads.VideoLocal
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptAgeException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||
|
@ -110,7 +107,6 @@ import java.time.OffsetDateTime
|
|||
import kotlin.collections.ArrayList
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToLong
|
||||
import kotlin.streams.toList
|
||||
|
||||
|
||||
class VideoDetailView : ConstraintLayout {
|
||||
|
@ -580,7 +576,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
_container_content_current = _container_content_main;
|
||||
|
||||
_commentsList.onClick.subscribe { c ->
|
||||
_commentsList.onRepliesClick.subscribe { c ->
|
||||
val replyCount = c.replyCount ?: 0;
|
||||
var metadata = "";
|
||||
if (replyCount > 0) {
|
||||
|
@ -589,7 +585,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
if (c is PolycentricPlatformComment) {
|
||||
var parentComment: PolycentricPlatformComment = c;
|
||||
_container_content_replies.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference,
|
||||
_container_content_replies.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference, c,
|
||||
{ StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) },
|
||||
{
|
||||
val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1);
|
||||
|
@ -597,7 +593,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
parentComment = newComment;
|
||||
});
|
||||
} else {
|
||||
_container_content_replies.load(_toggleCommentType.value, metadata, null, null, { StatePlatform.instance.getSubComments(c) });
|
||||
_container_content_replies.load(_toggleCommentType.value, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
|
||||
}
|
||||
switchContentView(_container_content_replies);
|
||||
};
|
||||
|
@ -1132,7 +1128,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
_player.setMetadata(video.name, video.author.name);
|
||||
|
||||
_toggleCommentType.setValue(Settings.instance.comments.defaultCommentSection == 1, false);
|
||||
_toggleCommentType.setValue(!Settings.instance.other.polycentricEnabled || Settings.instance.comments.defaultCommentSection == 1, false);
|
||||
updateCommentType(true);
|
||||
|
||||
//UI
|
||||
|
|
|
@ -3,8 +3,11 @@ package com.futo.platformplayer.helpers
|
|||
import android.net.Uri
|
||||
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.HLSManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||
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.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
|
@ -20,11 +23,23 @@ import com.google.android.exoplayer2.upstream.ResolvingDataSource
|
|||
class VideoHelper {
|
||||
companion object {
|
||||
|
||||
fun isDownloadable(detail: IPlatformVideoDetails) =
|
||||
(detail.video.videoSources.any { isDownloadable(it) }) ||
|
||||
(if (detail is VideoUnMuxedSourceDescriptor) detail.audioSources.any { isDownloadable(it) } else false);
|
||||
fun isDownloadable(source: IVideoSource) = source is IVideoUrlSource;
|
||||
fun isDownloadable(source: IAudioSource) = source is IAudioUrlSource;
|
||||
fun isDownloadable(detail: IPlatformVideoDetails): Boolean {
|
||||
if (detail.video.videoSources.any { isDownloadable(it) }) {
|
||||
return true
|
||||
}
|
||||
|
||||
val descriptor = detail.video
|
||||
if (descriptor is VideoUnMuxedSourceDescriptor) {
|
||||
if (descriptor.audioSources.any { isDownloadable(it) }) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun isDownloadable(source: IVideoSource) = source is IVideoUrlSource || source is IHLSManifestSource;
|
||||
fun isDownloadable(source: IAudioSource) = source is IAudioUrlSource || source is IHLSManifestAudioSource;
|
||||
|
||||
fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers);
|
||||
fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? {
|
||||
|
|
|
@ -1,8 +1,22 @@
|
|||
package com.futo.platformplayer.parsers
|
||||
|
||||
import android.view.View
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantSubtitleUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.toYesNo
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||
import com.futo.platformplayer.yesNoToBoolean
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.URI
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
@ -85,6 +99,48 @@ class HLS {
|
|||
return VariantPlaylist(version, targetDuration, mediaSequence, discontinuitySequence, programDateTime, playlistType, streamInfo, segments)
|
||||
}
|
||||
|
||||
fun parseAndGetVideoSources(source: Any, content: String, url: String): List<HLSVariantVideoUrlSource> {
|
||||
val masterPlaylist: MasterPlaylist
|
||||
try {
|
||||
masterPlaylist = parseMasterPlaylist(content, url)
|
||||
return masterPlaylist.getVideoSources()
|
||||
} catch (e: Throwable) {
|
||||
if (content.lines().any { it.startsWith("#EXTINF:") }) {
|
||||
return if (source is IHLSManifestSource) {
|
||||
listOf(HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, url))
|
||||
} else if (source is IHLSManifestAudioSource) {
|
||||
listOf()
|
||||
} else {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun parseAndGetAudioSources(source: Any, content: String, url: String): List<HLSVariantAudioUrlSource> {
|
||||
val masterPlaylist: MasterPlaylist
|
||||
try {
|
||||
masterPlaylist = parseMasterPlaylist(content, url)
|
||||
return masterPlaylist.getAudioSources()
|
||||
} catch (e: Throwable) {
|
||||
if (content.lines().any { it.startsWith("#EXTINF:") }) {
|
||||
return if (source is IHLSManifestSource) {
|
||||
listOf()
|
||||
} else if (source is IHLSManifestAudioSource) {
|
||||
listOf(HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, url))
|
||||
} else {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: getSubtitleSources
|
||||
|
||||
private fun resolveUrl(baseUrl: String, url: String): String {
|
||||
val baseUri = URI(baseUrl)
|
||||
val urlUri = URI(url)
|
||||
|
@ -269,6 +325,49 @@ class HLS {
|
|||
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
fun getVideoSources(): List<HLSVariantVideoUrlSource> {
|
||||
return variantPlaylistsRefs.map {
|
||||
var width: Int? = null
|
||||
var height: Int? = null
|
||||
val resolutionTokens = it.streamInfo.resolution?.split('x')
|
||||
if (resolutionTokens?.isNotEmpty() == true) {
|
||||
width = resolutionTokens[0].toIntOrNull()
|
||||
height = resolutionTokens[1].toIntOrNull()
|
||||
}
|
||||
|
||||
val suffix = listOf(it.streamInfo.video, it.streamInfo.codecs).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
||||
HLSVariantVideoUrlSource(suffix, width ?: 0, height ?: 0, "application/vnd.apple.mpegurl", it.streamInfo.codecs ?: "", it.streamInfo.bandwidth, 0, false, it.url)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAudioSources(): List<HLSVariantAudioUrlSource> {
|
||||
return mediaRenditions.mapNotNull {
|
||||
if (it.uri == null) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
||||
return@mapNotNull when (it.type) {
|
||||
"AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, it.uri)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getSubtitleSources(): List<HLSVariantSubtitleUrlSource> {
|
||||
return mediaRenditions.mapNotNull {
|
||||
if (it.uri == null) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
||||
return@mapNotNull when (it.type) {
|
||||
"SUBTITLE" -> HLSVariantSubtitleUrlSource(it.name?.ifEmpty { "Subtitle (${suffix})" } ?: "Subtitle (${suffix})", it.uri, "application/vnd.apple.mpegurl")
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class VariantPlaylistReference(val url: String, val streamInfo: StreamInfo) {
|
||||
|
|
|
@ -7,9 +7,9 @@ import com.futo.platformplayer.constructs.BatchedTaskHandler
|
|||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.getNowDiffSeconds
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.resolveChannelUrl
|
||||
import com.futo.platformplayer.resolveChannelUrls
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.stores.CachedPolycentricProfileStorage
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.google.protobuf.ByteString
|
||||
|
@ -19,17 +19,21 @@ import java.nio.ByteBuffer
|
|||
import java.time.OffsetDateTime
|
||||
|
||||
class PolycentricCache {
|
||||
data class CachedOwnedClaims(val ownedClaims: List<OwnedClaim>?, val creationTime: OffsetDateTime = OffsetDateTime.now());
|
||||
data class CachedOwnedClaims(val ownedClaims: List<OwnedClaim>?, val creationTime: OffsetDateTime = OffsetDateTime.now()) {
|
||||
val expired get() = creationTime.getNowDiffSeconds() > CACHE_EXPIRATION_SECONDS
|
||||
}
|
||||
@Serializable
|
||||
data class CachedPolycentricProfile(val profile: PolycentricProfile?, @Serializable(with = OffsetDateTimeSerializer::class) val creationTime: OffsetDateTime = OffsetDateTime.now());
|
||||
data class CachedPolycentricProfile(val profile: PolycentricProfile?, @Serializable(with = OffsetDateTimeSerializer::class) val creationTime: OffsetDateTime = OffsetDateTime.now()) {
|
||||
val expired get() = creationTime.getNowDiffSeconds() > CACHE_EXPIRATION_SECONDS
|
||||
}
|
||||
|
||||
private val _cacheExpirationSeconds = 60 * 60 * 3;
|
||||
private val _cache = hashMapOf<PlatformID, CachedOwnedClaims>()
|
||||
private val _profileCache = hashMapOf<PublicKey, CachedPolycentricProfile>()
|
||||
private val _profileUrlCache = FragmentedStorage.get<CachedPolycentricProfileStorage>("profileUrlCache")
|
||||
private val _scope = CoroutineScope(Dispatchers.IO);
|
||||
|
||||
private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_scope, { system ->
|
||||
private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_scope,
|
||||
{ system ->
|
||||
val signedProfileEvents = ApiMethods.getQueryLatest(
|
||||
SERVER,
|
||||
system.toProto(),
|
||||
|
@ -140,7 +144,7 @@ class PolycentricCache {
|
|||
{ _, _ -> });
|
||||
|
||||
fun getCachedValidClaims(id: PlatformID, ignoreExpired: Boolean = false): CachedOwnedClaims? {
|
||||
if (id.claimType <= 0) {
|
||||
if (!StatePolycentric.instance.enabled || id.claimType <= 0) {
|
||||
return CachedOwnedClaims(null);
|
||||
}
|
||||
|
||||
|
@ -150,7 +154,7 @@ class PolycentricCache {
|
|||
return null
|
||||
}
|
||||
|
||||
if (!ignoreExpired && cached.creationTime.getNowDiffSeconds() > _cacheExpirationSeconds) {
|
||||
if (!ignoreExpired && cached.expired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -160,7 +164,7 @@ class PolycentricCache {
|
|||
|
||||
//TODO: Review all return null in this file, perhaps it should be CachedX(null) instead
|
||||
fun getValidClaimsAsync(id: PlatformID): Deferred<CachedOwnedClaims> {
|
||||
if (id.value == null || id.claimType <= 0) {
|
||||
if (!StatePolycentric.instance.enabled || id.value == null || id.claimType <= 0) {
|
||||
return _scope.async { CachedOwnedClaims(null) };
|
||||
}
|
||||
|
||||
|
@ -182,13 +186,18 @@ class PolycentricCache {
|
|||
}
|
||||
|
||||
fun getDataAsync(url: String): Deferred<ByteBuffer> {
|
||||
StatePolycentric.instance.ensureEnabled()
|
||||
return _batchTaskGetData.execute(url);
|
||||
}
|
||||
|
||||
fun getCachedProfile(url: String, ignoreExpired: Boolean = false): CachedPolycentricProfile? {
|
||||
if (!StatePolycentric.instance.enabled) {
|
||||
return CachedPolycentricProfile(null)
|
||||
}
|
||||
|
||||
synchronized (_profileCache) {
|
||||
val cached = _profileUrlCache.get(url) ?: return null;
|
||||
if (!ignoreExpired && cached.creationTime.getNowDiffSeconds() > _cacheExpirationSeconds) {
|
||||
if (!ignoreExpired && cached.expired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -197,9 +206,13 @@ class PolycentricCache {
|
|||
}
|
||||
|
||||
fun getCachedProfile(system: PublicKey, ignoreExpired: Boolean = false): CachedPolycentricProfile? {
|
||||
if (!StatePolycentric.instance.enabled) {
|
||||
return CachedPolycentricProfile(null)
|
||||
}
|
||||
|
||||
synchronized(_profileCache) {
|
||||
val cached = _profileCache[system] ?: return null;
|
||||
if (!ignoreExpired && cached.creationTime.getNowDiffSeconds() > _cacheExpirationSeconds) {
|
||||
if (!ignoreExpired && cached.expired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -208,7 +221,7 @@ class PolycentricCache {
|
|||
}
|
||||
|
||||
suspend fun getProfileAsync(id: PlatformID): CachedPolycentricProfile? {
|
||||
if (id.claimType <= 0) {
|
||||
if (!StatePolycentric.instance.enabled || id.claimType <= 0) {
|
||||
return CachedPolycentricProfile(null);
|
||||
}
|
||||
|
||||
|
@ -234,6 +247,10 @@ class PolycentricCache {
|
|||
}
|
||||
|
||||
fun getProfileAsync(system: PublicKey): Deferred<CachedPolycentricProfile?> {
|
||||
if (!StatePolycentric.instance.enabled) {
|
||||
return _scope.async { CachedPolycentricProfile(null) };
|
||||
}
|
||||
|
||||
Logger.i(TAG, "getProfileAsync (system: ${system})")
|
||||
val def = _taskGetProfile.execute(system);
|
||||
def.invokeOnCompletion {
|
||||
|
@ -281,6 +298,7 @@ class PolycentricCache {
|
|||
private const val TAG = "PolycentricCache"
|
||||
const val SERVER = "https://srv1-stg.polycentric.io"
|
||||
private var _instance: PolycentricCache? = null;
|
||||
private val CACHE_EXPIRATION_SECONDS = 60 * 60 * 3;
|
||||
|
||||
@JvmStatic
|
||||
val instance: PolycentricCache
|
||||
|
|
|
@ -162,6 +162,8 @@ class DownloadService : Service() {
|
|||
Logger.i(TAG, "doDownloading - Ending Downloads");
|
||||
stopService(this);
|
||||
}
|
||||
|
||||
|
||||
private suspend fun doDownload(download: VideoDownload) {
|
||||
if(!Settings.instance.downloads.shouldDownload())
|
||||
throw IllegalStateException("Downloading disabled on current network");
|
||||
|
@ -183,14 +185,14 @@ class DownloadService : Service() {
|
|||
|
||||
Logger.i(TAG, "Preparing [${download.name}] started");
|
||||
if(download.state == VideoDownload.State.PREPARING)
|
||||
download.prepare();
|
||||
download.prepare(_client);
|
||||
download.changeState(VideoDownload.State.DOWNLOADING);
|
||||
notifyDownload(download);
|
||||
|
||||
var lastNotifyTime: Long = 0L;
|
||||
Logger.i(TAG, "Downloading [${download.name}] started");
|
||||
//TODO: Use plugin client?
|
||||
download.download(_client) { progress ->
|
||||
download.download(applicationContext, _client) { progress ->
|
||||
download.progress = progress;
|
||||
|
||||
val currentTime = System.currentTimeMillis();
|
||||
|
|
|
@ -23,6 +23,7 @@ import com.bumptech.glide.request.target.CustomTarget
|
|||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
|
@ -49,6 +50,7 @@ class MediaPlaybackService : Service() {
|
|||
private var _mediaSession: MediaSessionCompat? = null;
|
||||
private var _hasFocus: Boolean = false;
|
||||
private var _focusRequest: AudioFocusRequest? = null;
|
||||
private var _audioFocusLossTime_ms: Long? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Logger.v(TAG, "onStartCommand");
|
||||
|
@ -335,16 +337,32 @@ class MediaPlaybackService : Service() {
|
|||
//Do not start playing on gaining audo focus
|
||||
//MediaControlReceiver.onPlayReceived.emit();
|
||||
_hasFocus = true;
|
||||
Log.i(TAG, "Audio focus gained");
|
||||
Log.i(TAG, "Audio focus gained (restartPlaybackAfterLoss = ${Settings.instance.playback.restartPlaybackAfterLoss}, _audioFocusLossTime_ms = $_audioFocusLossTime_ms)");
|
||||
|
||||
if (Settings.instance.playback.restartPlaybackAfterLoss == 1) {
|
||||
val lossTime_ms = _audioFocusLossTime_ms
|
||||
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 30) {
|
||||
MediaControlReceiver.onPlayReceived.emit()
|
||||
}
|
||||
} else if (Settings.instance.playback.restartPlaybackAfterLoss == 2) {
|
||||
val lossTime_ms = _audioFocusLossTime_ms
|
||||
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 10) {
|
||||
MediaControlReceiver.onPlayReceived.emit()
|
||||
}
|
||||
} else if (Settings.instance.playback.restartPlaybackAfterLoss == 3) {
|
||||
MediaControlReceiver.onPlayReceived.emit()
|
||||
}
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||
MediaControlReceiver.onPauseReceived.emit();
|
||||
_audioFocusLossTime_ms = System.currentTimeMillis()
|
||||
Log.i(TAG, "Audio focus transient loss");
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||
Log.i(TAG, "Audio focus transient loss, can duck");
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
_audioFocusLossTime_ms = System.currentTimeMillis()
|
||||
_hasFocus = false;
|
||||
MediaControlReceiver.onPauseReceived.emit();
|
||||
Log.i(TAG, "Audio focus lost");
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.futo.platformplayer.states
|
||||
|
||||
import android.content.Context
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
|
@ -256,9 +257,6 @@ class StateAnnouncement {
|
|||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
fun registerDidYouKnow() {
|
||||
val random = Random();
|
||||
val message: String? = when (random.nextInt(4 * 18 + 1)) {
|
||||
|
@ -294,6 +292,23 @@ class StateAnnouncement {
|
|||
}
|
||||
}
|
||||
|
||||
fun registerDefaultHandlerAnnouncement() {
|
||||
registerAnnouncement(
|
||||
"default-url-handler",
|
||||
"Allow Grayjay to open URLs",
|
||||
"Click here to allow Grayjay to open URLs",
|
||||
AnnouncementType.SESSION_RECURRING,
|
||||
null,
|
||||
null,
|
||||
"Allow"
|
||||
) {
|
||||
UIDialogs.showUrlHandlingPrompt(StateApp.instance.context) {
|
||||
instance.neverAnnouncement("default-url-handler")
|
||||
instance.onAnnouncementChanged.emit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var _instance: StateAnnouncement? = null;
|
||||
val instance: StateAnnouncement
|
||||
|
|
|
@ -218,14 +218,33 @@ class StateApp {
|
|||
return state;
|
||||
}
|
||||
|
||||
fun requestFileReadAccess(activity: IWithResultLauncher, path: Uri?, handle: (DocumentFile?)->Unit) {
|
||||
fun requestFileReadAccess(activity: IWithResultLauncher, path: Uri?, contentType: String, handle: (DocumentFile?)->Unit) {
|
||||
if(activity is Context) {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
if(path != null)
|
||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path);
|
||||
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
.or(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
intent.setType(contentType);
|
||||
activity.launchForResult(intent, 98) {
|
||||
if(it.resultCode == Activity.RESULT_OK) {
|
||||
val uri = it.data?.data;
|
||||
if(uri != null)
|
||||
handle(DocumentFile.fromSingleUri(activity, uri));
|
||||
}
|
||||
else
|
||||
UIDialogs.showDialogOk(context, R.drawable.ic_security_pred, "No access granted");
|
||||
};
|
||||
}
|
||||
}
|
||||
fun requestFileCreateAccess(activity: IWithResultLauncher, path: Uri?, contentType: String, handle: (DocumentFile?)->Unit) {
|
||||
if(activity is Context) {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT);
|
||||
if(path != null)
|
||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path);
|
||||
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
.or(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.setType(contentType);
|
||||
activity.launchForResult(intent, 98) {
|
||||
if(it.resultCode == Activity.RESULT_OK) {
|
||||
val uri = it.data?.data;
|
||||
|
@ -526,6 +545,7 @@ class StateApp {
|
|||
);
|
||||
}
|
||||
|
||||
StateAnnouncement.instance.registerDefaultHandlerAnnouncement();
|
||||
StateAnnouncement.instance.registerDidYouKnow();
|
||||
Logger.i(TAG, "MainApp Started: Finished");
|
||||
|
||||
|
|
|
@ -8,17 +8,21 @@ import com.futo.platformplayer.R
|
|||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.activities.SettingsActivity
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.copyTo
|
||||
import com.futo.platformplayer.encryption.GPasswordEncryptionProvider
|
||||
import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
|
||||
import com.futo.platformplayer.getNowDiffHours
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.readBytes
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import com.futo.platformplayer.writeBytes
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -52,15 +56,6 @@ class StateBackup {
|
|||
val secondaryBackupFile = dir.findFile("GrayjayBackup.ezip.old") ?: if(create) dir.createFile("grayjay/ezip", "GrayjayBackup.ezip.old") else null;
|
||||
return Pair(mainBackupFile, secondaryBackupFile);
|
||||
}
|
||||
/*
|
||||
private fun getAutomaticBackupFiles(): Pair<File, File> {
|
||||
val dir = StateApp.instance.getExternalRootDirectory();
|
||||
if(dir == null)
|
||||
throw IllegalStateException("Can't access external files");
|
||||
return Pair(File(dir, "GrayjayBackup.ezip"), File(dir, "GrayjayBackup.ezip.old"))
|
||||
}*/
|
||||
|
||||
|
||||
fun getAllMigrationStores(): List<ManagedStore<*>> = listOf(
|
||||
StateSubscriptions.instance.toMigrateCheck(),
|
||||
StatePlaylists.instance.toMigrateCheck()
|
||||
|
@ -192,7 +187,19 @@ class StateBackup {
|
|||
importZipBytes(context, scope, backupBytes);
|
||||
}
|
||||
|
||||
fun startExternalBackup() {
|
||||
fun saveExternalBackup(activity: IWithResultLauncher) {
|
||||
val data = export();
|
||||
if(activity is Context)
|
||||
StateApp.instance.requestFileCreateAccess(activity, null, "application/zip") {
|
||||
if(it == null) {
|
||||
UIDialogs.toast("Cancelled");
|
||||
return@requestFileCreateAccess;
|
||||
}
|
||||
it.writeBytes(activity, data.asZip());
|
||||
UIDialogs.toast("Export saved");
|
||||
};
|
||||
}
|
||||
fun shareExternalBackup() {
|
||||
val data = export();
|
||||
val now = OffsetDateTime.now();
|
||||
val exportFile = File(
|
||||
|
@ -401,6 +408,46 @@ class StateBackup {
|
|||
).withCondition { doImport } else null
|
||||
);
|
||||
}
|
||||
|
||||
fun importTxt(context: MainActivity, text: String, allowFailure: Boolean = false): Boolean {
|
||||
if(text.startsWith("@/Subscription") || text.startsWith("Subscriptions")) {
|
||||
val lines = text.split("\n").map { it.trim() }.drop(1).filter { it.isNotEmpty() };
|
||||
context.navigate(context.getFragment<ImportSubscriptionsFragment>(), lines);
|
||||
return true;
|
||||
}
|
||||
else if(allowFailure) {
|
||||
UIDialogs.showGeneralErrorDialog(context, "Unknown text header [${text}]");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
fun importNewPipeSubs(context: MainActivity, json: String) {
|
||||
val newPipeSubsParsed = JsonParser.parseString(json).asJsonObject;
|
||||
if (!newPipeSubsParsed.has("subscriptions") || !newPipeSubsParsed["subscriptions"].isJsonArray)
|
||||
UIDialogs.showGeneralErrorDialog(context, "Invalid json");
|
||||
else {
|
||||
importNewPipeSubs(context, newPipeSubsParsed);
|
||||
}
|
||||
}
|
||||
fun importNewPipeSubs(context: MainActivity, obj: JsonObject) {
|
||||
try {
|
||||
val jsonSubs = obj["subscriptions"]
|
||||
val jsonSubsArray = jsonSubs.asJsonArray;
|
||||
val jsonSubsArrayItt = jsonSubsArray.iterator();
|
||||
val subs = mutableListOf<String>()
|
||||
while(jsonSubsArrayItt.hasNext()) {
|
||||
val jsonSubObj = jsonSubsArrayItt.next().asJsonObject;
|
||||
|
||||
if(jsonSubObj.has("url"))
|
||||
subs.add(jsonSubObj["url"].asString);
|
||||
}
|
||||
|
||||
context.navigate(context.getFragment<ImportSubscriptionsFragment>(), subs);
|
||||
}
|
||||
catch(ex: Exception) {
|
||||
Logger.e("StateBackup", ex.message, ex);
|
||||
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_parse_newpipe_subscriptions), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ExportStructure(
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.futo.platformplayer.states
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
|
@ -11,17 +12,12 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.contents.PlatformContentPlaceholder
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||
import com.futo.platformplayer.api.media.structures.DedupContentPager
|
||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||
import com.futo.platformplayer.api.media.structures.PlaceholderPager
|
||||
import com.futo.platformplayer.api.media.structures.RefreshChronoContentPager
|
||||
import com.futo.platformplayer.api.media.structures.RefreshDedupContentPager
|
||||
import com.futo.platformplayer.api.media.structures.RefreshDistributionContentPager
|
||||
import com.futo.platformplayer.awaitFirstDeferred
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
|
@ -38,11 +34,11 @@ import kotlinx.coroutines.Deferred
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import userpackage.Protocol
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import kotlin.Exception
|
||||
|
||||
class StatePolycentric {
|
||||
private data class LikeDislikeEntry(val unixMilliseconds: Long, val hasLiked: Boolean, val hasDisliked: Boolean);
|
||||
|
@ -50,23 +46,55 @@ class StatePolycentric {
|
|||
var processHandle: ProcessHandle? = null; private set;
|
||||
private var _likeDislikeMap = hashMapOf<String, LikeDislikeEntry>()
|
||||
private val _activeProcessHandle = FragmentedStorage.get<StringStorage>("activeProcessHandle");
|
||||
private var _transientEnabled = true
|
||||
val enabled get() = _transientEnabled && Settings.instance.other.polycentricEnabled
|
||||
|
||||
fun load(context: Context) {
|
||||
val db = SqlLiteDbHelper(context);
|
||||
Store.initializeSqlLiteStore(db);
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
val activeProcessHandleString = _activeProcessHandle.value;
|
||||
if (activeProcessHandleString.isNotEmpty()) {
|
||||
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
||||
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
||||
try {
|
||||
val db = SqlLiteDbHelper(context);
|
||||
Store.initializeSqlLiteStore(db);
|
||||
|
||||
val activeProcessHandleString = _activeProcessHandle.value;
|
||||
if (activeProcessHandleString.isNotEmpty()) {
|
||||
try {
|
||||
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
||||
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
||||
} catch (e: Throwable) {
|
||||
db.upgradeOldSecrets(db.writableDatabase);
|
||||
|
||||
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
||||
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
||||
|
||||
Log.i(TAG, "Failed to initialize Polycentric.", e)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
_transientEnabled = false
|
||||
UIDialogs.showGeneralErrorDialog(context, "Failed to initialize Polycentric.", e);
|
||||
Log.i(TAG, "Failed to initialize Polycentric.", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureEnabled() {
|
||||
if (!enabled) {
|
||||
throw Exception("Polycentric is disabled")
|
||||
}
|
||||
}
|
||||
|
||||
fun getProcessHandles(): List<ProcessHandle> {
|
||||
if (!enabled) {
|
||||
return listOf()
|
||||
}
|
||||
|
||||
return Store.instance.getProcessSecrets().map { it.toProcessHandle(); };
|
||||
}
|
||||
|
||||
fun setProcessHandle(processHandle: ProcessHandle?) {
|
||||
ensureEnabled()
|
||||
this.processHandle = processHandle;
|
||||
|
||||
if (processHandle != null) {
|
||||
|
@ -96,20 +124,34 @@ class StatePolycentric {
|
|||
}
|
||||
|
||||
fun updateLikeMap(ref: Protocol.Reference, hasLiked: Boolean, hasDisliked: Boolean) {
|
||||
ensureEnabled()
|
||||
_likeDislikeMap[ref.toByteArray().toBase64()] = LikeDislikeEntry(System.currentTimeMillis(), hasLiked, hasDisliked);
|
||||
}
|
||||
|
||||
fun hasDisliked(ref: Protocol.Reference): Boolean {
|
||||
if (!enabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
val entry = _likeDislikeMap[ref.toByteArray().toBase64()] ?: return false;
|
||||
return entry.hasDisliked;
|
||||
}
|
||||
|
||||
fun hasLiked(ref: Protocol.Reference): Boolean {
|
||||
if (!enabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
val entry = _likeDislikeMap[ref.toByteArray().toBase64()] ?: return false;
|
||||
return entry.hasLiked;
|
||||
}
|
||||
|
||||
fun requireLogin(context: Context, text: String, action: (processHandle: ProcessHandle) -> Unit) {
|
||||
if (!enabled) {
|
||||
UIDialogs.toast(context, "Polycentric is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
val p = processHandle;
|
||||
if (p == null) {
|
||||
Logger.i(TAG, "requireLogin preventPictureInPicture.emit()");
|
||||
|
@ -127,24 +169,10 @@ class StatePolycentric {
|
|||
}
|
||||
}
|
||||
|
||||
fun getChannelContent(profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1, ignorePlugins: List<String>? = null): IPager<IPlatformContent> {
|
||||
//TODO: Currently abusing subscription concurrency for parallelism
|
||||
val concurrency = if (channelConcurrency == -1) Settings.instance.subscriptions.getSubscriptionsConcurrency() else channelConcurrency;
|
||||
val pagers = profile.ownedClaims.groupBy { it.claim.claimType }.mapNotNull {
|
||||
val url = it.value.firstOrNull()?.claim?.resolveChannelUrl() ?: return@mapNotNull null;
|
||||
if (!StatePlatform.instance.hasEnabledChannelClient(url)) {
|
||||
return@mapNotNull null;
|
||||
}
|
||||
|
||||
return@mapNotNull StatePlatform.instance.getChannelContent(url, isSubscriptionOptimized, concurrency, ignorePlugins);
|
||||
}.toTypedArray();
|
||||
|
||||
val pager = MultiChronoContentPager(pagers);
|
||||
pager.initialize();
|
||||
return DedupContentPager(pager, StatePlatform.instance.getEnabledClients().map { it.id });
|
||||
}
|
||||
|
||||
fun getChannelUrls(url: String, channelId: PlatformID? = null, cacheOnly: Boolean = false): List<String> {
|
||||
if (!enabled) {
|
||||
return listOf(url);
|
||||
}
|
||||
|
||||
var polycentricProfile: PolycentricProfile? = null;
|
||||
try {
|
||||
|
@ -172,7 +200,10 @@ class StatePolycentric {
|
|||
else
|
||||
return listOf(url);
|
||||
}
|
||||
|
||||
fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager<IPlatformContent>? {
|
||||
ensureEnabled()
|
||||
|
||||
//TODO: Currently abusing subscription concurrency for parallelism
|
||||
val concurrency = if (channelConcurrency == -1) Settings.instance.subscriptions.getSubscriptionsConcurrency() else channelConcurrency;
|
||||
val deferred = profile.ownedClaims.groupBy { it.claim.claimType }
|
||||
|
@ -212,13 +243,78 @@ class StatePolycentric {
|
|||
StatePlatform.instance.getEnabledClients().map { it.id }
|
||||
);*/
|
||||
}
|
||||
suspend fun getChannelContent(profile: PolycentricProfile): IPager<IPlatformContent> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
getChannelContent(this, profile) ?: EmptyPager();
|
||||
fun getSystemComments(context: Context, system: PublicKey): List<IPlatformComment> {
|
||||
if (!enabled) {
|
||||
return listOf()
|
||||
}
|
||||
|
||||
val dp_25 = 25.dp(context.resources)
|
||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
|
||||
val author = system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable())
|
||||
val posts = arrayListOf<PolycentricPlatformComment>()
|
||||
Store.instance.enumerateSignedEvents(system, ContentType.POST) { se ->
|
||||
val ev = se.event
|
||||
val post = Protocol.Post.parseFrom(ev.content)
|
||||
|
||||
posts.add(PolycentricPlatformComment(
|
||||
contextUrl = author,
|
||||
author = PlatformAuthorLink(
|
||||
id = PlatformID("polycentric", author, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||
name = systemState.username,
|
||||
url = author,
|
||||
thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
||||
subscribers = null
|
||||
),
|
||||
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||
rating = RatingLikeDislikes(0, 0),
|
||||
date = if (ev.unixMilliseconds != null) Instant.ofEpochMilli(ev.unixMilliseconds!!).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
||||
replyCount = 0,
|
||||
eventPointer = se.toPointer()
|
||||
))
|
||||
}
|
||||
|
||||
return posts
|
||||
}
|
||||
|
||||
data class LikesDislikesReplies(
|
||||
var likes: Long,
|
||||
var dislikes: Long,
|
||||
var replyCount: Long
|
||||
)
|
||||
|
||||
suspend fun getLikesDislikesReplies(reference: Protocol.Reference): LikesDislikesReplies {
|
||||
ensureEnabled()
|
||||
|
||||
val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null,
|
||||
null,
|
||||
listOf(
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
||||
.setFromType(ContentType.OPINION.value)
|
||||
.setValue(ByteString.copyFrom(Opinion.like.data))
|
||||
.build(),
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
||||
.setFromType(ContentType.OPINION.value)
|
||||
.setValue(ByteString.copyFrom(Opinion.dislike.data))
|
||||
.build()
|
||||
),
|
||||
listOf(
|
||||
Protocol.QueryReferencesRequestCountReferences.newBuilder()
|
||||
.setFromType(ContentType.POST.value)
|
||||
.build()
|
||||
)
|
||||
);
|
||||
|
||||
val likes = response.countsList[0];
|
||||
val dislikes = response.countsList[1];
|
||||
val replyCount = response.countsList[2];
|
||||
return LikesDislikesReplies(likes, dislikes, replyCount)
|
||||
}
|
||||
|
||||
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference): IPager<IPlatformComment> {
|
||||
if (!enabled) {
|
||||
return EmptyPager()
|
||||
}
|
||||
|
||||
val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null,
|
||||
Protocol.QueryReferencesRequestEvents.newBuilder()
|
||||
.setFromType(ContentType.POST.value)
|
||||
|
@ -284,7 +380,7 @@ class StatePolycentric {
|
|||
};
|
||||
}
|
||||
|
||||
private suspend fun mapQueryReferences(contextUrl: String, response: Protocol.QueryReferencesResponse): List<IPlatformComment> {
|
||||
private suspend fun mapQueryReferences(contextUrl: String, response: Protocol.QueryReferencesResponse): List<PolycentricPlatformComment> {
|
||||
return response.itemsList.mapNotNull {
|
||||
val sev = SignedEvent.fromProto(it.event);
|
||||
val ev = sev.event;
|
||||
|
@ -294,7 +390,6 @@ class StatePolycentric {
|
|||
|
||||
try {
|
||||
val post = Protocol.Post.parseFrom(ev.content);
|
||||
val id = ev.system.toProto().key.toByteArray().toBase64();
|
||||
val likes = it.countsList[0];
|
||||
val dislikes = it.countsList[1];
|
||||
val replies = it.countsList[2];
|
||||
|
@ -338,7 +433,7 @@ class StatePolycentric {
|
|||
rating = RatingLikeDislikes(likes, dislikes),
|
||||
date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
||||
replyCount = replies.toInt(),
|
||||
reference = sev.toPointer().toReference()
|
||||
eventPointer = sev.toPointer()
|
||||
);
|
||||
} catch (e: Throwable) {
|
||||
return@mapNotNull null;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.futo.platformplayer.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
|
@ -11,9 +12,10 @@ import android.widget.LinearLayout
|
|||
import androidx.core.view.updateLayoutParams
|
||||
import com.futo.platformplayer.R
|
||||
|
||||
class Loader : LinearLayout {
|
||||
class LoaderView : LinearLayout {
|
||||
private val _imageLoader: ImageView;
|
||||
private val _automatic: Boolean;
|
||||
private var _isWhite: Boolean;
|
||||
private val _animatable: Animatable;
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
|
@ -24,18 +26,25 @@ class Loader : LinearLayout {
|
|||
if (attrs != null) {
|
||||
val attrArr = context.obtainStyledAttributes(attrs, R.styleable.LoaderView, 0, 0);
|
||||
_automatic = attrArr.getBoolean(R.styleable.LoaderView_automatic, false);
|
||||
_isWhite = attrArr.getBoolean(R.styleable.LoaderView_isWhite, false);
|
||||
attrArr.recycle();
|
||||
} else {
|
||||
_automatic = false;
|
||||
_isWhite = false;
|
||||
}
|
||||
|
||||
visibility = View.GONE;
|
||||
|
||||
if (_isWhite) {
|
||||
_imageLoader.setColorFilter(Color.WHITE)
|
||||
}
|
||||
}
|
||||
constructor(context: Context, automatic: Boolean, height: Int = -1) : super(context) {
|
||||
constructor(context: Context, automatic: Boolean, height: Int = -1, isWhite: Boolean = false) : super(context) {
|
||||
inflate(context, R.layout.view_loader, this);
|
||||
_imageLoader = findViewById(R.id.image_loader);
|
||||
_animatable = _imageLoader.drawable as Animatable;
|
||||
_automatic = automatic;
|
||||
_isWhite = isWhite;
|
||||
|
||||
if(height > 0) {
|
||||
layoutParams = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, height);
|
|
@ -41,7 +41,7 @@ class MonetizationView : LinearLayout {
|
|||
|
||||
private val _textMerchandise: TextView;
|
||||
private val _recyclerMerchandise: RecyclerView;
|
||||
private val _loaderMerchandise: Loader;
|
||||
private val _loaderViewMerchandise: LoaderView;
|
||||
private val _layoutMerchandise: FrameLayout;
|
||||
private var _merchandiseAdapterView: AnyAdapterView<StoreItem, StoreItemViewHolder>? = null;
|
||||
|
||||
|
@ -81,7 +81,7 @@ class MonetizationView : LinearLayout {
|
|||
|
||||
_textMerchandise = findViewById(R.id.text_merchandise);
|
||||
_recyclerMerchandise = findViewById(R.id.recycler_merchandise);
|
||||
_loaderMerchandise = findViewById(R.id.loader_merchandise);
|
||||
_loaderViewMerchandise = findViewById(R.id.loader_merchandise);
|
||||
_layoutMerchandise = findViewById(R.id.layout_merchandise);
|
||||
|
||||
_root = findViewById(R.id.root);
|
||||
|
@ -108,7 +108,7 @@ class MonetizationView : LinearLayout {
|
|||
}
|
||||
|
||||
private fun setMerchandise(items: List<StoreItem>?) {
|
||||
_loaderMerchandise.stop();
|
||||
_loaderViewMerchandise.stop();
|
||||
|
||||
if (items == null) {
|
||||
_textMerchandise.visibility = View.GONE;
|
||||
|
@ -147,7 +147,7 @@ class MonetizationView : LinearLayout {
|
|||
val uri = Uri.parse(storeData);
|
||||
if (uri.isAbsolute) {
|
||||
_taskLoadMerchandise.run(storeData);
|
||||
_loaderMerchandise.start();
|
||||
_loaderViewMerchandise.start();
|
||||
} else {
|
||||
Logger.i(TAG, "Merchandise not loaded, not URL nor JSON")
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.futo.platformplayer.views.adapters
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
|
@ -37,8 +38,10 @@ class CommentViewHolder : ViewHolder {
|
|||
private val _layoutRating: LinearLayout;
|
||||
private val _pillRatingLikesDislikes: PillRatingLikesDislikes;
|
||||
private val _layoutComment: ConstraintLayout;
|
||||
private val _buttonDelete: FrameLayout;
|
||||
|
||||
var onClick = Event1<IPlatformComment>();
|
||||
var onRepliesClick = Event1<IPlatformComment>();
|
||||
var onDelete = Event1<IPlatformComment>();
|
||||
var comment: IPlatformComment? = null
|
||||
private set;
|
||||
|
||||
|
@ -55,6 +58,7 @@ class CommentViewHolder : ViewHolder {
|
|||
_buttonReplies = itemView.findViewById(R.id.button_replies);
|
||||
_layoutRating = itemView.findViewById(R.id.layout_rating);
|
||||
_pillRatingLikesDislikes = itemView.findViewById(R.id.rating);
|
||||
_buttonDelete = itemView.findViewById(R.id.button_delete);
|
||||
|
||||
_pillRatingLikesDislikes.onLikeDislikeUpdated.subscribe { args ->
|
||||
val c = comment
|
||||
|
@ -87,7 +91,12 @@ class CommentViewHolder : ViewHolder {
|
|||
|
||||
_buttonReplies.onClick.subscribe {
|
||||
val c = comment ?: return@subscribe;
|
||||
onClick.emit(c);
|
||||
onRepliesClick.emit(c);
|
||||
}
|
||||
|
||||
_buttonDelete.setOnClickListener {
|
||||
val c = comment ?: return@setOnClickListener;
|
||||
onDelete.emit(c);
|
||||
}
|
||||
|
||||
_textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context);
|
||||
|
@ -108,7 +117,8 @@ class CommentViewHolder : ViewHolder {
|
|||
|
||||
val rating = comment.rating;
|
||||
if (rating is RatingLikeDislikes) {
|
||||
_layoutComment.alpha = if (rating.dislikes > 2 && rating.dislikes.toFloat() / (rating.likes + rating.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f;
|
||||
_layoutComment.alpha = if (Settings.instance.comments.badReputationCommentsFading &&
|
||||
rating.dislikes > 2 && rating.dislikes.toFloat() / (rating.likes + rating.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f;
|
||||
} else {
|
||||
_layoutComment.alpha = 1.0f;
|
||||
}
|
||||
|
@ -167,6 +177,13 @@ class CommentViewHolder : ViewHolder {
|
|||
_buttonReplies.visibility = View.GONE;
|
||||
}
|
||||
|
||||
val processHandle = StatePolycentric.instance.processHandle
|
||||
if (processHandle != null && comment is PolycentricPlatformComment && processHandle.system == comment.eventPointer.system) {
|
||||
_buttonDelete.visibility = View.VISIBLE
|
||||
} else {
|
||||
_buttonDelete.visibility = View.GONE
|
||||
}
|
||||
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
package com.futo.platformplayer.views.adapters
|
||||
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.pills.PillButton
|
||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||
import com.futo.polycentric.core.Opinion
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.IdentityHashMap
|
||||
|
||||
class CommentWithReferenceViewHolder : ViewHolder {
|
||||
private val _creatorThumbnail: CreatorThumbnail;
|
||||
private val _textAuthor: TextView;
|
||||
private val _textMetadata: TextView;
|
||||
private val _textBody: TextView;
|
||||
private val _buttonReplies: PillButton;
|
||||
private val _pillRatingLikesDislikes: PillRatingLikesDislikes;
|
||||
private val _layoutComment: ConstraintLayout;
|
||||
private val _buttonDelete: FrameLayout;
|
||||
private val _cache: IdentityHashMap<IPlatformComment, StatePolycentric.LikesDislikesReplies>;
|
||||
private var _likesDislikesReplies: StatePolycentric.LikesDislikesReplies? = null;
|
||||
|
||||
private val _taskGetLiveComment = TaskHandler(StateApp.instance.scopeGetter, ::getLikesDislikesReplies)
|
||||
.success {
|
||||
_likesDislikesReplies = it
|
||||
updateLikesDislikesReplies()
|
||||
}
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to get live comment.", it);
|
||||
//TODO: Show error
|
||||
hideLikesDislikesReplies()
|
||||
}
|
||||
|
||||
var onRepliesClick = Event1<IPlatformComment>();
|
||||
var onDelete = Event1<IPlatformComment>();
|
||||
var comment: IPlatformComment? = null
|
||||
private set;
|
||||
|
||||
constructor(viewGroup: ViewGroup, cache: IdentityHashMap<IPlatformComment, StatePolycentric.LikesDislikesReplies>) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_comment_with_reference, viewGroup, false)) {
|
||||
_layoutComment = itemView.findViewById(R.id.layout_comment);
|
||||
_creatorThumbnail = itemView.findViewById(R.id.image_thumbnail);
|
||||
_textAuthor = itemView.findViewById(R.id.text_author);
|
||||
_textMetadata = itemView.findViewById(R.id.text_metadata);
|
||||
_textBody = itemView.findViewById(R.id.text_body);
|
||||
_buttonReplies = itemView.findViewById(R.id.button_replies);
|
||||
_pillRatingLikesDislikes = itemView.findViewById(R.id.rating);
|
||||
_buttonDelete = itemView.findViewById(R.id.button_delete)
|
||||
_cache = cache
|
||||
|
||||
_pillRatingLikesDislikes.onLikeDislikeUpdated.subscribe { args ->
|
||||
val c = comment
|
||||
if (c !is PolycentricPlatformComment) {
|
||||
throw Exception("Not implemented for non polycentric comments")
|
||||
}
|
||||
|
||||
if (args.hasLiked) {
|
||||
args.processHandle.opinion(c.reference, Opinion.like);
|
||||
} else if (args.hasDisliked) {
|
||||
args.processHandle.opinion(c.reference, Opinion.dislike);
|
||||
} else {
|
||||
args.processHandle.opinion(c.reference, Opinion.neutral);
|
||||
}
|
||||
|
||||
_layoutComment.alpha = if (args.dislikes > 2 && args.dislikes.toFloat() / (args.likes + args.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f;
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
Logger.i(TAG, "Started backfill");
|
||||
args.processHandle.fullyBackfillServersAnnounceExceptions();
|
||||
Logger.i(TAG, "Finished backfill");
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to backfill servers.", e)
|
||||
}
|
||||
}
|
||||
|
||||
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
|
||||
};
|
||||
|
||||
_buttonReplies.onClick.subscribe {
|
||||
val c = comment ?: return@subscribe;
|
||||
onRepliesClick.emit(c);
|
||||
}
|
||||
|
||||
_buttonDelete.setOnClickListener {
|
||||
val c = comment ?: return@setOnClickListener;
|
||||
onDelete.emit(c);
|
||||
}
|
||||
|
||||
_textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context);
|
||||
}
|
||||
|
||||
private suspend fun getLikesDislikesReplies(c: PolycentricPlatformComment): StatePolycentric.LikesDislikesReplies {
|
||||
val likesDislikesReplies = StatePolycentric.instance.getLikesDislikesReplies(c.reference)
|
||||
synchronized(_cache) {
|
||||
_cache[c] = likesDislikesReplies
|
||||
}
|
||||
return likesDislikesReplies
|
||||
}
|
||||
|
||||
fun bind(comment: IPlatformComment) {
|
||||
Log.i(TAG, "bind")
|
||||
|
||||
_likesDislikesReplies = null;
|
||||
_taskGetLiveComment.cancel()
|
||||
|
||||
_creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
|
||||
_creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false);
|
||||
_textAuthor.text = comment.author.name;
|
||||
|
||||
val date = comment.date;
|
||||
if (date != null) {
|
||||
_textMetadata.visibility = View.VISIBLE;
|
||||
_textMetadata.text = " • ${date.toHumanNowDiffString()} ago";
|
||||
} else {
|
||||
_textMetadata.visibility = View.GONE;
|
||||
}
|
||||
|
||||
val rating = comment.rating;
|
||||
if (rating is RatingLikeDislikes) {
|
||||
_layoutComment.alpha = if (Settings.instance.comments.badReputationCommentsFading &&
|
||||
rating.dislikes > 2 && rating.dislikes.toFloat() / (rating.likes + rating.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f;
|
||||
} else {
|
||||
_layoutComment.alpha = 1.0f;
|
||||
}
|
||||
|
||||
_textBody.text = comment.message.fixHtmlLinks();
|
||||
|
||||
this.comment = comment;
|
||||
updateLikesDislikesReplies();
|
||||
}
|
||||
|
||||
private fun updateLikesDislikesReplies() {
|
||||
Log.i(TAG, "updateLikesDislikesReplies")
|
||||
|
||||
val c = comment ?: return
|
||||
if (c is PolycentricPlatformComment) {
|
||||
if (_likesDislikesReplies == null) {
|
||||
Log.i(TAG, "updateLikesDislikesReplies retrieving from cache")
|
||||
|
||||
synchronized(_cache) {
|
||||
_likesDislikesReplies = _cache[c]
|
||||
}
|
||||
}
|
||||
|
||||
val likesDislikesReplies = _likesDislikesReplies
|
||||
if (likesDislikesReplies != null) {
|
||||
Log.i(TAG, "updateLikesDislikesReplies set")
|
||||
|
||||
val hasLiked = StatePolycentric.instance.hasLiked(c.reference);
|
||||
val hasDisliked = StatePolycentric.instance.hasDisliked(c.reference);
|
||||
_pillRatingLikesDislikes.setRating(RatingLikeDislikes(likesDislikesReplies.likes, likesDislikesReplies.dislikes), hasLiked, hasDisliked);
|
||||
|
||||
_buttonReplies.setLoading(false)
|
||||
|
||||
val replies = likesDislikesReplies.replyCount ?: 0;
|
||||
_buttonReplies.visibility = View.VISIBLE;
|
||||
_buttonReplies.text.text = "$replies " + itemView.context.getString(R.string.replies);
|
||||
} else {
|
||||
Log.i(TAG, "updateLikesDislikesReplies to load")
|
||||
|
||||
_pillRatingLikesDislikes.setLoading(true)
|
||||
_buttonReplies.setLoading(true)
|
||||
_taskGetLiveComment.run(c)
|
||||
}
|
||||
} else {
|
||||
hideLikesDislikesReplies()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideLikesDislikesReplies() {
|
||||
_pillRatingLikesDislikes.visibility = View.GONE
|
||||
_buttonReplies.visibility = View.GONE
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CommentWithReferenceViewHolder";
|
||||
}
|
||||
}
|
|
@ -74,9 +74,9 @@ class DeviceViewHolder : ViewHolder {
|
|||
} else if (d is AirPlayCastingDevice) {
|
||||
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
||||
_textType.text = "AirPlay";
|
||||
} else if (d is FastCastCastingDevice) {
|
||||
} else if (d is FCastCastingDevice) {
|
||||
_imageDevice.setImageResource(R.drawable.ic_fc);
|
||||
_textType.text = "FastCast";
|
||||
_textType.text = "FCast";
|
||||
}
|
||||
|
||||
_textName.text = d.name;
|
||||
|
|
|
@ -15,7 +15,12 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
|||
|
||||
var onClick = Event1<Subscription>();
|
||||
var onSettings = Event1<Subscription>();
|
||||
var sortBy: Int = 3
|
||||
var sortBy: Int = 5
|
||||
set(value) {
|
||||
field = value
|
||||
updateDataset()
|
||||
}
|
||||
var query: String? = null
|
||||
set(value) {
|
||||
field = value;
|
||||
updateDataset();
|
||||
|
@ -53,6 +58,7 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
|||
}
|
||||
|
||||
private fun updateDataset() {
|
||||
val queryLower = query?.lowercase() ?: "";
|
||||
_sortedDataset = when (sortBy) {
|
||||
0 -> StateSubscriptions.instance.getSubscriptions().sortedBy({ u -> u.channel.name.lowercase() })
|
||||
1 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending({ u -> u.channel.name.lowercase() })
|
||||
|
@ -61,7 +67,9 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
|||
4 -> StateSubscriptions.instance.getSubscriptions().sortedBy { it.playbackSeconds }
|
||||
5 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackSeconds }
|
||||
else -> throw IllegalStateException("Invalid sorting algorithm selected.");
|
||||
}.toList();
|
||||
}
|
||||
.filter { (queryLower.isNullOrBlank() || it.channel.name.lowercase().contains(queryLower)) }
|
||||
.toList();
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
|
|
@ -81,17 +81,19 @@ class SubscriptionViewHolder : ViewHolder {
|
|||
|
||||
this.subscription = sub;
|
||||
|
||||
_creatorThumbnail.setThumbnail(sub.channel.thumbnail, false);
|
||||
_taskLoadProfile.run(sub.channel.id);
|
||||
_textName.text = sub.channel.name;
|
||||
bindViewMetrics(sub);
|
||||
_platformIndicator.setPlatformFromClientID(sub.channel.id.pluginId);
|
||||
|
||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(sub.channel.url, true);
|
||||
if (cachedProfile != null) {
|
||||
onProfileLoaded(sub, cachedProfile, false);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(sub.channel.thumbnail, false);
|
||||
_taskLoadProfile.run(sub.channel.id);
|
||||
_textName.text = sub.channel.name;
|
||||
bindViewMetrics(sub);
|
||||
if (cachedProfile.expired) {
|
||||
_taskLoadProfile.run(sub.channel.id);
|
||||
}
|
||||
}
|
||||
|
||||
_platformIndicator.setPlatformFromClientID(sub.channel.id.pluginId);
|
||||
}
|
||||
|
||||
private fun onProfileLoaded(sub: Subscription?, cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
|
|
|
@ -19,7 +19,7 @@ import com.futo.platformplayer.states.StateApp
|
|||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.video.PlayerManager
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.Loader
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -28,7 +28,7 @@ class PreviewNestedVideoView : PreviewVideoView {
|
|||
|
||||
protected val _platformIndicatorNested: PlatformIndicator;
|
||||
protected val _containerLoader: LinearLayout;
|
||||
protected val _loader: Loader;
|
||||
protected val _loaderView: LoaderView;
|
||||
protected val _containerUnavailable: LinearLayout;
|
||||
protected val _textNestedUrl: TextView;
|
||||
|
||||
|
@ -42,7 +42,7 @@ class PreviewNestedVideoView : PreviewVideoView {
|
|||
constructor(context: Context, feedStyle: FeedStyle, exoPlayer: PlayerManager? = null): super(context, feedStyle, exoPlayer) {
|
||||
_platformIndicatorNested = findViewById(R.id.thumbnail_platform_nested);
|
||||
_containerLoader = findViewById(R.id.container_loader);
|
||||
_loader = findViewById(R.id.loader);
|
||||
_loaderView = findViewById(R.id.loader);
|
||||
_containerUnavailable = findViewById(R.id.container_unavailable);
|
||||
_textNestedUrl = findViewById(R.id.text_nested_url);
|
||||
|
||||
|
@ -116,7 +116,7 @@ class PreviewNestedVideoView : PreviewVideoView {
|
|||
if(!_contentSupported) {
|
||||
_containerUnavailable.visibility = View.VISIBLE;
|
||||
_containerLoader.visibility = View.GONE;
|
||||
_loader.stop();
|
||||
_loaderView.stop();
|
||||
}
|
||||
else {
|
||||
if(_feedStyle == FeedStyle.THUMBNAIL)
|
||||
|
@ -132,14 +132,14 @@ class PreviewNestedVideoView : PreviewVideoView {
|
|||
_contentSupported = false;
|
||||
_containerUnavailable.visibility = View.VISIBLE;
|
||||
_containerLoader.visibility = View.GONE;
|
||||
_loader.stop();
|
||||
_loaderView.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadNested(content: IPlatformNestedContent, onCompleted: ((IPlatformContentDetails)->Unit)? = null) {
|
||||
Logger.i(TAG, "Loading nested content [${content.contentUrl}]");
|
||||
_containerLoader.visibility = View.VISIBLE;
|
||||
_loader.start();
|
||||
_loaderView.start();
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
val def = StatePlatform.instance.getContentDetails(content.contentUrl);
|
||||
def.invokeOnCompletion {
|
||||
|
@ -150,13 +150,13 @@ class PreviewNestedVideoView : PreviewVideoView {
|
|||
if(_content == content) {
|
||||
_containerUnavailable.visibility = View.VISIBLE;
|
||||
_containerLoader.visibility = View.GONE;
|
||||
_loader.stop();
|
||||
_loaderView.stop();
|
||||
}
|
||||
//TODO: Handle exception
|
||||
}
|
||||
else if(_content == content) {
|
||||
_containerLoader.visibility = View.GONE;
|
||||
_loader.stop();
|
||||
_loaderView.stop();
|
||||
val nestedContent = def.getCompleted();
|
||||
_contentNested = nestedContent;
|
||||
if(nestedContent is IPlatformVideoDetails) {
|
||||
|
|
|
@ -176,20 +176,23 @@ open class PreviewVideoView : LinearLayout {
|
|||
|
||||
stopPreview();
|
||||
|
||||
_imageNeopassChannel?.visibility = View.GONE;
|
||||
_creatorThumbnail?.setThumbnail(content.author.thumbnail, false);
|
||||
_imageChannel?.let {
|
||||
Glide.with(_imageChannel)
|
||||
.load(content.author.thumbnail)
|
||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||
.into(_imageChannel);
|
||||
}
|
||||
_taskLoadProfile.run(content.author.id);
|
||||
_textChannelName.text = content.author.name
|
||||
|
||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(content.author.url, true);
|
||||
if (cachedProfile != null) {
|
||||
onProfileLoaded(cachedProfile, false);
|
||||
} else {
|
||||
_imageNeopassChannel?.visibility = View.GONE;
|
||||
_creatorThumbnail?.setThumbnail(content.author.thumbnail, false);
|
||||
_imageChannel?.let {
|
||||
Glide.with(_imageChannel)
|
||||
.load(content.author.thumbnail)
|
||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||
.into(_imageChannel);
|
||||
if (cachedProfile.expired) {
|
||||
_taskLoadProfile.run(content.author.id);
|
||||
}
|
||||
_taskLoadProfile.run(content.author.id);
|
||||
_textChannelName.text = content.author.name
|
||||
}
|
||||
|
||||
_imageChannel?.clipToOutline = true;
|
||||
|
|
|
@ -65,13 +65,16 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
|
|||
override fun bind(authorLink: PlatformAuthorLink) {
|
||||
_taskLoadProfile.cancel();
|
||||
|
||||
_creatorThumbnail.setThumbnail(authorLink.thumbnail, false);
|
||||
_taskLoadProfile.run(authorLink.id);
|
||||
_textName.text = authorLink.name;
|
||||
|
||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(authorLink.url, true);
|
||||
if (cachedProfile != null) {
|
||||
onProfileLoaded(cachedProfile, false);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(authorLink.thumbnail, false);
|
||||
_taskLoadProfile.run(authorLink.id);
|
||||
_textName.text = authorLink.name;
|
||||
if (cachedProfile.expired) {
|
||||
_taskLoadProfile.run(authorLink.id);
|
||||
}
|
||||
}
|
||||
|
||||
if(authorLink.subscribers == null || (authorLink.subscribers ?: 0) <= 0L)
|
||||
|
|
|
@ -51,13 +51,16 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
|
|||
|
||||
_channel = subscription.channel;
|
||||
|
||||
_creatorThumbnail.setThumbnail(subscription.channel.thumbnail, false);
|
||||
_taskLoadProfile.run(subscription.channel.id);
|
||||
_name.text = subscription.channel.name;
|
||||
|
||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(subscription.channel.url, true);
|
||||
if (cachedProfile != null) {
|
||||
onProfileLoaded(cachedProfile, false);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(subscription.channel.thumbnail, false);
|
||||
_taskLoadProfile.run(subscription.channel.id);
|
||||
_name.text = subscription.channel.name;
|
||||
if (cachedProfile.expired) {
|
||||
_taskLoadProfile.run(subscription.channel.id);
|
||||
}
|
||||
}
|
||||
|
||||
_subscription = subscription;
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.animation.ObjectAnimator
|
|||
import android.content.Context
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.GestureDetector
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
|
@ -63,11 +62,15 @@ class GestureControlView : LinearLayout {
|
|||
private var _fullScreenFactorUp = 1.0f;
|
||||
private var _fullScreenFactorDown = 1.0f;
|
||||
|
||||
private val _gestureController: GestureDetectorCompat;
|
||||
|
||||
val onSeek = Event1<Long>();
|
||||
val onBrightnessAdjusted = Event1<Float>();
|
||||
val onSoundAdjusted = Event1<Float>();
|
||||
val onToggleFullscreen = Event0();
|
||||
|
||||
var fullScreenGestureEnabled = true
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_gesture_controls, this, true);
|
||||
|
||||
|
@ -82,13 +85,8 @@ class GestureControlView : LinearLayout {
|
|||
_layoutControlsBrightness = findViewById(R.id.layout_controls_brightness);
|
||||
_progressBrightness = findViewById(R.id.progress_brightness);
|
||||
_layoutControlsFullscreen = findViewById(R.id.layout_controls_fullscreen);
|
||||
}
|
||||
|
||||
fun setupTouchArea(view: View, layoutControls: ViewGroup? = null, background: View? = null) {
|
||||
_layoutControls = layoutControls;
|
||||
_background = background;
|
||||
|
||||
val gestureController = GestureDetectorCompat(context, object : GestureDetector.OnGestureListener {
|
||||
_gestureController = GestureDetectorCompat(context, object : GestureDetector.OnGestureListener {
|
||||
override fun onDown(p0: MotionEvent): Boolean { return false; }
|
||||
override fun onShowPress(p0: MotionEvent) = Unit;
|
||||
override fun onSingleTapUp(p0: MotionEvent): Boolean { return false; }
|
||||
|
@ -116,15 +114,14 @@ class GestureControlView : LinearLayout {
|
|||
_fullScreenFactorDown = (_fullScreenFactorDown + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_layoutControlsFullscreen.alpha = _fullScreenFactorDown;
|
||||
} else {
|
||||
val rx = p0.x / width;
|
||||
val ry = p0.y / height;
|
||||
Logger.v(TAG, "rx = $rx, ry = $ry, _isFullScreen = $_isFullScreen")
|
||||
val rx = (p0.x + p1.x) / (2 * width);
|
||||
val ry = (p0.y + p1.y) / (2 * height);
|
||||
if (ry > 0.1 && ry < 0.9) {
|
||||
if (_isFullScreen && rx < 0.4) {
|
||||
if (_isFullScreen && rx < 0.2) {
|
||||
startAdjustingBrightness();
|
||||
} else if (_isFullScreen && rx > 0.6) {
|
||||
} else if (_isFullScreen && rx > 0.8) {
|
||||
startAdjustingSound();
|
||||
} else if (rx >= 0.4 && rx <= 0.6) {
|
||||
} else if (fullScreenGestureEnabled && rx in 0.3..0.7) {
|
||||
if (_isFullScreen) {
|
||||
startAdjustingFullscreenDown();
|
||||
} else {
|
||||
|
@ -139,7 +136,7 @@ class GestureControlView : LinearLayout {
|
|||
override fun onLongPress(p0: MotionEvent) = Unit
|
||||
});
|
||||
|
||||
gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
|
||||
_gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
|
||||
override fun onSingleTapConfirmed(ev: MotionEvent): Boolean {
|
||||
if (_skipping) {
|
||||
return false;
|
||||
|
@ -169,52 +166,58 @@ class GestureControlView : LinearLayout {
|
|||
}
|
||||
});
|
||||
|
||||
val touchListener = object : OnTouchListener {
|
||||
override fun onTouch(v: View?, ev: MotionEvent): Boolean {
|
||||
cancelHideJob();
|
||||
isClickable = true
|
||||
}
|
||||
|
||||
if (_skipping) {
|
||||
if (ev.action == MotionEvent.ACTION_UP) {
|
||||
startExitFastForward();
|
||||
stopAutoFastForward();
|
||||
} else if (ev.action == MotionEvent.ACTION_DOWN) {
|
||||
_jobExitFastForward?.cancel();
|
||||
_jobExitFastForward = null;
|
||||
fun setupTouchArea(layoutControls: ViewGroup? = null, background: View? = null) {
|
||||
_layoutControls = layoutControls;
|
||||
_background = background;
|
||||
}
|
||||
|
||||
startAutoFastForward();
|
||||
fastForwardTick();
|
||||
}
|
||||
}
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
val ev = event ?: return super.onTouchEvent(event);
|
||||
|
||||
if (_adjustingSound && ev.action == MotionEvent.ACTION_UP) {
|
||||
stopAdjustingSound();
|
||||
}
|
||||
cancelHideJob();
|
||||
|
||||
if (_adjustingBrightness && ev.action == MotionEvent.ACTION_UP) {
|
||||
stopAdjustingBrightness();
|
||||
}
|
||||
if (_skipping) {
|
||||
if (ev.action == MotionEvent.ACTION_UP) {
|
||||
startExitFastForward();
|
||||
stopAutoFastForward();
|
||||
} else if (ev.action == MotionEvent.ACTION_DOWN) {
|
||||
_jobExitFastForward?.cancel();
|
||||
_jobExitFastForward = null;
|
||||
|
||||
if (_adjustingFullscreenUp && ev.action == MotionEvent.ACTION_UP) {
|
||||
if (_fullScreenFactorUp > 0.5) {
|
||||
onToggleFullscreen.emit();
|
||||
}
|
||||
stopAdjustingFullscreenUp();
|
||||
}
|
||||
|
||||
if (_adjustingFullscreenDown && ev.action == MotionEvent.ACTION_UP) {
|
||||
if (_fullScreenFactorDown > 0.5) {
|
||||
onToggleFullscreen.emit();
|
||||
}
|
||||
stopAdjustingFullscreenDown();
|
||||
}
|
||||
|
||||
startHideJobIfNecessary();
|
||||
return gestureController.onTouchEvent(ev);
|
||||
startAutoFastForward();
|
||||
fastForwardTick();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
view.setOnTouchListener(touchListener);
|
||||
view.isClickable = true;
|
||||
if (_adjustingSound && ev.action == MotionEvent.ACTION_UP) {
|
||||
stopAdjustingSound();
|
||||
}
|
||||
|
||||
if (_adjustingBrightness && ev.action == MotionEvent.ACTION_UP) {
|
||||
stopAdjustingBrightness();
|
||||
}
|
||||
|
||||
if (_adjustingFullscreenUp && ev.action == MotionEvent.ACTION_UP) {
|
||||
if (_fullScreenFactorUp > 0.5) {
|
||||
onToggleFullscreen.emit();
|
||||
}
|
||||
stopAdjustingFullscreenUp();
|
||||
}
|
||||
|
||||
if (_adjustingFullscreenDown && ev.action == MotionEvent.ACTION_UP) {
|
||||
if (_fullScreenFactorDown > 0.5) {
|
||||
onToggleFullscreen.emit();
|
||||
}
|
||||
stopAdjustingFullscreenDown();
|
||||
}
|
||||
|
||||
startHideJobIfNecessary();
|
||||
|
||||
_gestureController.onTouchEvent(ev)
|
||||
return true;
|
||||
}
|
||||
|
||||
fun cancelHideJob() {
|
||||
|
|
|
@ -58,7 +58,8 @@ class CastView : ConstraintLayout {
|
|||
_timeBar = findViewById(R.id.time_progress);
|
||||
_background = findViewById(R.id.layout_background);
|
||||
_gestureControlView = findViewById(R.id.gesture_control);
|
||||
_gestureControlView.setupTouchArea(_background);
|
||||
_gestureControlView.fullScreenGestureEnabled = false
|
||||
_gestureControlView.setupTouchArea();
|
||||
_gestureControlView.onSeek.subscribe {
|
||||
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
||||
StateCasting.instance.videoSeekTo(d.expectedCurrentTime + it / 1000);
|
||||
|
|
|
@ -4,15 +4,21 @@ import android.content.Context
|
|||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.fixHtmlLinks
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.toHumanNowDiffString
|
||||
import com.futo.platformplayer.views.behavior.NonScrollingTextView
|
||||
import com.futo.platformplayer.views.comments.AddCommentView
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.segments.CommentsList
|
||||
import userpackage.Protocol
|
||||
|
||||
|
@ -22,6 +28,11 @@ class RepliesOverlay : LinearLayout {
|
|||
private val _topbar: OverlayTopbar;
|
||||
private val _commentsList: CommentsList;
|
||||
private val _addCommentView: AddCommentView;
|
||||
private val _textBody: NonScrollingTextView;
|
||||
private val _textAuthor: TextView;
|
||||
private val _textMetadata: TextView;
|
||||
private val _creatorThumbnail: CreatorThumbnail;
|
||||
private val _layoutParentComment: ConstraintLayout;
|
||||
private var _readonly = false;
|
||||
private var _onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null;
|
||||
|
||||
|
@ -30,6 +41,11 @@ class RepliesOverlay : LinearLayout {
|
|||
_topbar = findViewById(R.id.topbar);
|
||||
_commentsList = findViewById(R.id.comments_list);
|
||||
_addCommentView = findViewById(R.id.add_comment_view);
|
||||
_textBody = findViewById(R.id.text_body)
|
||||
_textMetadata = findViewById(R.id.text_metadata)
|
||||
_textAuthor = findViewById(R.id.text_author)
|
||||
_creatorThumbnail = findViewById(R.id.image_thumbnail)
|
||||
_layoutParentComment = findViewById(R.id.layout_parent_comment)
|
||||
|
||||
_addCommentView.onCommentAdded.subscribe {
|
||||
_commentsList.addComment(it);
|
||||
|
@ -42,7 +58,7 @@ class RepliesOverlay : LinearLayout {
|
|||
}
|
||||
}
|
||||
|
||||
_commentsList.onClick.subscribe { c ->
|
||||
_commentsList.onRepliesClick.subscribe { c ->
|
||||
val replyCount = c.replyCount;
|
||||
var metadata = "";
|
||||
if (replyCount != null && replyCount > 0) {
|
||||
|
@ -50,9 +66,9 @@ class RepliesOverlay : LinearLayout {
|
|||
}
|
||||
|
||||
if (c is PolycentricPlatformComment) {
|
||||
load(false, metadata, c.contextUrl, c.reference, { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) });
|
||||
load(false, metadata, c.contextUrl, c.reference, c, { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) });
|
||||
} else {
|
||||
load(true, metadata, null, null, { StatePlatform.instance.getSubComments(c) });
|
||||
load(true, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -60,7 +76,7 @@ class RepliesOverlay : LinearLayout {
|
|||
_topbar.setInfo(context.getString(R.string.Replies), "");
|
||||
}
|
||||
|
||||
fun load(readonly: Boolean, metadata: String, contextUrl: String?, ref: Protocol.Reference?, loader: suspend () -> IPager<IPlatformComment>, onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null) {
|
||||
fun load(readonly: Boolean, metadata: String, contextUrl: String?, ref: Protocol.Reference?, parentComment: IPlatformComment? = null, loader: suspend () -> IPager<IPlatformComment>, onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null) {
|
||||
_readonly = readonly;
|
||||
if (readonly) {
|
||||
_addCommentView.visibility = View.GONE;
|
||||
|
@ -69,6 +85,26 @@ class RepliesOverlay : LinearLayout {
|
|||
_addCommentView.setContext(contextUrl, ref);
|
||||
}
|
||||
|
||||
if (parentComment == null) {
|
||||
_layoutParentComment.visibility = View.GONE
|
||||
} else {
|
||||
_layoutParentComment.visibility = View.VISIBLE
|
||||
|
||||
_textBody.text = parentComment.message.fixHtmlLinks()
|
||||
_textAuthor.text = parentComment.author.name
|
||||
|
||||
val date = parentComment.date
|
||||
if (date != null) {
|
||||
_textMetadata.visibility = View.VISIBLE
|
||||
_textMetadata.text = " • ${date.toHumanNowDiffString()} ago"
|
||||
} else {
|
||||
_textMetadata.visibility = View.GONE
|
||||
}
|
||||
|
||||
_creatorThumbnail.setThumbnail(parentComment.author.thumbnail, false);
|
||||
_creatorThumbnail.setHarborAvailable(parentComment is PolycentricPlatformComment,false);
|
||||
}
|
||||
|
||||
_topbar.setInfo(context.getString(R.string.Replies), metadata);
|
||||
_commentsList.load(readonly, loader);
|
||||
_onCommentAdded = onCommentAdded;
|
||||
|
|
|
@ -25,7 +25,7 @@ class SlideUpMenuItem : RelativeLayout {
|
|||
init();
|
||||
}
|
||||
|
||||
constructor(context: Context, imageRes: Int = 0, mainText: String, subText: String = "", tag: Any, call: (()->Unit)? = null, invokeParent: Boolean = true): super(context){
|
||||
constructor(context: Context, imageRes: Int = 0, mainText: String, subText: String = "", tag: Any?, call: (()->Unit)? = null, invokeParent: Boolean = true): super(context){
|
||||
init();
|
||||
_image.setImageResource(imageRes);
|
||||
_text.text = mainText;
|
||||
|
|
|
@ -73,8 +73,9 @@ class SlideUpMenuOverlay : RelativeLayout {
|
|||
item.setParentClickListener { hide() };
|
||||
else if(item is SlideUpMenuItem)
|
||||
item.setParentClickListener { hide() };
|
||||
|
||||
}
|
||||
|
||||
_groupItems = items;
|
||||
}
|
||||
|
||||
private fun init(animated: Boolean, okText: String?){
|
||||
|
|
|
@ -9,16 +9,20 @@ import android.widget.LinearLayout
|
|||
import android.widget.TextView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
|
||||
class PillButton : LinearLayout {
|
||||
val icon: ImageView;
|
||||
val text: TextView;
|
||||
val loaderView: LoaderView;
|
||||
val onClick = Event0();
|
||||
private var _isLoading = false;
|
||||
|
||||
constructor(context : Context, attrs : AttributeSet) : super(context, attrs) {
|
||||
LayoutInflater.from(context).inflate(R.layout.pill_button, this, true);
|
||||
icon = findViewById(R.id.pill_icon);
|
||||
text = findViewById(R.id.pill_text);
|
||||
loaderView = findViewById(R.id.loader)
|
||||
|
||||
val attrArr = context.obtainStyledAttributes(attrs, R.styleable.PillButton, 0, 0);
|
||||
val attrIconRef = attrArr.getResourceId(R.styleable.PillButton_pillIcon, -1);
|
||||
|
@ -31,7 +35,29 @@ class PillButton : LinearLayout {
|
|||
text.text = attrText;
|
||||
|
||||
findViewById<LinearLayout>(R.id.root).setOnClickListener {
|
||||
if (_isLoading) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
onClick.emit();
|
||||
};
|
||||
}
|
||||
|
||||
fun setLoading(loading: Boolean) {
|
||||
if (loading == _isLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
text.visibility = View.GONE
|
||||
loaderView.visibility = View.VISIBLE
|
||||
loaderView.start()
|
||||
} else {
|
||||
loaderView.stop()
|
||||
text.visibility = View.VISIBLE
|
||||
loaderView.visibility = View.GONE
|
||||
}
|
||||
|
||||
_isLoading = loading
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ import com.futo.platformplayer.constructs.Event1
|
|||
import com.futo.platformplayer.constructs.Event3
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.toHumanNumber
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
import com.futo.polycentric.core.ProcessHandle
|
||||
|
||||
data class OnLikeDislikeUpdatedArgs(
|
||||
|
@ -29,9 +30,12 @@ data class OnLikeDislikeUpdatedArgs(
|
|||
class PillRatingLikesDislikes : LinearLayout {
|
||||
private val _textLikes: TextView;
|
||||
private val _textDislikes: TextView;
|
||||
private val _loaderViewLikes: LoaderView;
|
||||
private val _loaderViewDislikes: LoaderView;
|
||||
private val _seperator: View;
|
||||
private val _iconLikes: ImageView;
|
||||
private val _iconDislikes: ImageView;
|
||||
private var _isLoading: Boolean = false;
|
||||
|
||||
private var _likes = 0L;
|
||||
private var _hasLiked = false;
|
||||
|
@ -47,14 +51,42 @@ class PillRatingLikesDislikes : LinearLayout {
|
|||
_seperator = findViewById(R.id.pill_seperator);
|
||||
_iconDislikes = findViewById(R.id.pill_dislike_icon);
|
||||
_iconLikes = findViewById(R.id.pill_like_icon);
|
||||
_loaderViewLikes = findViewById(R.id.loader_likes)
|
||||
_loaderViewDislikes = findViewById(R.id.loader_dislikes)
|
||||
|
||||
_iconLikes.setOnClickListener { StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; };
|
||||
_textLikes.setOnClickListener { StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; };
|
||||
_iconDislikes.setOnClickListener { StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; };
|
||||
_textDislikes.setOnClickListener { StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; };
|
||||
_iconLikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; };
|
||||
_textLikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; };
|
||||
_iconDislikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; };
|
||||
_textDislikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; };
|
||||
}
|
||||
|
||||
fun setLoading(loading: Boolean) {
|
||||
if (_isLoading == loading) {
|
||||
return
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
_textLikes.visibility = View.GONE
|
||||
_loaderViewLikes.visibility = View.VISIBLE
|
||||
_textDislikes.visibility = View.GONE
|
||||
_loaderViewDislikes.visibility = View.VISIBLE
|
||||
_loaderViewLikes.start()
|
||||
_loaderViewDislikes.start()
|
||||
} else {
|
||||
_loaderViewLikes.stop()
|
||||
_loaderViewDislikes.stop()
|
||||
_textLikes.visibility = View.VISIBLE
|
||||
_loaderViewLikes.visibility = View.GONE
|
||||
_textDislikes.visibility = View.VISIBLE
|
||||
_loaderViewDislikes.visibility = View.GONE
|
||||
}
|
||||
|
||||
_isLoading = loading
|
||||
}
|
||||
|
||||
fun setRating(rating: IRating, hasLiked: Boolean = false, hasDisliked: Boolean = false) {
|
||||
setLoading(false)
|
||||
|
||||
when (rating) {
|
||||
is RatingLikeDislikes -> {
|
||||
setRating(rating, hasLiked, hasDisliked);
|
||||
|
@ -127,6 +159,8 @@ class PillRatingLikesDislikes : LinearLayout {
|
|||
}
|
||||
|
||||
fun setRating(rating: RatingLikeDislikes, hasLiked: Boolean = false, hasDisliked: Boolean = false) {
|
||||
setLoading(false)
|
||||
|
||||
_textLikes.text = rating.likes.toHumanNumber();
|
||||
_textDislikes.text = rating.dislikes.toHumanNumber();
|
||||
_textLikes.visibility = View.VISIBLE;
|
||||
|
@ -140,6 +174,8 @@ class PillRatingLikesDislikes : LinearLayout {
|
|||
updateColors();
|
||||
}
|
||||
fun setRating(rating: RatingLikes, hasLiked: Boolean = false) {
|
||||
setLoading(false)
|
||||
|
||||
_textLikes.text = rating.likes.toHumanNumber();
|
||||
_textLikes.visibility = View.VISIBLE;
|
||||
_textDislikes.visibility = View.GONE;
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
package com.futo.platformplayer.views.segments
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.KeyCharacterMap.UnavailableException
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -19,22 +23,33 @@ import com.futo.platformplayer.api.media.structures.IAsyncPager
|
|||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.views.adapters.CommentViewHolder
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.UnknownHostException
|
||||
|
||||
class CommentsList : ConstraintLayout {
|
||||
private val _llmReplies: LinearLayoutManager;
|
||||
private val _textMessage: TextView;
|
||||
private val _taskLoadComments = if(!isInEditMode) TaskHandler<suspend () -> IPager<IPlatformComment>, IPager<IPlatformComment>>(StateApp.instance.scopeGetter, { it(); })
|
||||
.success { pager -> onCommentsLoaded(pager); }
|
||||
.exception<UnknownHostException> {
|
||||
UIDialogs.toast("Failed to load comments");
|
||||
setMessage("UnknownHostException: " + it.message);
|
||||
Logger.e(TAG, "Failed to load comments.", it);
|
||||
setLoading(false);
|
||||
}
|
||||
.exception<ScriptUnavailableException> {
|
||||
setMessage(it.message);
|
||||
Logger.e(TAG, "Failed to load comments.", it);
|
||||
setLoading(false);
|
||||
}
|
||||
.exception<Throwable> {
|
||||
setMessage("Throwable: " + it.message);
|
||||
Logger.e(TAG, "Failed to load comments.", it);
|
||||
UIDialogs.toast(context, context.getString(R.string.failed_to_load_comments) + "\n" + (it.message ?: ""));
|
||||
//UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_comments) + (it.message ?: ""), it, ::fetchComments);
|
||||
setLoading(false);
|
||||
} else TaskHandler(IPlatformVideoDetails::class.java, StateApp.instance.scopeGetter);
|
||||
|
@ -69,23 +84,35 @@ class CommentsList : ConstraintLayout {
|
|||
private val _prependedView: FrameLayout;
|
||||
private var _readonly: Boolean = false;
|
||||
|
||||
var onClick = Event1<IPlatformComment>();
|
||||
var onRepliesClick = Event1<IPlatformComment>();
|
||||
var onCommentsLoaded = Event1<Int>();
|
||||
|
||||
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_comments_list, this, true);
|
||||
|
||||
_recyclerComments = findViewById(R.id.recycler_comments);
|
||||
_textMessage = TextView(context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||
setMargins(0, 30, 0, 0)
|
||||
}
|
||||
textSize = 12.0f
|
||||
setTextColor(Color.WHITE)
|
||||
typeface = resources.getFont(R.font.inter_regular)
|
||||
gravity = Gravity.CENTER_HORIZONTAL
|
||||
}
|
||||
|
||||
_prependedView = FrameLayout(context);
|
||||
_prependedView.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
_adapterComments = InsertedViewAdapterWithLoader(context, arrayListOf(_prependedView), arrayListOf(),
|
||||
_adapterComments = InsertedViewAdapterWithLoader(context, arrayListOf(_prependedView, _textMessage), arrayListOf(),
|
||||
childCountGetter = { _comments.size },
|
||||
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(_comments[position], _readonly); },
|
||||
childViewHolderFactory = { viewGroup, _ ->
|
||||
val holder = CommentViewHolder(viewGroup);
|
||||
holder.onClick.subscribe { c -> onClick.emit(c) };
|
||||
holder.onRepliesClick.subscribe { c -> onRepliesClick.emit(c) };
|
||||
holder.onDelete.subscribe(::onDelete);
|
||||
return@InsertedViewAdapterWithLoader holder;
|
||||
}
|
||||
);
|
||||
|
@ -96,6 +123,16 @@ class CommentsList : ConstraintLayout {
|
|||
_recyclerComments.addOnScrollListener(_scrollListener);
|
||||
}
|
||||
|
||||
private fun setMessage(message: String?) {
|
||||
Logger.i(TAG, "setMessage " + message)
|
||||
if (message != null) {
|
||||
_textMessage.visibility = View.VISIBLE
|
||||
_textMessage.text = message
|
||||
} else {
|
||||
_textMessage.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun addComment(comment: IPlatformComment) {
|
||||
_comments.add(0, comment);
|
||||
_adapterComments.notifyItemRangeInserted(_adapterComments.childToParentPosition(0), 1);
|
||||
|
@ -106,6 +143,38 @@ class CommentsList : ConstraintLayout {
|
|||
_prependedView.addView(view);
|
||||
}
|
||||
|
||||
private fun onDelete(comment: IPlatformComment) {
|
||||
UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete this comment?", {
|
||||
val processHandle = StatePolycentric.instance.processHandle ?: return@showConfirmationDialog
|
||||
if (comment !is PolycentricPlatformComment) {
|
||||
return@showConfirmationDialog
|
||||
}
|
||||
|
||||
val index = _comments.indexOf(comment)
|
||||
if (index != -1) {
|
||||
_comments.removeAt(index)
|
||||
_adapterComments.notifyItemRemoved(_adapterComments.childToParentPosition(index))
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
processHandle.delete(comment.eventPointer.process, comment.eventPointer.logicalClock)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to delete event.", e);
|
||||
return@launch;
|
||||
}
|
||||
|
||||
try {
|
||||
Logger.i(TAG, "Started backfill");
|
||||
processHandle.fullyBackfillServersAnnounceExceptions();
|
||||
Logger.i(TAG, "Finished backfill");
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to fully backfill servers.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun onScrolled() {
|
||||
val visibleItemCount = _recyclerComments.childCount;
|
||||
val firstVisibleItem = _llmReplies.findFirstVisibleItemPosition();
|
||||
|
@ -147,6 +216,7 @@ class CommentsList : ConstraintLayout {
|
|||
|
||||
fun load(readonly: Boolean, loader: suspend () -> IPager<IPlatformComment>) {
|
||||
cancel();
|
||||
setMessage(null);
|
||||
|
||||
_readonly = readonly;
|
||||
setLoading(true);
|
||||
|
@ -177,6 +247,7 @@ class CommentsList : ConstraintLayout {
|
|||
_comments.clear();
|
||||
_commentsPager = null;
|
||||
_adapterComments.notifyDataSetChanged();
|
||||
setMessage(null);
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
|
|
|
@ -156,7 +156,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||
_layoutControls = findViewById(R.id.layout_controls);
|
||||
gestureControl = findViewById(R.id.gesture_control);
|
||||
|
||||
_videoView?.videoSurfaceView?.let { gestureControl.setupTouchArea(it, _layoutControls, background); };
|
||||
gestureControl.setupTouchArea(_layoutControls, background);
|
||||
gestureControl.onSeek.subscribe { seekFromCurrent(it); };
|
||||
gestureControl.onSoundAdjusted.subscribe { setVolume(it) };
|
||||
gestureControl.onToggleFullscreen.subscribe { setFullScreen(!isFullScreen) };
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package com.futo.platformplayer.views.video
|
||||
|
||||
import android.content.Context
|
||||
import android.media.session.PlaybackState
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.widget.RelativeLayout
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||
|
@ -16,6 +16,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlR
|
|||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.video.PlayerManager
|
||||
import com.google.android.exoplayer2.*
|
||||
|
@ -54,6 +55,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||
private var _lastSubtitleMediaSource: MediaSource? = null;
|
||||
private var _shouldPlaybackRestartOnConnectivity: Boolean = false;
|
||||
private val _referenceObject = Object();
|
||||
private var _connectivityLossTime_ms: Long? = null
|
||||
|
||||
private var _chapters: List<IChapter>? = null;
|
||||
|
||||
|
@ -152,7 +154,24 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||
|
||||
val pos = position;
|
||||
val dur = duration;
|
||||
var shouldRestartPlayback = false
|
||||
if (_shouldPlaybackRestartOnConnectivity && abs(pos - dur) > 2000) {
|
||||
if (Settings.instance.playback.restartPlaybackAfterConnectivityLoss == 1) {
|
||||
val lossTime_ms = _connectivityLossTime_ms
|
||||
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 30) {
|
||||
shouldRestartPlayback = true
|
||||
}
|
||||
} else if (Settings.instance.playback.restartPlaybackAfterConnectivityLoss == 2) {
|
||||
val lossTime_ms = _connectivityLossTime_ms
|
||||
if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 10) {
|
||||
shouldRestartPlayback = true
|
||||
}
|
||||
} else if (Settings.instance.playback.restartPlaybackAfterConnectivityLoss == 3) {
|
||||
shouldRestartPlayback = true
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRestartPlayback) {
|
||||
Logger.i(TAG, "Playback ended due to connection loss, resuming playback since connection is restored.");
|
||||
exoPlayer?.player?.playWhenReady = true;
|
||||
exoPlayer?.player?.prepare();
|
||||
|
@ -509,16 +528,17 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
|
||||
onDatasourceError.emit(error);
|
||||
}
|
||||
PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED,
|
||||
PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND,
|
||||
PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE,
|
||||
//PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED,
|
||||
//PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND,
|
||||
//PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE,
|
||||
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
|
||||
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT,
|
||||
PlaybackException.ERROR_CODE_IO_NO_PERMISSION,
|
||||
PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
|
||||
//PlaybackException.ERROR_CODE_IO_NO_PERMISSION,
|
||||
//PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
|
||||
PlaybackException.ERROR_CODE_IO_UNSPECIFIED -> {
|
||||
Logger.i(TAG, "IO error, set _shouldPlaybackRestartOnConnectivity=true");
|
||||
_shouldPlaybackRestartOnConnectivity = true;
|
||||
_connectivityLossTime_ms = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -536,8 +556,6 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||
Logger.i(TAG, "_shouldPlaybackRestartOnConnectivity=false");
|
||||
_shouldPlaybackRestartOnConnectivity = false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
syntax = "proto2";
|
||||
option optimize_for = LITE_RUNTIME;
|
||||
package com.futo.platformplayer.protos;
|
||||
|
||||
message CastMessage {
|
||||
enum ProtocolVersion { CASTV2_1_0 = 0; }
|
||||
required ProtocolVersion protocol_version = 1;
|
||||
required string source_id = 2;
|
||||
required string destination_id = 3;
|
||||
required string namespace = 4;
|
||||
enum PayloadType {
|
||||
STRING = 0;
|
||||
BINARY = 1;
|
||||
}
|
||||
required PayloadType payload_type = 5;
|
||||
optional string payload_utf8 = 6;
|
||||
optional bytes payload_binary = 7;
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
syntax = "proto2";
|
||||
option optimize_for = LITE_RUNTIME;
|
||||
package com.futo.platformplayer.protos;
|
||||
|
||||
message CastMessage {
|
||||
// Always pass a version of the protocol for future compatibility
|
||||
// requirements.
|
||||
enum ProtocolVersion { CASTV2_1_0 = 0; }
|
||||
required ProtocolVersion protocol_version = 1;
|
||||
// source and destination ids identify the origin and destination of the
|
||||
// message. They are used to route messages between endpoints that share a
|
||||
// device-to-device channel.
|
||||
//
|
||||
// For messages between applications:
|
||||
// - The sender application id is a unique identifier generated on behalf of
|
||||
// the sender application.
|
||||
// - The receiver id is always the the session id for the application.
|
||||
//
|
||||
// For messages to or from the sender or receiver platform, the special ids
|
||||
// 'sender-0' and 'receiver-0' can be used.
|
||||
//
|
||||
// For messages intended for all endpoints using a given channel, the
|
||||
// wildcard destination_id '*' can be used.
|
||||
required string source_id = 2;
|
||||
required string destination_id = 3;
|
||||
// This is the core multiplexing key. All messages are sent on a namespace
|
||||
// and endpoints sharing a channel listen on one or more namespaces. The
|
||||
// namespace defines the protocol and semantics of the message.
|
||||
required string namespace = 4;
|
||||
// Encoding and payload info follows.
|
||||
// What type of data do we have in this message.
|
||||
enum PayloadType {
|
||||
STRING = 0;
|
||||
BINARY = 1;
|
||||
}
|
||||
required PayloadType payload_type = 5;
|
||||
// Depending on payload_type, exactly one of the following optional fields
|
||||
// will always be set.
|
||||
optional string payload_utf8 = 6;
|
||||
optional bytes payload_binary = 7;
|
||||
}
|
||||
enum SignatureAlgorithm {
|
||||
UNSPECIFIED = 0;
|
||||
RSASSA_PKCS1v15 = 1;
|
||||
RSASSA_PSS = 2;
|
||||
}
|
||||
enum HashAlgorithm {
|
||||
SHA1 = 0;
|
||||
SHA256 = 1;
|
||||
}
|
||||
// Messages for authentication protocol between a sender and a receiver.
|
||||
message AuthChallenge {
|
||||
optional SignatureAlgorithm signature_algorithm = 1
|
||||
[default = RSASSA_PKCS1v15];
|
||||
optional bytes sender_nonce = 2;
|
||||
optional HashAlgorithm hash_algorithm = 3 [default = SHA1];
|
||||
}
|
||||
message AuthResponse {
|
||||
required bytes signature = 1;
|
||||
required bytes client_auth_certificate = 2;
|
||||
repeated bytes intermediate_certificate = 3;
|
||||
optional SignatureAlgorithm signature_algorithm = 4
|
||||
[default = RSASSA_PKCS1v15];
|
||||
optional bytes sender_nonce = 5;
|
||||
optional HashAlgorithm hash_algorithm = 6 [default = SHA1];
|
||||
optional bytes crl = 7;
|
||||
}
|
||||
message AuthError {
|
||||
enum ErrorType {
|
||||
INTERNAL_ERROR = 0;
|
||||
NO_TLS = 1; // The underlying connection is not TLS
|
||||
SIGNATURE_ALGORITHM_UNAVAILABLE = 2;
|
||||
}
|
||||
required ErrorType error_type = 1;
|
||||
}
|
||||
message DeviceAuthMessage {
|
||||
// Request fields
|
||||
optional AuthChallenge challenge = 1;
|
||||
// Response fields
|
||||
optional AuthResponse response = 2;
|
||||
optional AuthError error = 3;
|
||||
}
|
6
app/src/main/res/drawable/background_comment.xml
Normal file
6
app/src/main/res/drawable/background_comment.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#1A1A1A" />
|
||||
<corners android:radius="4dp" />
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
7
app/src/main/res/drawable/background_pill_pred.xml
Normal file
7
app/src/main/res/drawable/background_pill_pred.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#A03D3D" />
|
||||
<corners android:radius="500dp" />
|
||||
<size android:height="20dp" />
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
9
app/src/main/res/drawable/ic_chat_filled.xml
Normal file
9
app/src/main/res/drawable/ic_chat_filled.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M289.23,649.23q-13.08,0 -21.92,-8.85 -8.85,-8.85 -8.85,-21.92v-40h470l25.38,25.38L753.85,240h40q13.08,0 21.92,8.85 8.85,8.85 8.85,21.92v501.54L701.54,649.23L289.23,649.23ZM135.38,621.54v-470.77q0,-13.08 8.85,-21.92Q153.08,120 166.15,120h476.92q13.08,0 21.92,8.85 8.85,8.85 8.85,21.92v316.92q0,13.08 -8.85,21.92 -8.85,8.85 -21.92,8.85L258.46,498.46L135.38,621.54Z"/>
|
||||
</vector>
|
12
app/src/main/res/drawable/ic_fcast.xml
Normal file
12
app/src/main/res/drawable/ic_fcast.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="97dp"
|
||||
android:height="97dp"
|
||||
android:viewportWidth="97"
|
||||
android:viewportHeight="97">
|
||||
<path
|
||||
android:pathData="M20,0L77,0A20,20 0,0 1,97 20L97,77A20,20 0,0 1,77 97L20,97A20,20 0,0 1,0 77L0,20A20,20 0,0 1,20 0z"
|
||||
android:fillColor="#D9D9D9"/>
|
||||
<path
|
||||
android:pathData="M17.03,67V30.636H42.598V38.591H26.902V44.841H41.036V52.796H26.902V67H17.03ZM80.178,44.273H70.164C70.093,43.444 69.904,42.693 69.596,42.018C69.3,41.343 68.886,40.763 68.353,40.278C67.832,39.78 67.199,39.402 66.453,39.141C65.707,38.869 64.861,38.733 63.914,38.733C62.257,38.733 60.854,39.135 59.706,39.94C58.57,40.745 57.706,41.899 57.114,43.403C56.534,44.906 56.244,46.711 56.244,48.818C56.244,51.044 56.54,52.908 57.132,54.411C57.735,55.903 58.605,57.027 59.742,57.785C60.878,58.53 62.245,58.903 63.843,58.903C64.755,58.903 65.571,58.791 66.293,58.566C67.016,58.329 67.643,57.992 68.175,57.554C68.708,57.116 69.14,56.589 69.472,55.974C69.815,55.346 70.046,54.642 70.164,53.861L80.178,53.932C80.06,55.471 79.628,57.039 78.882,58.637C78.136,60.223 77.077,61.691 75.704,63.041C74.343,64.378 72.656,65.455 70.644,66.272C68.631,67.089 66.293,67.497 63.63,67.497C60.292,67.497 57.297,66.781 54.646,65.349C52.006,63.916 49.917,61.809 48.378,59.028C46.851,56.246 46.087,52.843 46.087,48.818C46.087,44.77 46.869,41.361 48.431,38.591C49.994,35.809 52.101,33.708 54.752,32.288C57.404,30.855 60.363,30.139 63.63,30.139C65.926,30.139 68.039,30.453 69.969,31.08C71.898,31.708 73.591,32.625 75.047,33.832C76.503,35.028 77.675,36.502 78.563,38.254C79.45,40.005 79.989,42.012 80.178,44.273Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
121
app/src/main/res/layout/activity_fcast_guide.xml
Normal file
121
app/src/main/res/layout/activity_fcast_guide.xml
Normal file
|
@ -0,0 +1,121 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:background="@color/black">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_back"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:padding="10dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_back_thin_white_16dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="4dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
app:srcCompat="@drawable/ic_fcast" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_polycentric"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/fcast"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:includeFontPadding="false"
|
||||
android:textSize="32dp"
|
||||
android:layout_marginLeft="12dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/text_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:animateLayoutChanges="true"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/background_videodetail_description"
|
||||
android:layout_marginLeft="14dp"
|
||||
android:layout_marginRight="14dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:paddingTop="3dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingRight="12dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_back"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/layout_buttons">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.futo.platformplayer.views.behavior.NonScrollingTextView
|
||||
android:id="@+id/text_explanation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:background="@color/transparent"
|
||||
android:textSize="14sp" />
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_buttons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_website"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:buttonText="@string/fcast_website"
|
||||
app:buttonSubText="@string/open_the_fcast_website"
|
||||
app:buttonIcon="@drawable/ic_link" />
|
||||
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_technical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:buttonText="@string/fcast_technical_documentation"
|
||||
app:buttonSubText="@string/view_the_fcast_technical_documentation"
|
||||
app:buttonIcon="@drawable/ic_wrench"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_close"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:buttonText="@string/close"
|
||||
app:buttonSubText="@string/go_back_to_casting_add_dialog"
|
||||
app:buttonIcon="@drawable/ic_close"
|
||||
android:layout_marginTop="8dp" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,89 +1,105 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:background="@color/black">
|
||||
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="20dp"
|
||||
android:paddingBottom="15dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_back"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingRight="20dp"
|
||||
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings"
|
||||
android:textSize="24dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_extra_light" />
|
||||
</FrameLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:background="@color/black">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<com.futo.platformplayer.views.Loader
|
||||
android:id="@+id/loader"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:layout_marginTop="15dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp" />
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="20dp"
|
||||
android:paddingBottom="15dp">
|
||||
|
||||
<com.futo.platformplayer.views.fields.FieldForm
|
||||
android:id="@+id/settings_form"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
<ImageButton
|
||||
android:id="@+id/button_back"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingRight="20dp"
|
||||
app:srcCompat="@drawable/ic_back_thin_white_16dp" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings"
|
||||
android:textSize="24dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_extra_light" />
|
||||
</FrameLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:id="@+id/dev_settings"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
<com.futo.platformplayer.views.LoaderView
|
||||
android:id="@+id/loader"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:layout_marginTop="15dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14dp"
|
||||
android:textAlignment="center"
|
||||
android:layout_margin="5dp"
|
||||
android:text="@string/you_re_apparantly_a_developer" />
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_dev"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/developer_settings" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
android:layout_height="60dp" />
|
||||
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
<com.futo.platformplayer.views.fields.FieldForm
|
||||
android:id="@+id/settings_form"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/dev_settings"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14dp"
|
||||
android:textAlignment="center"
|
||||
android:layout_margin="5dp"
|
||||
android:text="@string/you_re_apparantly_a_developer" />
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_dev"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/developer_settings" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/overlay_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:visibility="gone"
|
||||
android:elevation="15dp">
|
||||
</FrameLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -8,13 +8,31 @@
|
|||
android:background="@color/gray_1d"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/add_casting_device"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular" />
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/add_casting_device"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/button_tutorial"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Help"
|
||||
android:textSize="14dp"
|
||||
android:padding="8dp"
|
||||
android:gravity="end|center_vertical"
|
||||
android:textColor="@color/primary"
|
||||
android:fontFamily="@font/inter_regular" />
|
||||
</LinearLayout>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_type"
|
||||
|
|
|
@ -89,18 +89,32 @@
|
|||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular" />
|
||||
|
||||
<Space android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
<Button
|
||||
android:id="@+id/button_scan_qr"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1.7"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/scan_qr"
|
||||
android:textSize="14dp"
|
||||
android:textAlignment="center"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:background="@color/transparent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_add"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/add"
|
||||
android:textSize="14dp"
|
||||
android:textAlignment="textEnd"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:background="@color/transparent" />
|
||||
|
|
68
app/src/main/res/layout/dialog_casting_help.xml
Normal file
68
app/src/main/res/layout/dialog_casting_help.xml
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/gray_1d"
|
||||
android:padding="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_buttons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_video"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:buttonText="@string/video"
|
||||
app:buttonSubText="@string/view_a_video_about_how_to_cast"
|
||||
app:buttonIcon="@drawable/ic_smart_display"
|
||||
android:layout_marginTop="8dp"
|
||||
app:buttonBackground="@drawable/background_big_button_black"
|
||||
android:alpha="0.4" />
|
||||
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_guide"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:buttonText="FCast Guide"
|
||||
app:buttonSubText="@string/how_to_use_fcast_guide"
|
||||
app:buttonIcon="@drawable/ic_code"
|
||||
android:layout_marginTop="8dp"
|
||||
app:buttonBackground="@drawable/background_big_button_black" />
|
||||
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_website"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:buttonText="@string/fcast_website"
|
||||
app:buttonSubText="@string/open_the_fcast_website"
|
||||
app:buttonIcon="@drawable/ic_link"
|
||||
android:layout_marginTop="8dp"
|
||||
app:buttonBackground="@drawable/background_big_button_black" />
|
||||
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_technical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:buttonText="@string/fcast_technical_documentation"
|
||||
app:buttonSubText="@string/view_the_fcast_technical_documentation"
|
||||
app:buttonIcon="@drawable/ic_wrench"
|
||||
android:layout_marginTop="8dp"
|
||||
app:buttonBackground="@drawable/background_big_button_black" />
|
||||
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_close"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:buttonText="@string/close"
|
||||
app:buttonSubText="@string/go_back_to_casting_add_dialog"
|
||||
app:buttonIcon="@drawable/ic_close"
|
||||
android:layout_marginTop="8dp"
|
||||
app:buttonBackground="@drawable/background_big_button_black"/>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
|
@ -4,7 +4,7 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:background="@color/gray_1d">
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:paddingTop="40dp">
|
||||
android:paddingTop="20dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -21,8 +21,8 @@
|
|||
|
||||
<ImageView
|
||||
android:id="@+id/update_spinner"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="70dp"
|
||||
app:srcCompat="@drawable/ic_move_up" />
|
||||
|
||||
<TextView
|
||||
|
@ -46,7 +46,19 @@
|
|||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginTop="30dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="You can open and share files directly to Grayjay as well."
|
||||
android:textAlignment="center"
|
||||
android:textSize="13dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
|
@ -55,39 +67,47 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="10dp"
|
||||
android:layout_marginTop="28dp"
|
||||
android:layout_marginBottom="28dp">
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingTop="10dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="10dp">
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_import_zip"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleY="0.9"
|
||||
android:scaleX="0.9"
|
||||
app:buttonIcon="@drawable/ic_zip"
|
||||
app:buttonText="Import Grayjay export (.zip)"
|
||||
android:layout_margin="5dp"
|
||||
app:buttonBackground="@drawable/background_big_button_black"
|
||||
app:buttonSubText="Pick a Grayjay export zip file" />
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_import_ezip"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:buttonIcon="@drawable/ic_encrypted"
|
||||
android:scaleY="0.9"
|
||||
android:scaleX="0.9"
|
||||
android:alpha="0.5"
|
||||
app:buttonBackground="@drawable/background_big_button_black"
|
||||
app:buttonText="Import Grayjay Auto-Backup (.ezip)"
|
||||
android:layout_margin="5dp"
|
||||
app:buttonSubText="Pick a Grayjay auto-backup encrypted zip file" />
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_import_txt"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:scaleY="0.9"
|
||||
android:scaleX="0.9"
|
||||
app:buttonIcon="@drawable/ic_lines"
|
||||
android:alpha="0.5"
|
||||
app:buttonBackground="@drawable/background_big_button_black"
|
||||
app:buttonText="Import Line Text file (.txt)"
|
||||
app:buttonSubText="Pick a text file with one entry per line" />
|
||||
<com.futo.platformplayer.views.buttons.BigButton
|
||||
android:id="@+id/button_import_newpipe_subs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:scaleY="0.9"
|
||||
android:scaleX="0.9"
|
||||
app:buttonIcon="@drawable/ic_play"
|
||||
app:buttonBackground="@drawable/background_big_button_black"
|
||||
app:buttonText="Import NewPipe Subscriptions (.json)"
|
||||
|
@ -98,7 +118,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/close"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:textSize="14dp"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/colorPrimary"
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:textColor="#AAAAAA"
|
||||
android:fontFamily="monospace"
|
||||
android:text="source.getVideoDetails(...)"
|
||||
android:text=""
|
||||
android:textAlignment="center"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp"
|
||||
|
@ -58,6 +58,7 @@
|
|||
android:padding="5dp"
|
||||
android:background="#111111"
|
||||
android:textSize="8dp"
|
||||
android:visibility="gone"
|
||||
android:layout_height="wrap_content"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
|
|
86
app/src/main/res/layout/dialog_url_handling.xml
Normal file
86
app/src/main/res/layout/dialog_url_handling.xml
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?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:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:background="@color/gray_1d">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialog_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/allow_grayjay_to_handle_specific_urls"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textAlignment="center"
|
||||
android:layout_marginTop="25dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
<TextView
|
||||
android:id="@+id/dialog_text_details"
|
||||
android:layout_width="match_parent"
|
||||
android:textColor="#AAAAAA"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:text="@string/allow_grayjay_to_handle_specific_urls_please_set_it_as_default_in_the_app_settings"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:textSize="12dp"
|
||||
android:layout_height="wrap_content" />
|
||||
<LinearLayout
|
||||
android:id="@+id/dialog_buttons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical|end"
|
||||
android:layout_marginTop="14dp"
|
||||
android:layout_marginBottom="28dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/button_no"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/no"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/primary"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingStart="32dp"
|
||||
android:paddingEnd="32dp"
|
||||
android:layout_marginEnd="16dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_yes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_button_primary"
|
||||
android:layout_marginEnd="28dp"
|
||||
android:clickable="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/yes"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingStart="28dp"
|
||||
android:paddingEnd="28dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
131
app/src/main/res/layout/fragment_comments.xml
Normal file
131
app/src/main/res/layout/fragment_comments.xml
Normal file
|
@ -0,0 +1,131 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="24dp"
|
||||
android:text="@string/comments"
|
||||
android:fontFamily="@font/inter_extra_light"
|
||||
android:textColor="@color/white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_comment_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12dp"
|
||||
android:text="@string/these_are_all_commentcount_comments_you_have_made_in_grayjay"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="#808080" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/gray_ac"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/sort_by" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_sortby"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="12dp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_comments"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||
android:id="@+id/replies_overlay"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<LinearLayout android:id="@+id/layout_not_logged_in"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/black">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login_to_view_your_comments"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingStart="28dp"
|
||||
android:paddingEnd="28dp"
|
||||
android:layout_marginBottom="20dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_login"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_button_primary"
|
||||
android:clickable="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingStart="28dp"
|
||||
android:paddingEnd="28dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout android:id="@+id/layout_polycentric_disabled"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/black">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/polycentric_is_disabled"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingStart="28dp"
|
||||
android:paddingEnd="28dp"
|
||||
android:layout_marginBottom="20dp"/>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
|
@ -16,33 +16,67 @@
|
|||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_height="100dp"
|
||||
android:minHeight="0dp"
|
||||
app:layout_scrollFlags="scroll"
|
||||
app:contentInsetStart="0dp"
|
||||
app:contentInsetEnd="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
<!--Search Text-->
|
||||
<FrameLayout
|
||||
android:id="@+id/container_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/gray_ac"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/sort_by"
|
||||
android:paddingStart="20dp" />
|
||||
android:visibility="visible"
|
||||
android:layout_margin="10dp">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_sortby"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
<EditText
|
||||
android:id="@+id/edit_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:imeOptions="actionDone"
|
||||
android:singleLine="true"
|
||||
android:hint="Search"
|
||||
android:paddingEnd="46dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_clear_search"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="18dp"
|
||||
android:paddingEnd="18dp"
|
||||
android:layout_gravity="right|center_vertical"
|
||||
android:visibility="invisible"
|
||||
android:src="@drawable/ic_clear_16dp" />
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp" />
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/gray_ac"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/sort_by"
|
||||
android:paddingStart="20dp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_sortby"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_body"
|
||||
|
@ -136,6 +136,30 @@
|
|||
app:pillIcon="@drawable/ic_forum"
|
||||
app:pillText="55 Replies"
|
||||
android:layout_marginStart="15dp" />
|
||||
|
||||
<Space android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/button_delete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_pill_pred"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
<TextView
|
||||
android:id="@+id/pill_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="13dp"
|
||||
android:gravity="center_vertical"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/delete" />
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
|
|
124
app/src/main/res/layout/list_comment_with_reference.xml
Normal file
124
app/src/main/res/layout/list_comment_with_reference.xml
Normal file
|
@ -0,0 +1,124 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/layout_comment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:layout_marginStart="14dp"
|
||||
android:layout_marginEnd="14dp"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/background_comment"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
android:id="@+id/image_thumbnail"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
android:contentDescription="@string/channel_image"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/placeholder_channel_thumbnail" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_author"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintTop_toTopOf="@id/image_thumbnail"
|
||||
tools:text="ShortCircuit" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_metadata"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/gray_ac"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/text_author"
|
||||
app:layout_constraintLeft_toRightOf="@id/text_author"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/text_author"
|
||||
tools:text=" • 3 years ago" />
|
||||
|
||||
<com.futo.platformplayer.views.behavior.NonScrollingTextView
|
||||
android:id="@+id/text_body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:background="@color/transparent"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:isScrollContainer="false"
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="13sp"
|
||||
android:maxLines="100"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_metadata"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
tools:text="@string/lorem_ipsum" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_body"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||
android:id="@+id/rating"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginStart="10dp" />
|
||||
|
||||
<com.futo.platformplayer.views.pills.PillButton
|
||||
android:id="@+id/button_replies"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:pillIcon="@drawable/ic_forum"
|
||||
app:pillText="55 Replies"
|
||||
android:layout_marginStart="15dp" />
|
||||
|
||||
<Space android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/button_delete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_pill_pred"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
<TextView
|
||||
android:id="@+id/pill_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="13dp"
|
||||
android:gravity="center_vertical"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/delete" />
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -127,7 +127,7 @@
|
|||
android:visibility="gone"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
<com.futo.platformplayer.views.Loader
|
||||
<com.futo.platformplayer.views.LoaderView
|
||||
android:id="@+id/loader"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp" />
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:background="@color/black"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
|
@ -15,6 +16,78 @@
|
|||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/layout_parent_comment"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/topbar"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:padding="12dp"
|
||||
android:background="@drawable/background_16_round_4dp">
|
||||
|
||||
<com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
android:id="@+id/image_thumbnail"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
android:contentDescription="@string/channel_image"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/placeholder_channel_thumbnail" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_author"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintTop_toTopOf="@id/image_thumbnail"
|
||||
android:text="ShortCircuit" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_metadata"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/gray_ac"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/text_author"
|
||||
app:layout_constraintLeft_toRightOf="@id/text_author"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/text_author"
|
||||
android:text=" • 3 years ago" />
|
||||
|
||||
<com.futo.platformplayer.views.behavior.NonScrollingTextView
|
||||
android:id="@+id/text_body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:background="@color/transparent"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:isScrollContainer="false"
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="13sp"
|
||||
android:maxLines="3"
|
||||
android:ellipsize="end"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_metadata"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:text="@string/lorem_ipsum" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.futo.platformplayer.views.comments.AddCommentView
|
||||
android:id="@+id/add_comment_view"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -22,8 +95,7 @@
|
|||
android:layout_marginTop="12dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/topbar"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_parent_comment"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
|
@ -32,6 +104,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_comment_view"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginTop="12dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -9,12 +9,13 @@
|
|||
android:paddingStart="7dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:background="@drawable/background_pill"
|
||||
android:id="@+id/root">
|
||||
android:id="@+id/root"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pill_icon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginTop="0dp"
|
||||
|
@ -31,4 +32,10 @@
|
|||
android:fontFamily="@font/inter_light"
|
||||
tools:text="500K" />
|
||||
|
||||
<com.futo.platformplayer.views.LoaderView
|
||||
android:id="@+id/loader"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
app:isWhite="true" />
|
||||
|
||||
</LinearLayout>
|
|
@ -8,7 +8,8 @@
|
|||
android:paddingBottom="7dp"
|
||||
android:paddingLeft="7dp"
|
||||
android:paddingRight="12dp"
|
||||
android:background="@drawable/background_pill">
|
||||
android:background="@drawable/background_pill"
|
||||
android:gravity="center_vertical">
|
||||
<ImageView
|
||||
android:id="@+id/pill_like_icon"
|
||||
android:layout_width="30dp"
|
||||
|
@ -22,6 +23,11 @@
|
|||
android:textSize="13dp"
|
||||
android:gravity="center_vertical"
|
||||
tools:text="500K" />
|
||||
<com.futo.platformplayer.views.LoaderView
|
||||
android:id="@+id/loader_likes"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
app:isWhite="true" />
|
||||
|
||||
<View
|
||||
android:id="@+id/pill_seperator"
|
||||
|
@ -44,5 +50,10 @@
|
|||
android:gravity="center_vertical"
|
||||
android:textSize="13dp"
|
||||
tools:text="500K" />
|
||||
<com.futo.platformplayer.views.LoaderView
|
||||
android:id="@+id/loader_dislikes"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
app:isWhite="true" />
|
||||
|
||||
</LinearLayout>
|
|
@ -23,6 +23,11 @@
|
|||
android:background="#cc000000"
|
||||
android:layout_marginBottom="6dp" />
|
||||
|
||||
<com.futo.platformplayer.views.behavior.GestureControlView
|
||||
android:id="@+id/gesture_control"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_minimize"
|
||||
android:layout_width="50dp"
|
||||
|
@ -129,11 +134,6 @@
|
|||
app:layout_constraintTop_toTopOf="@id/text_position"
|
||||
app:layout_constraintBottom_toBottomOf="@id/text_position"/>
|
||||
|
||||
<com.futo.platformplayer.views.behavior.GestureControlView
|
||||
android:id="@+id/gesture_control"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.google.android.exoplayer2.ui.DefaultTimeBar
|
||||
android:id="@+id/time_progress"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_comments"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
|
@ -120,7 +120,7 @@
|
|||
android:orientation="horizontal"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<com.futo.platformplayer.views.Loader
|
||||
<com.futo.platformplayer.views.LoaderView
|
||||
android:id="@+id/loader_merchandise"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
|
|
|
@ -722,4 +722,8 @@
|
|||
<item>معلومات</item>
|
||||
<item>تفصيلي</item>
|
||||
</string-array>
|
||||
<string-array name="comments_sortby_array">
|
||||
<item>Newest</item>
|
||||
<item>Oldest</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
|
|
@ -722,4 +722,8 @@
|
|||
<item>Information</item>
|
||||
<item>Ausführlich</item>
|
||||
</string-array>
|
||||
<string-array name="comments_sortby_array">
|
||||
<item>Newest</item>
|
||||
<item>Oldest</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
|
|
@ -738,4 +738,8 @@
|
|||
<item>Información</item>
|
||||
<item>Detallado</item>
|
||||
</string-array>
|
||||
<string-array name="comments_sortby_array">
|
||||
<item>Newest</item>
|
||||
<item>Oldest</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
|
|
@ -722,4 +722,8 @@
|
|||
<item>Information</item>
|
||||
<item>Verbeux</item>
|
||||
</string-array>
|
||||
<string-array name="comments_sortby_array">
|
||||
<item>Newest</item>
|
||||
<item>Oldest</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
|
|
@ -722,4 +722,8 @@
|
|||
<item>情報</item>
|
||||
<item>詳細</item>
|
||||
</string-array>
|
||||
<string-array name="comments_sortby_array">
|
||||
<item>Newest</item>
|
||||
<item>Oldest</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
|
|
@ -722,4 +722,8 @@
|
|||
<item>정보</item>
|
||||
<item>상세</item>
|
||||
</string-array>
|
||||
<string-array name="comments_sortby_array">
|
||||
<item>Newest</item>
|
||||
<item>Oldest</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
|
|
@ -722,4 +722,8 @@
|
|||
<item>Informação</item>
|
||||
<item>Detalhado</item>
|
||||
</string-array>
|
||||
<string-array name="comments_sortby_array">
|
||||
<item>Newest</item>
|
||||
<item>Oldest</item>
|
||||
</string-array>
|
||||
</resources>
|
|
@ -722,4 +722,8 @@
|
|||
<item>Информация</item>
|
||||
<item>Подробно</item>
|
||||
</string-array>
|
||||
<string-array name="comments_sortby_array">
|
||||
<item>Newest</item>
|
||||
<item>Oldest</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
|
|
@ -722,4 +722,8 @@
|
|||
<item>信息</item>
|
||||
<item>详细</item>
|
||||
</string-array>
|
||||
<string-array name="comments_sortby_array">
|
||||
<item>Newest</item>
|
||||
<item>Oldest</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
<resources>
|
||||
<declare-styleable name="LoaderView">
|
||||
<attr name="automatic" format="boolean" />
|
||||
<attr name="isWhite" format="boolean" />
|
||||
</declare-styleable>
|
||||
</resources>
|
|
@ -292,6 +292,8 @@
|
|||
<string name="clear_external_downloads_directory">Clear external Downloads directory</string>
|
||||
<string name="change_external_general_directory">Change external General directory</string>
|
||||
<string name="change_tabs_visible_on_the_home_screen">Change tabs visible on the home screen</string>
|
||||
<string name="link_handling">Link Handling</string>
|
||||
<string name="allow_grayjay_to_handle_links">Allow Grayjay to handle links</string>
|
||||
<string name="change_the_external_directory_for_general_files">Change the external directory for general files</string>
|
||||
<string name="clear_the_external_storage_for_download_files">Clear the external storage for download files</string>
|
||||
<string name="change_the_external_storage_for_download_files">Change the external storage for download files</string>
|
||||
|
@ -354,6 +356,8 @@
|
|||
<string name="payment">Payment</string>
|
||||
<string name="payment_status">Payment Status</string>
|
||||
<string name="bypass_rotation_prevention">Bypass Rotation Prevention</string>
|
||||
<string name="enable_polycentric">Enable Polycentric</string>
|
||||
<string name="can_be_disabled_when_you_are_experiencing_issues">Can be disabled when you are experiencing issues</string>
|
||||
<string name="bypass_rotation_prevention_description">Allows for rotation on non-video views.\nWARNING: Not designed for it</string>
|
||||
<string name="bypass_rotation_prevention_warning">This may cause unexpected behavior, and is mostly untested.</string>
|
||||
<string name="player">Player</string>
|
||||
|
@ -366,6 +370,8 @@
|
|||
<string name="preferred_preview_quality_description">Default quality while previewing a video in a feed</string>
|
||||
<string name="primary_language">Primary Language</string>
|
||||
<string name="default_comment_section">Default Comment Section</string>
|
||||
<string name="bad_reputation_comments_fading">Bad Reputation Comment Fading</string>
|
||||
<string name="bad_reputation_comments_fading_description">If comments with a very bad reputation should be faded. Disabling may worsen experience.</string>
|
||||
<string name="reinstall_embedded_plugins">Reinstall Embedded Plugins</string>
|
||||
<string name="remove_cached_version">Remove Cached Version</string>
|
||||
<string name="remove_the_last_downloaded_version">Remove the last downloaded version</string>
|
||||
|
@ -377,6 +383,10 @@
|
|||
<string name="restore_a_previous_automatic_backup">Restore a previous automatic backup</string>
|
||||
<string name="resume_after_preview">Resume After Preview</string>
|
||||
<string name="review_the_current_and_past_changelogs">Review the current and past changelogs</string>
|
||||
<string name="restart_after_audio_focus_loss">Restart after audio focus loss</string>
|
||||
<string name="restart_playback_when_gaining_audio_focus_after_a_loss">Restart playback when gaining audio focus after a loss</string>
|
||||
<string name="restart_after_connectivity_loss">Restart after connectivity loss</string>
|
||||
<string name="restart_playback_when_gaining_connectivity_after_a_loss">Restart playback when gaining connectivity after a loss</string>
|
||||
<string name="chapter_update_fps_title">Chapter Update FPS</string>
|
||||
<string name="chapter_update_fps_description">Change accuracy of chapter updating, higher might cost more performance</string>
|
||||
<string name="set_automatic_backup">Set Automatic Backup</string>
|
||||
|
@ -682,6 +692,24 @@
|
|||
<string name="plus_tax">" + Tax"</string>
|
||||
<string name="new_playlist">New playlist</string>
|
||||
<string name="add_to_new_playlist">Add to new playlist</string>
|
||||
<string name="url_handling">URL Handling</string>
|
||||
<string name="allow_grayjay_to_handle_specific_urls">Allow Grayjay to handle specific URLs?</string>
|
||||
<string name="allow_grayjay_to_handle_specific_urls_please_set_it_as_default_in_the_app_settings">When you click \'Yes\', the Grayjay app settings will open.\n\nThere, navigate to:\n1. "Open by default" or "Set as default" section.\nYou might find this option directly or under \'Advanced\' settings, depending on your device.\n\n2. Choose \'Open supported links\' for Grayjay.\n\n(some devices have this listed under \'Default Apps\' in the main settings followed by selecting Grayjay for relevant categories)</string>
|
||||
<string name="failed_to_show_settings">Failed to show settings</string>
|
||||
<string name="play_store_version_does_not_support_default_url_handling">Play store version does not support default URL handling.</string>
|
||||
<string name="these_are_all_commentcount_comments_you_have_made_in_grayjay">These are all {commentCount} comments you have made in Grayjay.</string>
|
||||
<string name="tutorial">Tutorial</string>
|
||||
<string name="go_back_to_casting_add_dialog">Go back to casting add dialog</string>
|
||||
<string name="view_a_video_about_how_to_cast">View a video about how to cast</string>
|
||||
<string name="view_the_fcast_technical_documentation">View the FCast technical documentation</string>
|
||||
<string name="guide">Guide</string>
|
||||
<string name="how_to_use_fcast_guide">How to use FCast guide</string>
|
||||
<string name="fcast">FCast</string>
|
||||
<string name="open_the_fcast_website">Open the FCast website</string>
|
||||
<string name="fcast_website">FCast Website</string>
|
||||
<string name="fcast_technical_documentation">FCast Technical Documentation</string>
|
||||
<string name="login_to_view_your_comments">Login to view your comments</string>
|
||||
<string name="polycentric_is_disabled">Polycentric is disabled</string>
|
||||
<string-array name="home_screen_array">
|
||||
<item>Recommendations</item>
|
||||
<item>Subscriptions</item>
|
||||
|
@ -769,6 +797,10 @@
|
|||
<item>Disabled</item>
|
||||
<item>Enabled</item>
|
||||
</string-array>
|
||||
<string-array name="comments_sortby_array">
|
||||
<item>Newest</item>
|
||||
<item>Oldest</item>
|
||||
</string-array>
|
||||
<string-array name="subscriptions_sortby_array">
|
||||
<item>Name Ascending</item>
|
||||
<item>Name Descending</item>
|
||||
|
@ -829,7 +861,7 @@
|
|||
<item>Russian</item>
|
||||
</string-array>
|
||||
<string-array name="casting_device_type_array" translatable="false">
|
||||
<item>FastCast</item>
|
||||
<item>FCast</item>
|
||||
<item>ChromeCast</item>
|
||||
<item>AirPlay</item>
|
||||
</string-array>
|
||||
|
@ -840,4 +872,10 @@
|
|||
<item>Information</item>
|
||||
<item>Verbose</item>
|
||||
</string-array>
|
||||
<string-array name="restart_playback_after_loss">
|
||||
<item>Never</item>
|
||||
<item>Within 10 seconds of loss</item>
|
||||
<item>Within 30 seconds of loss</item>
|
||||
<item>Always</item>
|
||||
</string-array>
|
||||
</resources>
|
|
@ -24,6 +24,12 @@
|
|||
<data android:host="www.youtube.com" />
|
||||
<data android:host="m.youtube.com" />
|
||||
<data android:host="rumble.com" />
|
||||
<data android:host="kick.com" />
|
||||
<data android:host="nebula.tv" />
|
||||
<data android:host="odysee.com" />
|
||||
<data android:host="patreon.com" />
|
||||
<data android:host="soundcloud.com" />
|
||||
<data android:host="twitch.tv" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
|
@ -33,11 +39,18 @@
|
|||
|
||||
<data android:mimeType="text/plain" />
|
||||
|
||||
<data android:host="youtube.com" />
|
||||
<data android:host="m.youtube.com" />
|
||||
<data android:host="you.be" />
|
||||
<data android:host="youtu.be" />
|
||||
<data android:host="www.you.be" />
|
||||
<data android:host="youtube.com" />
|
||||
<data android:host="www.youtube.com" />
|
||||
<data android:host="m.youtube.com" />
|
||||
<data android:host="rumble.com" />
|
||||
<data android:host="kick.com" />
|
||||
<data android:host="nebula.tv" />
|
||||
<data android:host="odysee.com" />
|
||||
<data android:host="patreon.com" />
|
||||
<data android:host="soundcloud.com" />
|
||||
<data android:host="twitch.tv" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 37fd342a760be6351b042732a4052bd54d723eb0
|
||||
Subproject commit 07aa5a9aab441657f89ae14ff3cfd9d9ca977fe6
|
244
docs/Example Plugin.md
Normal file
244
docs/Example Plugin.md
Normal file
|
@ -0,0 +1,244 @@
|
|||
# Example plugin
|
||||
|
||||
Note that this is just a starting point, plugins can also implement optional features such as login, importing playlists/subscriptions, etc. For full examples please see in-house developed plugins (click [here](https://gitlab.futo.org/videostreaming/plugins)).
|
||||
|
||||
```js
|
||||
source.enable = function (conf) {
|
||||
/**
|
||||
* @param conf: SourceV8PluginConfig (the SomeConfig.js)
|
||||
*/
|
||||
}
|
||||
|
||||
source.getHome = function(continuationToken) {
|
||||
/**
|
||||
* @param continuationToken: any?
|
||||
* @returns: VideoPager
|
||||
*/
|
||||
const videos = []; // The results (PlatformVideo)
|
||||
const hasMore = false; // Are there more pages?
|
||||
const context = { continuationToken: continuationToken }; // Relevant data for the next page
|
||||
return new SomeHomeVideoPager(videos, hasMore, context);
|
||||
}
|
||||
|
||||
source.searchSuggestions = function(query) {
|
||||
/**
|
||||
* @param query: string
|
||||
* @returns: string[]
|
||||
*/
|
||||
|
||||
const suggestions = []; //The suggestions for a specific search query
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
source.getSearchCapabilities = function() {
|
||||
//This is an example of how to return search capabilities like available sorts, filters and which feed types are available (see source.js for more details)
|
||||
return {
|
||||
types: [Type.Feed.Mixed],
|
||||
sorts: [Type.Order.Chronological, "^release_time"],
|
||||
filters: [
|
||||
{
|
||||
id: "date",
|
||||
name: "Date",
|
||||
isMultiSelect: false,
|
||||
filters: [
|
||||
{ id: Type.Date.Today, name: "Last 24 hours", value: "today" },
|
||||
{ id: Type.Date.LastWeek, name: "Last week", value: "thisweek" },
|
||||
{ id: Type.Date.LastMonth, name: "Last month", value: "thismonth" },
|
||||
{ id: Type.Date.LastYear, name: "Last year", value: "thisyear" }
|
||||
]
|
||||
},
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
source.search = function (query, type, order, filters, continuationToken) {
|
||||
/**
|
||||
* @param query: string
|
||||
* @param type: string
|
||||
* @param order: string
|
||||
* @param filters: Map<string, Array<string>>
|
||||
* @param continuationToken: any?
|
||||
* @returns: VideoPager
|
||||
*/
|
||||
const videos = []; // The results (PlatformVideo)
|
||||
const hasMore = false; // Are there more pages?
|
||||
const context = { query: query, type: type, order: order, filters: filters, continuationToken: continuationToken }; // Relevant data for the next page
|
||||
return new SomeSearchVideoPager(videos, hasMore, context);
|
||||
}
|
||||
|
||||
source.getSearchChannelContentsCapabilities = function () {
|
||||
//This is an example of how to return search capabilities on a channel like available sorts, filters and which feed types are available (see source.js for more details)
|
||||
return {
|
||||
types: [Type.Feed.Mixed],
|
||||
sorts: [Type.Order.Chronological],
|
||||
filters: []
|
||||
};
|
||||
}
|
||||
|
||||
source.searchChannelContents = function (url, query, type, order, filters, continuationToken) {
|
||||
/**
|
||||
* @param url: string
|
||||
* @param query: string
|
||||
* @param type: string
|
||||
* @param order: string
|
||||
* @param filters: Map<string, Array<string>>
|
||||
* @param continuationToken: any?
|
||||
* @returns: VideoPager
|
||||
*/
|
||||
|
||||
const videos = []; // The results (PlatformVideo)
|
||||
const hasMore = false; // Are there more pages?
|
||||
const context = { channelUrl: channelUrl, query: query, type: type, order: order, filters: filters, continuationToken: continuationToken }; // Relevant data for the next page
|
||||
return new SomeSearchChannelVideoPager(videos, hasMore, context);
|
||||
}
|
||||
|
||||
source.searchChannels = function (query, continuationToken) {
|
||||
/**
|
||||
* @param query: string
|
||||
* @param continuationToken: any?
|
||||
* @returns: ChannelPager
|
||||
*/
|
||||
|
||||
const channels = []; // The results (PlatformChannel)
|
||||
const hasMore = false; // Are there more pages?
|
||||
const context = { query: query, continuationToken: continuationToken }; // Relevant data for the next page
|
||||
return new SomeChannelPager(channels, hasMore, context);
|
||||
}
|
||||
|
||||
source.isChannelUrl = function(url) {
|
||||
/**
|
||||
* @param url: string
|
||||
* @returns: boolean
|
||||
*/
|
||||
|
||||
return REGEX_CHANNEL_URL.test(url);
|
||||
}
|
||||
|
||||
source.getChannel = function(url) {
|
||||
return new PlatformChannel({
|
||||
//... see source.js for more details
|
||||
});
|
||||
}
|
||||
|
||||
source.getChannelContents = function(url, type, order, filters, continuationToken) {
|
||||
/**
|
||||
* @param url: string
|
||||
* @param type: string
|
||||
* @param order: string
|
||||
* @param filters: Map<string, Array<string>>
|
||||
* @param continuationToken: any?
|
||||
* @returns: VideoPager
|
||||
*/
|
||||
|
||||
const videos = []; // The results (PlatformVideo)
|
||||
const hasMore = false; // Are there more pages?
|
||||
const context = { url: url, query: query, type: type, order: order, filters: filters, continuationToken: continuationToken }; // Relevant data for the next page
|
||||
return new SomeChannelVideoPager(videos, hasMore, context);
|
||||
}
|
||||
|
||||
source.isContentDetailsUrl = function(url) {
|
||||
/**
|
||||
* @param url: string
|
||||
* @returns: boolean
|
||||
*/
|
||||
|
||||
return REGEX_DETAILS_URL.test(url);
|
||||
}
|
||||
|
||||
source.getContentDetails = function(url) {
|
||||
/**
|
||||
* @param url: string
|
||||
* @returns: PlatformVideoDetails
|
||||
*/
|
||||
|
||||
return new PlatformVideoDetails({
|
||||
//... see source.js for more details
|
||||
});
|
||||
}
|
||||
|
||||
source.getComments = function (url, continuationToken) {
|
||||
/**
|
||||
* @param url: string
|
||||
* @param continuationToken: any?
|
||||
* @returns: CommentPager
|
||||
*/
|
||||
|
||||
const comments = []; // The results (Comment)
|
||||
const hasMore = false; // Are there more pages?
|
||||
const context = { url: url, continuationToken: continuationToken }; // Relevant data for the next page
|
||||
return new SomeCommentPager(comments, hasMore, context);
|
||||
|
||||
}
|
||||
source.getSubComments = function (comment) {
|
||||
/**
|
||||
* @param comment: Comment
|
||||
* @returns: SomeCommentPager
|
||||
*/
|
||||
|
||||
if (typeof comment === 'string') {
|
||||
comment = JSON.parse(comment);
|
||||
}
|
||||
|
||||
return getCommentsPager(comment.context.claimId, comment.context.claimId, 1, false, comment.context.commentId);
|
||||
}
|
||||
|
||||
class SomeCommentPager extends CommentPager {
|
||||
constructor(results, hasMore, context) {
|
||||
super(results, hasMore, context);
|
||||
}
|
||||
|
||||
nextPage() {
|
||||
return source.getComments(this.context.url, this.context.continuationToken);
|
||||
}
|
||||
}
|
||||
|
||||
class SomeHomeVideoPager extends VideoPager {
|
||||
constructor(results, hasMore, context) {
|
||||
super(results, hasMore, context);
|
||||
}
|
||||
|
||||
nextPage() {
|
||||
return source.getHome(this.context.continuationToken);
|
||||
}
|
||||
}
|
||||
|
||||
class SomeSearchVideoPager extends VideoPager {
|
||||
constructor(results, hasMore, context) {
|
||||
super(results, hasMore, context);
|
||||
}
|
||||
|
||||
nextPage() {
|
||||
return source.search(this.context.query, this.context.type, this.context.order, this.context.filters, this.context.continuationToken);
|
||||
}
|
||||
}
|
||||
|
||||
class SomeSearchChannelVideoPager extends VideoPager {
|
||||
constructor(results, hasMore, context) {
|
||||
super(results, hasMore, context);
|
||||
}
|
||||
|
||||
nextPage() {
|
||||
return source.searchChannelContents(this.context.channelUrl, this.context.query, this.context.type, this.context.order, this.context.filters, this.context.continuationToken);
|
||||
}
|
||||
}
|
||||
|
||||
class SomeChannelPager extends ChannelPager {
|
||||
constructor(results, hasMore, context) {
|
||||
super(results, hasMore, context);
|
||||
}
|
||||
|
||||
nextPage() {
|
||||
return source.searchChannelContents(this.context.query, this.context.continuationToken);
|
||||
}
|
||||
}
|
||||
|
||||
class SomeChannelVideoPager extends VideoPager {
|
||||
constructor(results, hasMore, context) {
|
||||
super(results, hasMore, context);
|
||||
}
|
||||
|
||||
nextPage() {
|
||||
return source.getChannelContents(this.context.url, this.context.type, this.context.order, this.context.filters, this.context.continuationToken);
|
||||
}
|
||||
}
|
||||
```
|
31
docs/Script Signing.md
Normal file
31
docs/Script Signing.md
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Script signing
|
||||
|
||||
The `scriptSignature` and `scriptPublicKey` should be set whenever you deploy your script (NOT REQUIRED DURING DEVELOPMENT). The purpose of these fields is to verify that a plugin update was made by the same individual that developed the original plugin. This prevents somebody from hijacking your plugin without having access to your public private keypair. When this value is not present, you can still use this plugin, however the user will be informed that these values are missing and that this is a security risk. Here is an example script showing you how to generate these values. See below for more details.
|
||||
|
||||
You can use this script to generate the `scriptSignature` and `scriptPublicKey` fields above:
|
||||
|
||||
`sign-script.sh`
|
||||
```sh
|
||||
#!/bin/sh
|
||||
#Example usage:
|
||||
#cat script.js | sign-script.sh
|
||||
#sh sign-script.sh script.js
|
||||
|
||||
#Set your key paths here
|
||||
PRIVATE_KEY_PATH=~/.ssh/id_rsa
|
||||
PUBLIC_KEY_PATH=~/.ssh/id_rsa.pub
|
||||
|
||||
PUBLIC_KEY_PKCS8=$(ssh-keygen -f "$PUBLIC_KEY_PATH" -e -m pkcs8 | tail -n +2 | head -n -1 | tr -d '\n')
|
||||
echo "This is your public key: '$PUBLIC_KEY_PKCS8'"
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
# No parameter provided, read from stdin
|
||||
DATA=$(cat)
|
||||
else
|
||||
# Parameter provided, read from file
|
||||
DATA=$(cat "$1")
|
||||
fi
|
||||
|
||||
SIGNATURE=$(echo -n "$DATA" | openssl dgst -sha512 -sign ~/.ssh/id_rsa | base64 -w 0)
|
||||
echo "This is your signature: '$SIGNATURE'"
|
||||
```
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue