mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-28 10:43:00 +02:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d64faac74c | |||
| 2cc873ef60 | |||
| 7a66ce6bcd | |||
| 2730569b6b | |||
| ede5c4409c | |||
| 0dbe398435 | |||
| bcab3bccbc | |||
| 13100dc38d | |||
| 5227041398 | |||
| 8491d4da1a | |||
| 9bea1563ca | |||
| 9e7b936663 | |||
| 19c84475db | |||
| 4164b1a3f8 | |||
| a9dc038190 | |||
| 2825db88a5 | |||
| 363099b303 | |||
| 5e25a5054f | |||
| 2bc6127f6b | |||
| 064824aedf | |||
| 52044edb2e | |||
| fb12073a82 | |||
| 9944842a2f | |||
| 99dc50894c | |||
| de39451f67 | |||
| 8f28653b28 | |||
| 6598dff6df | |||
| 389798457b | |||
| 623c47fa2e | |||
| 19861fe812 | |||
| dd1c04bea1 | |||
| e6159117f6 | |||
| 0d9e1cd3c5 | |||
| 10753eb879 | |||
| 29aec21095 | |||
| a810f82ce2 | |||
| 2c454a0ec5 | |||
| d3dca00482 | |||
| d08dffd9e2 | |||
| 5b50ac926e | |||
| 57a3be35d0 | |||
| 70f36e69e6 | |||
| 8e70f1b865 | |||
| f86fb0ee44 | |||
| fe0aac7c6e | |||
| b93447f712 | |||
| 84a5103526 | |||
| c333300906 |
@@ -11,7 +11,7 @@ import java.nio.ByteBuffer
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/*
|
||||
class SyncServerTests {
|
||||
|
||||
//private val relayHost = "relay.grayjay.app"
|
||||
@@ -335,4 +335,4 @@ class SyncServerTests {
|
||||
|
||||
class AlwaysAuthorized : IAuthorizable {
|
||||
override val isAuthorized: Boolean get() = true
|
||||
}
|
||||
}*/
|
||||
@@ -13,7 +13,7 @@ import kotlin.random.Random
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/*
|
||||
data class PipeStreams(
|
||||
val initiatorInput: LittleEndianDataInputStream,
|
||||
val initiatorOutput: LittleEndianDataOutputStream,
|
||||
@@ -509,4 +509,4 @@ class Authorized : IAuthorizable {
|
||||
|
||||
class Unauthorized : IAuthorizable {
|
||||
override val isAuthorized: Boolean = false
|
||||
}
|
||||
}*/
|
||||
@@ -403,6 +403,8 @@ class VideoUrlSource {
|
||||
this.bitrate = obj.bitrate ?? 0;
|
||||
this.duration = obj.duration ?? 0;
|
||||
this.url = obj.url;
|
||||
if(obj.frameRate)
|
||||
this.frameRate = obj.frameRate;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
|
||||
@@ -241,8 +241,11 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
|
||||
return null;
|
||||
}
|
||||
|
||||
val sortedAddresses: List<InetAddress> = addresses
|
||||
.sortedBy { addr -> addressScore(addr) }
|
||||
|
||||
val sockets: ArrayList<Socket> = arrayListOf();
|
||||
for (i in addresses.indices) {
|
||||
for (i in sortedAddresses.indices) {
|
||||
sockets.add(Socket());
|
||||
}
|
||||
|
||||
@@ -250,7 +253,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
|
||||
var connectedSocket: Socket? = null;
|
||||
val threads: ArrayList<Thread> = arrayListOf();
|
||||
for (i in 0 until sockets.size) {
|
||||
val address = addresses[i];
|
||||
val address = sortedAddresses[i];
|
||||
val socket = sockets[i];
|
||||
val thread = Thread {
|
||||
try {
|
||||
|
||||
@@ -442,14 +442,18 @@ class Settings : FragmentedStorageFileJson() {
|
||||
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
|
||||
@FormField(R.string.show_advanced_media_source_metadata, FieldForm.TOGGLE, R.string.show_advanced_media_source_metadata_desc, 4)
|
||||
var showAdvancedMediaSourceMetadata: Boolean = false;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 5)
|
||||
var simplifySources: Boolean = true;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5)
|
||||
@FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 6)
|
||||
var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
|
||||
|
||||
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
|
||||
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
|
||||
@DropdownFieldOptionsId(R.array.player_background_behavior)
|
||||
var backgroundPlay: Int = 2;
|
||||
|
||||
@@ -457,7 +461,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
fun isBackgroundPictureInPicture() = backgroundPlay == 2;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.resume_after_preview, FieldForm.DROPDOWN, R.string.when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code, 7)
|
||||
@FormField(R.string.resume_after_preview, FieldForm.DROPDOWN, R.string.when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code, 8)
|
||||
@DropdownFieldOptionsId(R.array.resume_after_preview)
|
||||
var resumeAfterPreview: Int = 1;
|
||||
|
||||
@@ -469,7 +473,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@FormField(R.string.chapter_update_fps_title, FieldForm.DROPDOWN, R.string.chapter_update_fps_description, 8)
|
||||
@FormField(R.string.chapter_update_fps_title, FieldForm.DROPDOWN, R.string.chapter_update_fps_description, 9)
|
||||
@DropdownFieldOptionsId(R.array.chapter_fps)
|
||||
var chapterUpdateFPS: Int = 0;
|
||||
|
||||
@@ -484,7 +488,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
}
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9)
|
||||
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 10)
|
||||
var useLiveChatWindow: Boolean = true;
|
||||
|
||||
@FormField(R.string.restart_after_audio_focus_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_audio_focus_after_a_loss, 11)
|
||||
@@ -531,6 +535,77 @@ class Settings : FragmentedStorageFileJson() {
|
||||
else -> 10_000L;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@FormField(R.string.min_playback_speed, FieldForm.DROPDOWN, R.string.min_playback_speed_description, 25)
|
||||
@DropdownFieldOptionsId(R.array.min_playback_speed)
|
||||
var minimumPlaybackSpeed: Int = 0;
|
||||
@FormField(R.string.max_playback_speed, FieldForm.DROPDOWN, R.string.max_playback_speed_description, 26)
|
||||
@DropdownFieldOptionsId(R.array.max_playback_speed)
|
||||
var maximumPlaybackSpeed: Int = 2;
|
||||
@FormField(R.string.step_playback_speed, FieldForm.DROPDOWN, R.string.step_playback_speed_description, 26)
|
||||
@DropdownFieldOptionsId(R.array.step_playback_speed)
|
||||
var stepPlaybackSpeed: Int = 1;
|
||||
|
||||
fun getPlaybackSpeedStep(): Double {
|
||||
return when(stepPlaybackSpeed) {
|
||||
0 -> 0.05
|
||||
1 -> 0.1
|
||||
2 -> 0.25
|
||||
else -> 0.1;
|
||||
}
|
||||
}
|
||||
fun getPlaybackSpeeds(): List<Double> {
|
||||
val playbackSpeeds = mutableListOf<Double>();
|
||||
playbackSpeeds.add(1.0);
|
||||
val minSpeed = when(minimumPlaybackSpeed) {
|
||||
0 -> 0.25
|
||||
1 -> 0.5
|
||||
2 -> 1.0
|
||||
else -> 0.25
|
||||
}
|
||||
val maxSpeed = when(maximumPlaybackSpeed) {
|
||||
0 -> 2.0
|
||||
1 -> 2.25
|
||||
2 -> 3.0
|
||||
3 -> 4.0
|
||||
4 -> 5.0
|
||||
else -> 2.25;
|
||||
}
|
||||
var testSpeed = 1.0;
|
||||
|
||||
while(testSpeed > minSpeed) {
|
||||
val nextSpeed = (testSpeed - 0.25) as Double;
|
||||
testSpeed = Math.max(nextSpeed, minSpeed);
|
||||
playbackSpeeds.add(testSpeed);
|
||||
}
|
||||
testSpeed = 1.0;
|
||||
while(testSpeed < maxSpeed) {
|
||||
val nextSpeed = (testSpeed + if(testSpeed < 2) 0.25 else 1.0) as Double;
|
||||
testSpeed = Math.min(nextSpeed, maxSpeed);
|
||||
playbackSpeeds.add(testSpeed);
|
||||
}
|
||||
playbackSpeeds.sort();
|
||||
return playbackSpeeds;
|
||||
}
|
||||
|
||||
@FormField(R.string.hold_playback_speed, FieldForm.DROPDOWN, R.string.hold_playback_speed_description, 27)
|
||||
@DropdownFieldOptionsId(R.array.hold_playback_speeds)
|
||||
var holdPlaybackSpeed: Int = 3;
|
||||
|
||||
fun getHoldPlaybackSpeed(): Double {
|
||||
return when(holdPlaybackSpeed) {
|
||||
0 -> 1.25
|
||||
1 -> 1.5
|
||||
2 -> 1.75
|
||||
3 -> 2.0
|
||||
4 -> 2.25
|
||||
5 -> 2.5
|
||||
6 -> 2.75
|
||||
7 -> 3.0
|
||||
else -> 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||
@@ -628,6 +703,11 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var allowIpv6: Boolean = true;
|
||||
|
||||
@AdvancedField
|
||||
@FormField(R.string.allow_ipv4, FieldForm.TOGGLE, R.string.allow_ipv4_description, 5)
|
||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||
var allowLinkLocalIpv4: Boolean = false;
|
||||
|
||||
/*TODO: Should we have a different casting quality?
|
||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||
@@ -941,10 +1021,13 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
|
||||
var playlistAllowDups: Boolean = true;
|
||||
|
||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4)
|
||||
@FormField(R.string.add_to_beginning_of_watch_later, FieldForm.TOGGLE, R.string.add_to_beginning_description, 4)
|
||||
var addToBeginning: Boolean = true;
|
||||
|
||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 5)
|
||||
var polycentricEnabled: Boolean = true;
|
||||
|
||||
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 5)
|
||||
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7)
|
||||
var polycentricLocalCache: Boolean = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.ByteArrayInputStream
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.Format
|
||||
import com.futo.platformplayer.others.Language
|
||||
import java.util.Locale
|
||||
|
||||
class UISlideOverlays {
|
||||
companion object {
|
||||
@@ -344,14 +347,18 @@ class UISlideOverlays {
|
||||
if (source is IHLSManifestAudioSource) {
|
||||
val variant = HLS.mediaRenditionToVariant(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null))!!
|
||||
|
||||
val estSize = VideoHelper.estimateSourceSize(variant);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
val language = variant.language
|
||||
val mainText = when {
|
||||
language != Language.UNKNOWN && variant.bitrate == Format.NO_VALUE -> Locale.forLanguageTag(language).displayLanguage
|
||||
language == Language.UNKNOWN && variant.bitrate != Format.NO_VALUE -> variant.bitrate.toHumanBitrate()
|
||||
language != Language.UNKNOWN && variant.bitrate != Format.NO_VALUE -> "${Locale.forLanguageTag(language).displayLanguage} ${variant.bitrate.toHumanBitrate()}"
|
||||
else -> "Default"
|
||||
}
|
||||
audioButtons.add(SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_music,
|
||||
variant.name,
|
||||
listOf(variant.language, variant.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
|
||||
(prefix + variant.codec).trim(),
|
||||
if (variant.name != "") variant.name else mainText,
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) variant.codec.trim() else "",
|
||||
tag = variant,
|
||||
call = {
|
||||
selectedAudioVariant = variant
|
||||
@@ -363,14 +370,12 @@ class UISlideOverlays {
|
||||
} else {
|
||||
val variant = HLS.variantReferenceToVariant(VariantPlaylistReference(playlist.baseUri, StreamInfo(null, null, null, null, null, null, null, null, null)))
|
||||
|
||||
val estSize = VideoHelper.estimateSourceSize(variant);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
videoButtons.add(SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
variant.name,
|
||||
"${variant.width}x${variant.height}",
|
||||
(prefix + variant.codec).trim(),
|
||||
if (variant.name != "") variant.name else "${variant.width}p${variant.frameRate ?: ""}",
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) variant.codec.trim() else "",
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) variant.bitrate?.toHumanBitrate() else "",
|
||||
tag = variant,
|
||||
call = {
|
||||
selectedVideoVariant = variant
|
||||
@@ -385,16 +390,19 @@ class UISlideOverlays {
|
||||
} else if (playlist is HlsMultivariantPlaylist) {
|
||||
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, resolvedPlaylistUrl)
|
||||
|
||||
masterPlaylist.getAudioSources().forEach { it ->
|
||||
|
||||
val estSize = VideoHelper.estimateSourceSize(it);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
masterPlaylist.getAudioSources().forEach {
|
||||
val language = it.language
|
||||
val mainText = when {
|
||||
language != Language.UNKNOWN && it.bitrate == Format.NO_VALUE -> Locale.forLanguageTag(language).displayLanguage
|
||||
language == Language.UNKNOWN && it.bitrate != Format.NO_VALUE -> it.bitrate.toHumanBitrate()
|
||||
language != Language.UNKNOWN && it.bitrate != Format.NO_VALUE -> "${Locale.forLanguageTag(language).displayLanguage} ${it.bitrate.toHumanBitrate()}"
|
||||
else -> "Default"
|
||||
}
|
||||
audioButtons.add(SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_music,
|
||||
it.name,
|
||||
listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "),
|
||||
(prefix + it.codec).trim(),
|
||||
if (it.name != "") it.name else mainText,
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) it.codec.trim() else "",
|
||||
tag = it,
|
||||
call = {
|
||||
selectedAudioVariant = it
|
||||
@@ -414,14 +422,12 @@ class UISlideOverlays {
|
||||
}*/
|
||||
|
||||
masterPlaylist.getVideoSources().forEach {
|
||||
val estSize = VideoHelper.estimateSourceSize(it);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
videoButtons.add(SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
it.name,
|
||||
"${it.width}x${it.height}",
|
||||
(prefix + it.codec).trim(),
|
||||
if (it.name != "") it.name else "${it.height}p${it.frameRate ?: ""}",
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) it.codec.trim() else "",
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) it.bitrate?.toHumanBitrate() else "",
|
||||
tag = it,
|
||||
call = {
|
||||
selectedVideoVariant = it
|
||||
@@ -535,9 +541,9 @@ class UISlideOverlays {
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
it.name,
|
||||
"${it.width}x${it.height}",
|
||||
(prefix + it.codec).trim(),
|
||||
if (it.name != "") it.name else "${it.height}p${it.frameRate ?: ""}",
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) it.codec.trim() else "",
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) it.bitrate?.toHumanBitrate() ?: "" else "",
|
||||
tag = it,
|
||||
call = {
|
||||
selectedVideo = it
|
||||
@@ -573,8 +579,7 @@ class UISlideOverlays {
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
it.name,
|
||||
"HLS",
|
||||
if (it.name != "") it.name else "HLS",
|
||||
tag = it,
|
||||
call = {
|
||||
showHlsPicker(video, it, it.url, container)
|
||||
@@ -606,14 +611,18 @@ class UISlideOverlays {
|
||||
.map {
|
||||
when (it) {
|
||||
is IAudioUrlSource -> {
|
||||
val estSize = VideoHelper.estimateSourceSize(it);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
val mainText = when {
|
||||
it.language != Language.UNKNOWN && it.bitrate == Format.NO_VALUE -> Locale.forLanguageTag(it.language).displayLanguage
|
||||
it.language == Language.UNKNOWN && it.bitrate != Format.NO_VALUE -> it.bitrate.toHumanBitrate()
|
||||
it.language != Language.UNKNOWN && it.bitrate != Format.NO_VALUE -> "${Locale.forLanguageTag(it.language).displayLanguage} ${it.bitrate.toHumanBitrate()}"
|
||||
else -> "Default"
|
||||
}
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_music,
|
||||
it.name,
|
||||
"${it.bitrate}",
|
||||
(prefix + it.codec).trim(),
|
||||
if (it.name != "") it.name else mainText,
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) it.codec.trim() ?: "" else "",
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) (if (it.original) "Original" else "") else "",
|
||||
tag = it,
|
||||
call = {
|
||||
selectedAudio = it
|
||||
@@ -647,8 +656,7 @@ class UISlideOverlays {
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
it.name,
|
||||
"HLS Audio",
|
||||
if (it.name != "") it.name else "HLS Audio",
|
||||
tag = it,
|
||||
call = {
|
||||
showHlsPicker(video, it, it.url, container)
|
||||
@@ -1151,6 +1159,8 @@ class UISlideOverlays {
|
||||
call = {
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
|
||||
UIDialogs.appToast("Added to watch later", false);
|
||||
else
|
||||
UIDialogs.toast(container.context.getString(R.string.already_in_watch_later))
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -434,7 +434,7 @@ private fun interfaceScore(nif: NetworkInterface): Int {
|
||||
}
|
||||
}
|
||||
|
||||
private fun addressScore(addr: InetAddress): Int {
|
||||
fun addressScore(addr: InetAddress): Int {
|
||||
return when (addr) {
|
||||
is Inet4Address -> {
|
||||
val octets = addr.address.map { it.toInt() and 0xFF }
|
||||
|
||||
@@ -115,6 +115,7 @@ import java.io.StringWriter
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.util.LinkedList
|
||||
import java.util.Queue
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
|
||||
@@ -218,6 +219,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
val mainId = UUID.randomUUID().toString().substring(0, 5)
|
||||
|
||||
constructor() : super() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.setVmPolicy(
|
||||
@@ -269,8 +272,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
|
||||
@UnstableApi
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Logger.i(TAG, "MainActivity Starting");
|
||||
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
||||
Logger.w(TAG, "MainActivity Starting [$mainId]");
|
||||
StateApp.instance.setGlobalContext(this, lifecycleScope, mainId);
|
||||
StateApp.instance.mainAppStarting(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -671,13 +674,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume();
|
||||
Logger.v(TAG, "onResume")
|
||||
Logger.w(TAG, "onResume [$mainId]")
|
||||
_isVisible = true;
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause();
|
||||
Logger.v(TAG, "onPause")
|
||||
Logger.w(TAG, "onPause [$mainId]")
|
||||
_isVisible = false;
|
||||
|
||||
_qrCodeLoadingDialog?.dismiss()
|
||||
@@ -686,7 +689,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
Logger.v(TAG, "_wasStopped = true");
|
||||
Logger.w(TAG, "onStop [$mainId]");
|
||||
_wasStopped = true;
|
||||
}
|
||||
|
||||
@@ -1103,8 +1106,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy();
|
||||
Logger.v(TAG, "onDestroy")
|
||||
StateApp.instance.mainAppDestroyed(this);
|
||||
Logger.w(TAG, "onDestroy [$mainId]")
|
||||
StateApp.instance.mainAppDestroyed(this, mainId);
|
||||
}
|
||||
|
||||
inline fun <reified T> isFragmentActive(): Boolean {
|
||||
|
||||
+2
@@ -9,6 +9,8 @@ class DashManifestSource : IVideoSource, IDashManifestSource {
|
||||
override val bitrate: Int? = null;
|
||||
override val url : String;
|
||||
override val duration: Long get() = 0;
|
||||
// only used for single source DASH
|
||||
override val frameRate: Int? = null
|
||||
|
||||
override var priority: Boolean = false;
|
||||
|
||||
|
||||
+2
@@ -9,6 +9,8 @@ class HLSManifestSource : IVideoSource, IHLSManifestSource {
|
||||
override val bitrate : Int? = null;
|
||||
override val url : String;
|
||||
override val duration: Long = 0;
|
||||
override val frameRate: Int?
|
||||
get() = null
|
||||
|
||||
override var priority: Boolean = false;
|
||||
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@ class HLSVariantVideoUrlSource(
|
||||
override val bitrate: Int?,
|
||||
override val duration: Long,
|
||||
override val priority: Boolean,
|
||||
val url: String
|
||||
val url: String,
|
||||
override val frameRate: Int? = null
|
||||
) : IVideoUrlSource {
|
||||
override fun getVideoUrl(): String {
|
||||
return url
|
||||
|
||||
+1
@@ -9,4 +9,5 @@ interface IVideoSource {
|
||||
val bitrate : Int?;
|
||||
val duration: Long;
|
||||
val priority: Boolean;
|
||||
val frameRate: Int?
|
||||
}
|
||||
+5
-2
@@ -13,6 +13,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
||||
override val name : String;
|
||||
override val bitrate : Int;
|
||||
override val duration : Long;
|
||||
override val frameRate: Int?
|
||||
|
||||
override var priority: Boolean = false;
|
||||
|
||||
@@ -22,7 +23,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
||||
//Only for particular videos
|
||||
override var streamMetaData: StreamMetaData? = null;
|
||||
|
||||
constructor(name : String, filePath : String, fileSize: Long, width : Int = 0, height : Int = 0, duration: Long = 0, container : String = "", codec : String = "", bitrate : Int = 0) {
|
||||
constructor(name : String, filePath : String, fileSize: Long, width : Int = 0, height : Int = 0, duration: Long = 0, container : String = "", codec : String = "", bitrate : Int = 0, frameRate : Int? = null) {
|
||||
this.name = name;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
@@ -32,6 +33,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
||||
this.filePath = filePath;
|
||||
this.fileSize = fileSize;
|
||||
this.bitrate = bitrate;
|
||||
this.frameRate = frameRate
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -45,7 +47,8 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
||||
source.duration,
|
||||
overrideContainer ?: source.container,
|
||||
source.codec,
|
||||
source.bitrate?:0
|
||||
source.bitrate?:0,
|
||||
source.frameRate
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -13,6 +13,7 @@ open class VideoUrlSource(
|
||||
override val container : String = "",
|
||||
override val codec : String = "",
|
||||
override val bitrate : Int? = 0,
|
||||
override val frameRate: Int? = null,
|
||||
|
||||
override var priority: Boolean = false
|
||||
) : IVideoUrlSource, IStreamMetaDataSource {
|
||||
@@ -38,7 +39,8 @@ open class VideoUrlSource(
|
||||
source.duration,
|
||||
source.container,
|
||||
source.codec,
|
||||
source.bitrate
|
||||
source.bitrate,
|
||||
source.frameRate
|
||||
);
|
||||
ret.streamMetaData = streamData;
|
||||
|
||||
|
||||
+13
-5
@@ -4,6 +4,7 @@ import android.net.Uri
|
||||
import com.futo.platformplayer.SignatureProvider
|
||||
import com.futo.platformplayer.api.media.Serializer
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.matchesDomain
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
import kotlinx.serialization.Contextual
|
||||
@@ -168,12 +169,17 @@ class SourcePluginConfig(
|
||||
}
|
||||
|
||||
fun validate(text: String): Boolean {
|
||||
if(scriptPublicKey.isNullOrEmpty())
|
||||
throw IllegalStateException("No public key present");
|
||||
if(scriptSignature.isNullOrEmpty())
|
||||
throw IllegalStateException("No signature present");
|
||||
try {
|
||||
if (scriptPublicKey.isNullOrEmpty())
|
||||
throw IllegalStateException("No public key present");
|
||||
if (scriptSignature.isNullOrEmpty())
|
||||
throw IllegalStateException("No signature present");
|
||||
|
||||
return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
|
||||
return SignatureProvider.verify(text, scriptSignature, scriptPublicKey);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to verify due to an unhandled exception", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun isUrlAllowed(url: String): Boolean {
|
||||
@@ -204,6 +210,8 @@ class SourcePluginConfig(
|
||||
obj.sourceUrl = sourceUrl;
|
||||
return obj;
|
||||
}
|
||||
|
||||
private val TAG = "SourcePluginConfig"
|
||||
}
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
|
||||
+3
@@ -31,6 +31,8 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
||||
override val bitrate: Int?;
|
||||
override val duration: Long;
|
||||
override val priority: Boolean;
|
||||
// only used for single source DASH
|
||||
override val frameRate: Int?
|
||||
|
||||
var url: String?;
|
||||
override var manifest: String?;
|
||||
@@ -52,6 +54,7 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
||||
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
||||
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
||||
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
||||
frameRate = _obj.getOrNull(config, "frameRate", contextName);
|
||||
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
||||
canMerge = _obj.getOrDefault(config, "canMerge", contextName, false) ?: false;
|
||||
hasGenerate = _obj.has("generate");
|
||||
|
||||
+2
@@ -18,6 +18,8 @@ class JSDashManifestSource : IVideoUrlSource, IDashManifestSource, JSSource {
|
||||
override val bitrate: Int? = null;
|
||||
override val url : String;
|
||||
override val duration: Long;
|
||||
// only used for single source DASH
|
||||
override val frameRate: Int? = null
|
||||
|
||||
override var priority: Boolean = false;
|
||||
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
|
||||
override val bitrate: Int? = null
|
||||
override val url: String
|
||||
override val duration: Long
|
||||
override val frameRate: Int? = null
|
||||
|
||||
override var priority: Boolean = false
|
||||
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ class JSHLSManifestSource : IHLSManifestSource, JSSource {
|
||||
override val bitrate : Int? = null;
|
||||
override val url : String;
|
||||
override val duration: Long;
|
||||
override val frameRate: Int? = null
|
||||
|
||||
override var priority: Boolean = false;
|
||||
|
||||
|
||||
+2
@@ -16,6 +16,7 @@ open class JSVideoUrlSource : IVideoUrlSource, JSSource {
|
||||
override val name : String;
|
||||
override val bitrate : Int;
|
||||
override val duration: Long;
|
||||
override val frameRate: Int?
|
||||
private val url : String;
|
||||
|
||||
override var priority: Boolean = false;
|
||||
@@ -31,6 +32,7 @@ open class JSVideoUrlSource : IVideoUrlSource, JSSource {
|
||||
name = _obj.getOrThrow(config, "name", contextName);
|
||||
bitrate = _obj.getOrThrow(config, "bitrate", contextName);
|
||||
duration = _obj.getOrThrow<Int>(config, "duration", contextName).toLong();
|
||||
frameRate = _obj.getOrNull(config, "frameRate", contextName);
|
||||
url = _obj.getOrThrow(config, "url", contextName);
|
||||
|
||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ class LocalVideoFileSource: IVideoSource {
|
||||
override val bitrate: Int = 0
|
||||
override val duration: Long;
|
||||
override val priority: Boolean = false;
|
||||
override val frameRate: Int? = null
|
||||
|
||||
constructor(file: File) {
|
||||
name = file.name;
|
||||
|
||||
@@ -35,7 +35,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
override var usedRemoteAddress: InetAddress? = null;
|
||||
override var localAddress: InetAddress? = null;
|
||||
override val canSetVolume: Boolean get() = true;
|
||||
override val canSetSpeed: Boolean get() = false; //TODO: Implement
|
||||
override val canSetSpeed: Boolean get() = true;
|
||||
|
||||
var addresses: Array<InetAddress>? = null;
|
||||
var port: Int = 0;
|
||||
@@ -144,6 +144,23 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
sendChannelMessage("sender-0", transportId, "urn:x-cast:com.google.cast.media", json);
|
||||
}
|
||||
|
||||
override fun changeSpeed(speed: Double) {
|
||||
if (invokeInIOScopeIfRequired { changeSpeed(speed) }) return
|
||||
|
||||
val speedClamped = speed.coerceAtLeast(1.0).coerceAtLeast(1.0).coerceAtMost(2.0)
|
||||
setSpeed(speedClamped)
|
||||
val mediaSessionId = _mediaSessionId ?: return
|
||||
val transportId = _transportId ?: return
|
||||
val setSpeedObject = JSONObject().apply {
|
||||
put("type", "SET_PLAYBACK_RATE")
|
||||
put("mediaSessionId", mediaSessionId)
|
||||
put("playbackRate", speedClamped)
|
||||
put("requestId", _requestId++)
|
||||
}
|
||||
|
||||
sendChannelMessage(sourceId = "sender-0", destinationId = transportId, namespace = "urn:x-cast:com.google.cast.media", json = setSpeedObject.toString())
|
||||
}
|
||||
|
||||
override fun changeVolume(volume: Double) {
|
||||
if (invokeInIOScopeIfRequired({ changeVolume(volume) })) {
|
||||
return;
|
||||
|
||||
@@ -166,10 +166,11 @@ class StateCasting {
|
||||
Logger.i(TAG, "CastingService started.");
|
||||
|
||||
_nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
||||
startDiscovering()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun startDiscovering() {
|
||||
private fun startDiscovering() {
|
||||
_nsdManager?.apply {
|
||||
_discoveryListeners.forEach {
|
||||
discoverServices(it.key, NsdManager.PROTOCOL_DNS_SD, it.value)
|
||||
@@ -178,7 +179,7 @@ class StateCasting {
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun stopDiscovering() {
|
||||
private fun stopDiscovering() {
|
||||
_nsdManager?.apply {
|
||||
_discoveryListeners.forEach {
|
||||
try {
|
||||
@@ -1220,9 +1221,16 @@ class StateCasting {
|
||||
|
||||
private fun getLocalUrl(ad: CastingDevice): String {
|
||||
var address = ad.localAddress!!
|
||||
if (address.isLinkLocalAddress) {
|
||||
address = findPreferredAddress() ?: address
|
||||
Logger.i(TAG, "Selected casting address: $address")
|
||||
if (Settings.instance.casting.allowLinkLocalIpv4) {
|
||||
if (address.isLinkLocalAddress && address is Inet6Address) {
|
||||
address = findPreferredAddress() ?: address
|
||||
Logger.i(TAG, "Selected casting address: $address")
|
||||
}
|
||||
} else {
|
||||
if (address.isLinkLocalAddress) {
|
||||
address = findPreferredAddress() ?: address
|
||||
Logger.i(TAG, "Selected casting address: $address")
|
||||
}
|
||||
}
|
||||
return "http://${address.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||
}
|
||||
|
||||
@@ -103,7 +103,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
super.show();
|
||||
Logger.i(TAG, "Dialog shown.");
|
||||
|
||||
StateCasting.instance.startDiscovering()
|
||||
(_imageLoader.drawable as Animatable?)?.start();
|
||||
|
||||
synchronized(StateCasting.instance.devices) {
|
||||
@@ -148,7 +147,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
override fun dismiss() {
|
||||
super.dismiss()
|
||||
(_imageLoader.drawable as Animatable?)?.stop()
|
||||
StateCasting.instance.stopDiscovering()
|
||||
StateCasting.instance.onDeviceAdded.remove(this)
|
||||
StateCasting.instance.onDeviceChanged.remove(this)
|
||||
StateCasting.instance.onDeviceRemoved.remove(this)
|
||||
|
||||
@@ -724,7 +724,7 @@ class VideoDownload {
|
||||
val t = cue.groupValues[1];
|
||||
val d = cue.groupValues[2];
|
||||
|
||||
val url = foundTemplateUrl.replace("\$Number\$", indexCounter.toString());
|
||||
val url = foundTemplateUrl.replace("\$Number\$", (indexCounter).toString());
|
||||
|
||||
val data = if(executor != null)
|
||||
executor.executeRequest("GET", url, null, mapOf());
|
||||
|
||||
+2
@@ -778,6 +778,8 @@ class ArticleDetailFragment : MainFragment {
|
||||
view.onAddToWatchLaterClicked.subscribe { a ->
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
|
||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||
else
|
||||
UIDialogs.toast(context.getString(R.string.already_in_watch_later))
|
||||
}
|
||||
}
|
||||
else if(content is IPlatformPost) {
|
||||
|
||||
+2
@@ -226,6 +226,8 @@ class ChannelFragment : MainFragment() {
|
||||
if (content is IPlatformVideo) {
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
|
||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||
else
|
||||
UIDialogs.toast(context.getString(R.string.already_in_watch_later))
|
||||
}
|
||||
}
|
||||
adapter.onUrlClicked.subscribe { url ->
|
||||
|
||||
+2
@@ -86,6 +86,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
if(it is IPlatformVideo) {
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
else
|
||||
UIDialogs.toast(context.getString(R.string.already_in_watch_later))
|
||||
}
|
||||
};
|
||||
adapter.onLongPress.subscribe(this) {
|
||||
|
||||
+6
-1
@@ -16,6 +16,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UISlideOverlays
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringStorage
|
||||
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
|
||||
|
||||
class CreatorsFragment : MainFragment() {
|
||||
@@ -29,6 +31,8 @@ class CreatorsFragment : MainFragment() {
|
||||
private var _editSearch: EditText? = null;
|
||||
private var _textMeta: TextView? = null;
|
||||
private var _buttonClearSearch: ImageButton? = null
|
||||
private var _ordering = FragmentedStorage.get<StringStorage>("creators_ordering")
|
||||
|
||||
|
||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val view = inflater.inflate(R.layout.fragment_creators, container, false);
|
||||
@@ -44,7 +48,7 @@ class CreatorsFragment : MainFragment() {
|
||||
_buttonClearSearch?.visibility = View.INVISIBLE;
|
||||
}
|
||||
|
||||
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription)) { subs ->
|
||||
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription), _ordering?.value?.toIntOrNull() ?: 5) { subs ->
|
||||
_textMeta?.let {
|
||||
it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}";
|
||||
}
|
||||
@@ -61,6 +65,7 @@ class CreatorsFragment : MainFragment() {
|
||||
spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
|
||||
adapter.sortBy = pos;
|
||||
_ordering.setAndSave(pos.toString())
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||
};
|
||||
|
||||
+23
-12
@@ -10,7 +10,6 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
@@ -165,14 +164,24 @@ class PlaylistFragment : MainFragment() {
|
||||
};
|
||||
}
|
||||
|
||||
private fun copyPlaylist(playlist: Playlist) {
|
||||
private fun savePlaylist(playlist: Playlist) {
|
||||
StatePlaylists.instance.playlistStore.save(playlist)
|
||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(
|
||||
arrayListOf()
|
||||
)
|
||||
UIDialogs.toast("Playlist saved")
|
||||
}
|
||||
|
||||
private fun copyPlaylist(playlist: Playlist) {
|
||||
var copyNumber = 1
|
||||
var newName = "${playlist.name} (Copy)"
|
||||
val playlists = StatePlaylists.instance.playlistStore.getItems()
|
||||
while (playlists.any { it.name == newName }) {
|
||||
copyNumber += 1
|
||||
newName = "${playlist.name} (Copy $copyNumber)"
|
||||
}
|
||||
StatePlaylists.instance.playlistStore.save(playlist.makeCopy(newName))
|
||||
_fragment.navigate<PlaylistsFragment>(withHistory = false)
|
||||
UIDialogs.toast("Playlist copied")
|
||||
}
|
||||
|
||||
fun onShown(parameter: Any?) {
|
||||
_taskLoadPlaylist.cancel()
|
||||
|
||||
@@ -188,12 +197,14 @@ class PlaylistFragment : MainFragment() {
|
||||
setButtonExportVisible(false)
|
||||
setButtonEditVisible(true)
|
||||
|
||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
|
||||
_fragment.topBar?.assume<NavigationTopBarFragment>()
|
||||
?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
|
||||
_fragment.topBar?.assume<NavigationTopBarFragment>()
|
||||
?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
|
||||
if (StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
|
||||
copyPlaylist(parameter)
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
savePlaylist(parameter)
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
setName(null)
|
||||
setVideos(null, false)
|
||||
@@ -259,7 +270,7 @@ class PlaylistFragment : MainFragment() {
|
||||
val playlist = _playlist ?: return
|
||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
|
||||
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to download", {
|
||||
copyPlaylist(playlist)
|
||||
savePlaylist(playlist)
|
||||
download()
|
||||
})
|
||||
return
|
||||
@@ -292,7 +303,7 @@ class PlaylistFragment : MainFragment() {
|
||||
val playlist = _playlist ?: return
|
||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
|
||||
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to edit the name", {
|
||||
copyPlaylist(playlist)
|
||||
savePlaylist(playlist)
|
||||
onEditClick()
|
||||
})
|
||||
return
|
||||
|
||||
+2
-2
@@ -191,7 +191,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
|
||||
private var _bypassRateLimit = false;
|
||||
private val _lastExceptions: List<Throwable>? = null;
|
||||
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({StateApp.instance.scope}, { withRefresh ->
|
||||
private val _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({fragment.lifecycleScope}, { withRefresh ->
|
||||
val group = subGroup;
|
||||
if(!_bypassRateLimit) {
|
||||
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(group);
|
||||
@@ -202,7 +202,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
throw RateLimitException(rateLimitPlugins.map { it.key.id });
|
||||
}
|
||||
_bypassRateLimit = false;
|
||||
val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(StateApp.instance.scope, withRefresh, group);
|
||||
val resp = StateSubscriptions.instance.getGlobalSubscriptionFeed(fragment.lifecycleScope, withRefresh, group);
|
||||
val feed = StateSubscriptions.instance.getFeed(group?.id);
|
||||
|
||||
val currentExs = feed?.exceptions ?: listOf();
|
||||
|
||||
+1
-6
@@ -101,7 +101,7 @@ class VideoDetailFragment() : MainFragment() {
|
||||
}
|
||||
|
||||
private fun isSmallWindow(): Boolean {
|
||||
return resources.configuration.smallestScreenWidthDp < resources.getInteger(R.integer.column_width_dp) * 2
|
||||
return resources.configuration.smallestScreenWidthDp < resources.getInteger(R.integer.smallest_width_dp)
|
||||
}
|
||||
|
||||
private fun isAutoRotateEnabled(): Boolean {
|
||||
@@ -627,11 +627,6 @@ class VideoDetailFragment() : MainFragment() {
|
||||
showSystemUI()
|
||||
}
|
||||
|
||||
// temporarily force the device to portrait if auto-rotate is disabled to prevent landscape when exiting full screen on a small device
|
||||
// @SuppressLint("SourceLockedOrientationActivity")
|
||||
// if (!isFullscreen && isSmallWindow() && !isAutoRotateEnabled() && !isMinimizingFromFullScreen) {
|
||||
// activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||
// }
|
||||
updateOrientation();
|
||||
_view?.allowMotion = !fullscreen;
|
||||
}
|
||||
|
||||
+118
-33
@@ -2,6 +2,8 @@ package com.futo.platformplayer.fragment.mainactivity.main
|
||||
|
||||
import android.app.PictureInPictureParams
|
||||
import android.app.RemoteAction
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
@@ -99,6 +101,7 @@ import com.futo.platformplayer.getNowDiffSeconds
|
||||
import com.futo.platformplayer.helpers.VideoHelper
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.others.Language
|
||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.AnnouncementType
|
||||
@@ -172,6 +175,7 @@ import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import userpackage.Protocol
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
@@ -408,6 +412,14 @@ class VideoDetailView : ConstraintLayout {
|
||||
showChaptersUI();
|
||||
};
|
||||
|
||||
_title.setOnLongClickListener {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager;
|
||||
val clip = ClipData.newPlainText("Video Title", (it as TextView).text);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
UIDialogs.toast(context, "Copied", false)
|
||||
// let other interactions happen based on the touch
|
||||
false
|
||||
}
|
||||
|
||||
_buttonSubscribe.onSubscribed.subscribe {
|
||||
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||
@@ -1399,8 +1411,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
onVideoChanged.emit(0, 0)
|
||||
}
|
||||
|
||||
val me = this;
|
||||
if (video is JSVideoDetails) {
|
||||
val me = this;
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
//TODO: Implement video.getContentChapters()
|
||||
@@ -1457,6 +1469,32 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
};
|
||||
}
|
||||
else {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
if (!StateApp.instance.privateMode) {
|
||||
val stopwatch = com.futo.platformplayer.debug.Stopwatch()
|
||||
var tracker = video.getPlaybackTracker()
|
||||
Logger.i(TAG, "video.getPlaybackTracker took ${stopwatch.elapsedMs}ms")
|
||||
|
||||
if (tracker == null) {
|
||||
stopwatch.reset()
|
||||
tracker = StatePlatform.instance.getPlaybackTracker(video.url);
|
||||
Logger.i(
|
||||
TAG,
|
||||
"StatePlatform.instance.getPlaybackTracker took ${stopwatch.elapsedMs}ms"
|
||||
)
|
||||
}
|
||||
|
||||
if (me.video?.url == video.url && !video.url.isNullOrBlank())
|
||||
me._playbackTracker = tracker;
|
||||
} else if (me.video == video)
|
||||
me._playbackTracker = null;
|
||||
} catch (ex: Throwable) {
|
||||
Logger.e(TAG, "Playback tracker failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||
val extraBytesRef = video.id.value?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
||||
@@ -1897,8 +1935,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
|
||||
updateQualityFormatsOverlay(
|
||||
videoTrackFormats.distinctBy { it.height }.sortedBy { it.height },
|
||||
audioTrackFormats.distinctBy { it.bitrate }.sortedBy { it.bitrate });
|
||||
videoTrackFormats.distinctBy { it.height }.sortedByDescending { it.height },
|
||||
audioTrackFormats.distinctBy { it.bitrate }.sortedByDescending { it.bitrate });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2149,23 +2187,40 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
|
||||
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
|
||||
val qualityPlaybackSpeedTitle = if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", currentPlaybackRate)})"); } else null;
|
||||
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
|
||||
R.string.quality), null, true,
|
||||
if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate)) } else null,
|
||||
qualityPlaybackSpeedTitle,
|
||||
if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply {
|
||||
setButtons(listOf("0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "2.25"), currentPlaybackRate!!.toString());
|
||||
val playbackSpeeds = Settings.instance.playback.getPlaybackSpeeds();
|
||||
val format = if(playbackSpeeds.size < 20) "%.2f" else "%.1f";
|
||||
val playbackLabels = playbackSpeeds.map { String.format(Locale.US, format, it) }.toMutableList();
|
||||
playbackLabels.add("+");
|
||||
playbackLabels.add(0, "-");
|
||||
|
||||
setButtons(playbackLabels, String.format(Locale.US, format, currentPlaybackRate));
|
||||
onClick.subscribe { v ->
|
||||
val currentPlaybackSpeed = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate();
|
||||
var playbackSpeedString = v;
|
||||
val stepSpeed = Settings.instance.playback.getPlaybackSpeedStep();
|
||||
if(v == "+")
|
||||
playbackSpeedString = String.format(Locale.US, "%.2f", Math.min((currentPlaybackSpeed?.toDouble() ?: 1.0) + stepSpeed, 5.0)).toString();
|
||||
else if(v == "-")
|
||||
playbackSpeedString = String.format(Locale.US, "%.2f", Math.max(0.1, (currentPlaybackSpeed?.toDouble() ?: 1.0) - stepSpeed)).toString();
|
||||
val newPlaybackSpeed = playbackSpeedString.toDouble();
|
||||
if (_isCasting) {
|
||||
val ad = StateCasting.instance.activeDevice ?: return@subscribe
|
||||
if (!ad.canSetSpeed) {
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
ad.changeSpeed(v.toDouble())
|
||||
setSelected(v);
|
||||
qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
|
||||
ad.changeSpeed(newPlaybackSpeed)
|
||||
setSelected(playbackSpeedString);
|
||||
} else {
|
||||
_player.setPlaybackRate(v.toFloat());
|
||||
setSelected(v);
|
||||
qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
|
||||
_player.setPlaybackRate(playbackSpeedString.toFloat());
|
||||
setSelected(playbackSpeedString);
|
||||
}
|
||||
};
|
||||
} else null,
|
||||
@@ -2176,8 +2231,9 @@ class VideoDetailView : ConstraintLayout {
|
||||
.map {
|
||||
SlideUpMenuItem(this.context,
|
||||
R.drawable.ic_movie,
|
||||
it.name,
|
||||
"${it.width}x${it.height}",
|
||||
if (it.name != "") it.name else "${it.height}p${it.frameRate ?: ""}",
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) it.codec.trim() else "",
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) it.bitrate?.toHumanBitrate() ?: "" else "",
|
||||
tag = it,
|
||||
call = { handleSelectVideoTrack(it) });
|
||||
}.toList().toTypedArray())
|
||||
@@ -2186,10 +2242,18 @@ class VideoDetailView : ConstraintLayout {
|
||||
SlideUpMenuGroup(this.context, context.getString(R.string.offline_audio), "audio",
|
||||
*localAudioSource
|
||||
.map {
|
||||
val mainText = when {
|
||||
it.language != Language.UNKNOWN && it.bitrate == Format.NO_VALUE -> Locale.forLanguageTag(it.language).displayLanguage
|
||||
it.language == Language.UNKNOWN && it.bitrate != Format.NO_VALUE -> it.bitrate.toHumanBitrate()
|
||||
it.language != Language.UNKNOWN && it.bitrate != Format.NO_VALUE -> "${Locale.forLanguageTag(it.language).displayLanguage} ${it.bitrate.toHumanBitrate()}"
|
||||
else -> "Default"
|
||||
}
|
||||
|
||||
SlideUpMenuItem(this.context,
|
||||
R.drawable.ic_music,
|
||||
it.name,
|
||||
it.bitrate.toHumanBitrate(),
|
||||
if (it.name != "") it.name else mainText,
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) it.codec.trim() ?: "" else "",
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) (if (it.original) "Original" else "") else "",
|
||||
tag = it,
|
||||
call = { handleSelectAudioTrack(it) });
|
||||
}.toList().toTypedArray())
|
||||
@@ -2206,36 +2270,49 @@ class VideoDetailView : ConstraintLayout {
|
||||
this.context, context.getString(R.string.stream_video), "video", (listOf(
|
||||
SlideUpMenuItem(this.context, R.drawable.ic_movie, "Auto", tag = "auto", call = { _player.selectVideoTrack(-1) })
|
||||
) + (liveStreamVideoFormats.map {
|
||||
SlideUpMenuItem(this.context, R.drawable.ic_movie, it.label
|
||||
?: it.containerMimeType
|
||||
?: it.bitrate.toString(), "${it.width}x${it.height}", tag = it, call = { _player.selectVideoTrack(it.height) });
|
||||
val frameRate =
|
||||
if (it.frameRate.toInt() == Format.NO_VALUE) "" else it.frameRate.toInt()
|
||||
SlideUpMenuItem(
|
||||
this.context, R.drawable.ic_movie, "${it.height}p${frameRate}",
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) it.containerMimeType ?: "" else "",
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) it.label ?: it.bitrate.toHumanBitrate() else "",
|
||||
tag = it, call = { _player.selectVideoTrack(it.height) });
|
||||
}))
|
||||
)
|
||||
else null,
|
||||
if(liveStreamAudioFormats?.isEmpty() == false)
|
||||
SlideUpMenuGroup(this.context, context.getString(R.string.stream_audio), "audio",
|
||||
if (liveStreamAudioFormats?.isEmpty() == false)
|
||||
SlideUpMenuGroup(
|
||||
this.context, context.getString(R.string.stream_audio), "audio",
|
||||
*liveStreamAudioFormats
|
||||
.map {
|
||||
SlideUpMenuItem(this.context,
|
||||
val language = it.language
|
||||
val mainText = when {
|
||||
language != null && it.bitrate == Format.NO_VALUE -> Locale.forLanguageTag(language).displayLanguage
|
||||
language == null && it.bitrate != Format.NO_VALUE -> it.bitrate.toHumanBitrate()
|
||||
language != null && it.bitrate != Format.NO_VALUE -> "${Locale.forLanguageTag(language).displayLanguage} ${it.bitrate.toHumanBitrate()}"
|
||||
else -> "Default"
|
||||
}
|
||||
SlideUpMenuItem(
|
||||
this.context,
|
||||
R.drawable.ic_music,
|
||||
"${it.label ?: it.containerMimeType} ${it.bitrate}",
|
||||
"",
|
||||
mainText,
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) it.containerMimeType ?: "" else "",
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) it.label else "",
|
||||
tag = it,
|
||||
call = { _player.selectAudioTrack(it.bitrate) });
|
||||
}.toList().toTypedArray())
|
||||
}.toList().toTypedArray()
|
||||
)
|
||||
else null,
|
||||
|
||||
if(bestVideoSources.isNotEmpty())
|
||||
SlideUpMenuGroup(this.context, context.getString(R.string.video), "video",
|
||||
*bestVideoSources
|
||||
.map {
|
||||
val estSize = VideoHelper.estimateSourceSize(it);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
SlideUpMenuItem(this.context,
|
||||
R.drawable.ic_movie,
|
||||
it!!.name,
|
||||
if (it.width > 0 && it.height > 0) "${it.width}x${it.height}" else "",
|
||||
(prefix + it.codec.trim()).trim(),
|
||||
if (it.name != "") it.name else "${it.height}p${it.frameRate ?: ""}",
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) it.codec.trim() else "",
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) it.bitrate?.toHumanBitrate() ?: "" else "",
|
||||
tag = it,
|
||||
call = { handleSelectVideoTrack(it) });
|
||||
}.toList().toTypedArray())
|
||||
@@ -2244,13 +2321,17 @@ class VideoDetailView : ConstraintLayout {
|
||||
SlideUpMenuGroup(this.context, context.getString(R.string.audio), "audio",
|
||||
*bestAudioSources
|
||||
.map {
|
||||
val estSize = VideoHelper.estimateSourceSize(it);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
val mainText = when {
|
||||
it.language != Language.UNKNOWN && it.bitrate == Format.NO_VALUE -> Locale.forLanguageTag(it.language).displayLanguage
|
||||
it.language == Language.UNKNOWN && it.bitrate != Format.NO_VALUE -> it.bitrate.toHumanBitrate()
|
||||
it.language != Language.UNKNOWN && it.bitrate != Format.NO_VALUE -> "${Locale.forLanguageTag(it.language).displayLanguage} ${it.bitrate.toHumanBitrate()}"
|
||||
else -> "Default"
|
||||
}
|
||||
SlideUpMenuItem(this.context,
|
||||
R.drawable.ic_music,
|
||||
it.name,
|
||||
it.bitrate.toHumanBitrate(),
|
||||
(prefix + it.codec.trim()).trim(),
|
||||
if (it.name != "") it.name else mainText,
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) it.codec.trim() ?: "" else "",
|
||||
if (Settings.instance.playback.showAdvancedMediaSourceMetadata) (if (it.original) "Original" else "") else "",
|
||||
tag = it,
|
||||
call = { handleSelectAudioTrack(it) });
|
||||
}.toList().toTypedArray())
|
||||
@@ -2522,7 +2603,9 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
|
||||
fun saveBrightness() {
|
||||
_player.gestureControl.saveBrightness()
|
||||
if (Settings.instance.gestureControls.useSystemBrightness) {
|
||||
_player.gestureControl.saveBrightness()
|
||||
}
|
||||
}
|
||||
fun restoreBrightness() {
|
||||
_player.gestureControl.restoreBrightness()
|
||||
@@ -2702,6 +2785,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
if(it is IPlatformVideo) {
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
else
|
||||
UIDialogs.toast(context.getString(R.string.already_in_watch_later))
|
||||
}
|
||||
}
|
||||
onAddToQueueClicked.subscribe(this) {
|
||||
|
||||
@@ -35,6 +35,9 @@ class Playlist {
|
||||
this.videos = ArrayList(list);
|
||||
}
|
||||
|
||||
fun makeCopy(newName: String? = null): Playlist {
|
||||
return Playlist(newName ?: name, videos)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromV8(config: SourcePluginConfig, obj: V8ValueObject?): Playlist? {
|
||||
|
||||
@@ -8,11 +8,14 @@ import android.text.method.LinkMovementMethod
|
||||
import android.text.style.URLSpan
|
||||
import android.view.MotionEvent
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||
import com.futo.platformplayer.timestampRegex
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMethod() {
|
||||
|
||||
@@ -60,31 +63,39 @@ class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMe
|
||||
val dx = event.x - downX
|
||||
val dy = event.y - downY
|
||||
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(widget, event)) {
|
||||
runBlocking {
|
||||
for (link in pressedLinks!!) {
|
||||
Logger.i(TAG) { "Link clicked '${link.url}'." }
|
||||
for (link in pressedLinks!!) {
|
||||
Logger.i(TAG) { "Link clicked '${link.url}'." }
|
||||
|
||||
if (_context is MainActivity) {
|
||||
if (_context.handleUrl(link.url)) continue
|
||||
if (timestampRegex.matches(link.url)) {
|
||||
val tokens = link.url.split(':')
|
||||
var time_s = -1L
|
||||
when (tokens.size) {
|
||||
2 -> time_s = tokens[0].toLong() * 60 + tokens[1].toLong()
|
||||
3 -> time_s = tokens[0].toLong() * 3600 +
|
||||
tokens[1].toLong() * 60 +
|
||||
tokens[2].toLong()
|
||||
}
|
||||
val c = _context
|
||||
if (c is MainActivity) {
|
||||
c.lifecycleScope.launch(Dispatchers.IO) {
|
||||
if (c.handleUrl(link.url)) {
|
||||
return@launch
|
||||
}
|
||||
if (timestampRegex.matches(link.url)) {
|
||||
val tokens = link.url.split(':')
|
||||
var time_s = -1L
|
||||
when (tokens.size) {
|
||||
2 -> time_s = tokens[0].toLong() * 60 + tokens[1].toLong()
|
||||
3 -> time_s = tokens[0].toLong() * 3600 +
|
||||
tokens[1].toLong() * 60 +
|
||||
tokens[2].toLong()
|
||||
}
|
||||
|
||||
if (time_s != -1L) {
|
||||
if (time_s != -1L) {
|
||||
withContext(Dispatchers.Main) {
|
||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000)
|
||||
continue
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pressedLinks = null
|
||||
linkPressed = false
|
||||
return true
|
||||
|
||||
@@ -156,6 +156,8 @@ class StateApp {
|
||||
return thisContext;
|
||||
}
|
||||
|
||||
private var _mainId: String? = null;
|
||||
|
||||
//Files
|
||||
private var _tempDirectory: File? = null;
|
||||
private var _cacheDirectory: File? = null;
|
||||
@@ -295,9 +297,12 @@ class StateApp {
|
||||
}
|
||||
|
||||
//Lifecycle
|
||||
fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null) {
|
||||
fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null, mainId: String? = null) {
|
||||
_mainId = mainId;
|
||||
_context = context;
|
||||
_scope = coroutineScope
|
||||
Logger.w(TAG, "Scope initialized ${(coroutineScope != null)}\n ${Log.getStackTraceString(Throwable())}")
|
||||
|
||||
}
|
||||
|
||||
fun initializeFiles(force: Boolean = false) {
|
||||
@@ -719,7 +724,9 @@ class StateApp {
|
||||
migrateStores(context, managedStores, index + 1);
|
||||
}
|
||||
|
||||
fun mainAppDestroyed(context: Context) {
|
||||
fun mainAppDestroyed(context: Context, mainId: String? = null) {
|
||||
if (mainId != null && (_mainId != mainId || _mainId == null))
|
||||
return
|
||||
Logger.i(TAG, "App ended");
|
||||
_receiverBecomingNoisy?.let {
|
||||
_receiverBecomingNoisy = null;
|
||||
@@ -743,7 +750,8 @@ class StateApp {
|
||||
|
||||
fun dispose(){
|
||||
_context = null;
|
||||
_scope = null;
|
||||
// _scope = null;
|
||||
Logger.w(TAG, "StateApp disposed: ${Log.getStackTraceString(Throwable())}")
|
||||
}
|
||||
|
||||
private val _connectivityEvents = object : ConnectivityManager.NetworkCallback() {
|
||||
|
||||
@@ -178,31 +178,30 @@ class StatePlaylists {
|
||||
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
||||
}
|
||||
}
|
||||
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1): Boolean {
|
||||
var wasNew = false;
|
||||
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false): Boolean {
|
||||
synchronized(_watchlistStore) {
|
||||
if(!_watchlistStore.hasItem { it.url == video.url })
|
||||
wasNew = true;
|
||||
_watchlistStore.saveAsync(video);
|
||||
if(orderPosition == -1)
|
||||
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray());
|
||||
else {
|
||||
val existing = _watchlistOrderStore.getAllValues().toMutableList();
|
||||
existing.add(orderPosition, video.url);
|
||||
_watchlistOrderStore.set(*existing.toTypedArray());
|
||||
if (_watchlistStore.hasItem { it.url == video.url }) {
|
||||
return false
|
||||
}
|
||||
_watchlistOrderStore.save();
|
||||
|
||||
_watchlistStore.saveAsync(video)
|
||||
if (Settings.instance.other.addToBeginning) {
|
||||
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray())
|
||||
} else {
|
||||
_watchlistOrderStore.set(*(_watchlistOrderStore.values + listOf(video.url)).toTypedArray())
|
||||
}
|
||||
_watchlistOrderStore.save()
|
||||
}
|
||||
onWatchLaterChanged.emit();
|
||||
|
||||
if(isUserInteraction) {
|
||||
if (isUserInteraction) {
|
||||
val now = OffsetDateTime.now();
|
||||
_watchLaterAdds.setAndSave(video.url, now);
|
||||
broadcastWatchLaterAddition(video, now);
|
||||
}
|
||||
|
||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||
return wasNew;
|
||||
return true;
|
||||
}
|
||||
|
||||
fun getLastPlayedPlaylist() : Playlist? {
|
||||
|
||||
@@ -31,10 +31,11 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
||||
updateDataset();
|
||||
}
|
||||
|
||||
constructor(inflater: LayoutInflater, confirmationMessage: String, onDatasetChanged: ((List<Subscription>)->Unit)? = null) : super() {
|
||||
constructor(inflater: LayoutInflater, confirmationMessage: String, sortByDefault: Int, onDatasetChanged: ((List<Subscription>)->Unit)? = null) : super() {
|
||||
_inflater = inflater;
|
||||
_confirmationMessage = confirmationMessage;
|
||||
_onDatasetChanged = onDatasetChanged;
|
||||
sortBy = sortByDefault
|
||||
|
||||
StateSubscriptions.instance.onSubscriptionsChanged.subscribe { _, _ -> if(Looper.myLooper() != Looper.getMainLooper())
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { updateDataset() }
|
||||
|
||||
@@ -39,6 +39,9 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
class GestureControlView : LinearLayout {
|
||||
@@ -79,6 +82,9 @@ class GestureControlView : LinearLayout {
|
||||
private var _adjustingFullscreenDown: Boolean = false;
|
||||
private var _fullScreenFactorUp = 1.0f;
|
||||
private var _fullScreenFactorDown = 1.0f;
|
||||
private val _layoutHoldSpeed: LinearLayout
|
||||
private val _textHoldFastForward: TextView
|
||||
private val _imageHoldFastForward: ImageView
|
||||
|
||||
private var _scaleGestureDetector: ScaleGestureDetector
|
||||
private var _scaleFactor = 1.0f
|
||||
@@ -92,6 +98,11 @@ class GestureControlView : LinearLayout {
|
||||
private var _surfaceView: View? = null
|
||||
private var _layoutIndicatorFill: FrameLayout;
|
||||
private var _layoutIndicatorFit: FrameLayout;
|
||||
private var _speedHolding = false
|
||||
|
||||
private val _speedFormatter = DecimalFormat("#.##", DecimalFormatSymbols(Locale.US)).apply {
|
||||
roundingMode = java.math.RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
private val _gestureController: GestureDetectorCompat;
|
||||
|
||||
@@ -103,6 +114,8 @@ class GestureControlView : LinearLayout {
|
||||
val onZoom = Event1<Float>();
|
||||
val onSoundAdjusted = Event1<Float>();
|
||||
val onToggleFullscreen = Event0();
|
||||
val onSpeedHoldStart = Event0()
|
||||
val onSpeedHoldEnd = Event0()
|
||||
|
||||
var fullScreenGestureEnabled = true
|
||||
|
||||
@@ -124,6 +137,9 @@ class GestureControlView : LinearLayout {
|
||||
_layoutControlsFullscreen = findViewById(R.id.layout_controls_fullscreen);
|
||||
_layoutIndicatorFill = findViewById(R.id.layout_indicator_fill);
|
||||
_layoutIndicatorFit = findViewById(R.id.layout_indicator_fit);
|
||||
_layoutHoldSpeed = findViewById(R.id.layout_controls_increased_speed)
|
||||
_textHoldFastForward = findViewById(R.id.text_holdFastForward)
|
||||
_imageHoldFastForward = findViewById(R.id.image_holdFastForward)
|
||||
|
||||
_scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||
@@ -216,7 +232,20 @@ class GestureControlView : LinearLayout {
|
||||
|
||||
return true;
|
||||
}
|
||||
override fun onLongPress(p0: MotionEvent) = Unit
|
||||
override fun onLongPress(p0: MotionEvent) {
|
||||
if (!_isControlsLocked
|
||||
&& !_skipping
|
||||
&& !_adjustingBrightness
|
||||
&& !_adjustingSound
|
||||
&& !_adjustingFullscreenUp
|
||||
&& !_adjustingFullscreenDown
|
||||
&& !_isPanning
|
||||
&& !_isZooming) {
|
||||
_speedHolding = true
|
||||
showHoldSpeedControls()
|
||||
onSpeedHoldStart.emit()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
|
||||
@@ -301,6 +330,17 @@ class GestureControlView : LinearLayout {
|
||||
onPan.emit(_translationX, _translationY)
|
||||
}
|
||||
|
||||
private fun showHoldSpeedControls() {
|
||||
_layoutHoldSpeed.visibility = View.VISIBLE
|
||||
_textHoldFastForward.text = _speedFormatter.format(Settings.instance.playback.getHoldPlaybackSpeed()) + "x"
|
||||
(_imageHoldFastForward.drawable as? Animatable)?.start()
|
||||
}
|
||||
|
||||
private fun hideHoldSpeedControls() {
|
||||
_layoutHoldSpeed.visibility = View.GONE
|
||||
(_imageHoldFastForward.drawable as? Animatable)?.stop()
|
||||
}
|
||||
|
||||
fun setupTouchArea(layoutControls: ViewGroup? = null, background: View? = null) {
|
||||
_layoutControls = layoutControls;
|
||||
_background = background;
|
||||
@@ -309,6 +349,12 @@ class GestureControlView : LinearLayout {
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
val ev = event ?: return super.onTouchEvent(event);
|
||||
|
||||
if (ev.action == MotionEvent.ACTION_UP && _speedHolding) {
|
||||
_speedHolding = false
|
||||
hideHoldSpeedControls()
|
||||
onSpeedHoldEnd.emit()
|
||||
}
|
||||
|
||||
cancelHideJob();
|
||||
|
||||
if (_skipping) {
|
||||
|
||||
@@ -8,12 +8,16 @@ import android.text.Spannable
|
||||
import android.text.style.URLSpan
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.timestampRegex
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
||||
private var _lastTouchedLinks: Array<URLSpan>? = null
|
||||
@@ -77,12 +81,14 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
||||
val dx = event.x - downX
|
||||
val dy = event.y - downY
|
||||
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(event)) {
|
||||
runBlocking {
|
||||
for (link in _lastTouchedLinks!!) {
|
||||
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." }
|
||||
val c = context
|
||||
if (c is MainActivity) {
|
||||
if (c.handleUrl(link.url)) continue
|
||||
for (link in _lastTouchedLinks!!) {
|
||||
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." }
|
||||
val c = context
|
||||
if (c is MainActivity) {
|
||||
c.lifecycleScope.launch(Dispatchers.IO) {
|
||||
if (c.handleUrl(link.url)) {
|
||||
return@launch
|
||||
}
|
||||
if (timestampRegex.matches(link.url)) {
|
||||
val tokens = link.url.split(':')
|
||||
var time_s = -1L
|
||||
@@ -92,13 +98,21 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
||||
tokens[1].toLong() * 60 +
|
||||
tokens[2].toLong()
|
||||
}
|
||||
|
||||
if (time_s != -1L) {
|
||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000)
|
||||
continue
|
||||
withContext(Dispatchers.Main) {
|
||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
|
||||
} else {
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.media3.ui.DefaultTimeBar
|
||||
import androidx.media3.ui.TimeBar
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.casting.AirPlayCastingDevice
|
||||
@@ -58,6 +59,8 @@ class CastView : ConstraintLayout {
|
||||
private var _inPictureInPicture: Boolean = false;
|
||||
private var _chapters: List<IChapter>? = null;
|
||||
private var _currentChapter: IChapter? = null;
|
||||
private var _speedHoldPrevRate = 1.0
|
||||
private var _speedHoldWasPlaying = false
|
||||
|
||||
val onChapterChanged = Event2<IChapter?, Boolean>();
|
||||
val onMinimizeClick = Event0();
|
||||
@@ -87,6 +90,20 @@ class CastView : ConstraintLayout {
|
||||
_gestureControlView = findViewById(R.id.gesture_control);
|
||||
_gestureControlView.fullScreenGestureEnabled = false
|
||||
_gestureControlView.setupTouchArea();
|
||||
_gestureControlView.onSpeedHoldStart.subscribe {
|
||||
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
||||
_speedHoldWasPlaying = d.isPlaying
|
||||
_speedHoldPrevRate = d.speed
|
||||
if (d.canSetSpeed)
|
||||
d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
|
||||
d.resumeVideo()
|
||||
}
|
||||
_gestureControlView.onSpeedHoldEnd.subscribe {
|
||||
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
||||
if (!_speedHoldWasPlaying) d.pauseVideo()
|
||||
d.changeSpeed(_speedHoldPrevRate)
|
||||
}
|
||||
|
||||
_gestureControlView.onSeek.subscribe {
|
||||
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
||||
StateCasting.instance.videoSeekTo(d.expectedCurrentTime + it / 1000);
|
||||
|
||||
@@ -90,7 +90,6 @@ class ToggleField : TableRow, IField {
|
||||
|
||||
val advancedFieldAttr = field.getAnnotation(AdvancedField::class.java)
|
||||
if(advancedFieldAttr != null || advanced) {
|
||||
Logger.w("ToggleField", "Found advanced field: " + field.name);
|
||||
isAdvanced = true;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ class SlideUpMenuButtonList : LinearLayout {
|
||||
fun setButtons(texts: List<String>, activeText: String? = null) {
|
||||
_root.removeAllViews();
|
||||
|
||||
val marginLeft = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.0f, resources.displayMetrics).toInt();
|
||||
val marginLeft = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.5f, resources.displayMetrics).toInt();
|
||||
val marginRight = marginLeft;
|
||||
|
||||
buttons.clear();
|
||||
|
||||
+15
-11
@@ -13,6 +13,7 @@ import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
|
||||
@@ -42,10 +43,14 @@ class SlideUpMenuOverlay : RelativeLayout {
|
||||
constructor(context: Context, parent: ViewGroup, titleText: String, okText: String?, animated: Boolean, items: List<View>, hideButtons: Boolean = false): super(context){
|
||||
init(animated, okText);
|
||||
_container = parent;
|
||||
if(!_container!!.children.contains(this)) {
|
||||
_container!!.removeAllViews();
|
||||
_container!!.addView(this);
|
||||
_container!!.removeAllViews();
|
||||
_container!!.addView(this);
|
||||
if (_container!!.isVisible) {
|
||||
isVisible = true
|
||||
_viewBackground.alpha = 1.0f;
|
||||
_viewOverlayContainer.translationY = 0.0f;
|
||||
}
|
||||
|
||||
_textTitle.text = titleText;
|
||||
groupItems = items;
|
||||
|
||||
@@ -56,6 +61,12 @@ class SlideUpMenuOverlay : RelativeLayout {
|
||||
}
|
||||
|
||||
setItems(items);
|
||||
|
||||
if (!isVisible) {
|
||||
_viewOverlayContainer.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
|
||||
_viewOverlayContainer.translationY = _viewOverlayContainer.measuredHeight.toFloat()
|
||||
_viewBackground.alpha = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -146,16 +157,9 @@ class SlideUpMenuOverlay : RelativeLayout {
|
||||
}
|
||||
|
||||
isVisible = true;
|
||||
_container?.post {
|
||||
_container?.visibility = View.VISIBLE;
|
||||
_container?.bringToFront();
|
||||
}
|
||||
_container?.visibility = View.VISIBLE;
|
||||
|
||||
if (_animated) {
|
||||
_viewOverlayContainer.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
|
||||
_viewOverlayContainer.translationY = _viewOverlayContainer.measuredHeight.toFloat()
|
||||
_viewBackground.alpha = 0f;
|
||||
|
||||
val animations = arrayListOf<Animator>();
|
||||
animations.add(ObjectAnimator.ofFloat(_viewBackground, "alpha", 0.0f, 1.0f).setDuration(ANIMATION_DURATION_MS));
|
||||
animations.add(ObjectAnimator.ofFloat(_viewOverlayContainer, "translationY", _viewOverlayContainer.measuredHeight.toFloat(), 0.0f).setDuration(ANIMATION_DURATION_MS));
|
||||
|
||||
@@ -117,6 +117,9 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
private var _isControlsLocked: Boolean = false;
|
||||
|
||||
private var _speedHoldPrevRate = 1f
|
||||
private var _speedHoldWasPlaying = false
|
||||
|
||||
private val _time_bar_listener: TimeBar.OnScrubListener;
|
||||
|
||||
var isFitMode : Boolean = false
|
||||
@@ -254,6 +257,20 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
gestureControl = findViewById(R.id.gesture_control);
|
||||
|
||||
gestureControl.setupTouchArea(_layoutControls, background);
|
||||
gestureControl.onSpeedHoldStart.subscribe {
|
||||
exoPlayer?.player?.let { player ->
|
||||
_speedHoldWasPlaying = player.isPlaying
|
||||
_speedHoldPrevRate = getPlaybackRate()
|
||||
setPlaybackRate(Settings.instance.playback.getHoldPlaybackSpeed().toFloat())
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
gestureControl.onSpeedHoldEnd.subscribe {
|
||||
exoPlayer?.player?.let { player ->
|
||||
if (!_speedHoldWasPlaying) player.pause()
|
||||
setPlaybackRate(_speedHoldPrevRate)
|
||||
}
|
||||
}
|
||||
gestureControl.onSeek.subscribe { seekFromCurrent(it); };
|
||||
gestureControl.onSoundAdjusted.subscribe {
|
||||
if (Settings.instance.gestureControls.useSystemVolume) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="10dp"
|
||||
android:animateLayoutChanges="true">
|
||||
<ScrollView
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
@@ -152,13 +152,14 @@
|
||||
android:id="@+id/button_add_sources"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
app:buttonIcon="@drawable/ic_explore"
|
||||
app:buttonText="Add Sources"
|
||||
app:buttonSubText="Install new sources to see more content."
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
android:isScrollContainer="false"
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="13sp"
|
||||
android:maxLines="100"
|
||||
android:maxLines="150"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_metadata"
|
||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
android:layout_marginTop="10dp"
|
||||
android:id="@+id/root"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingEnd="6dp">
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp">
|
||||
|
||||
</LinearLayout>
|
||||
@@ -195,4 +195,39 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_controls_increased_speed"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:background="@drawable/background_pill_black"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_holdFastForward"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="2x"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_holdFastForward"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="8dp"
|
||||
android:adjustViewBounds="true"
|
||||
app:srcCompat="@drawable/ic_fastforward_animated"
|
||||
android:layout_marginStart="4dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -3,4 +3,5 @@
|
||||
<dimen name="minimized_player_max_width">500dp</dimen>
|
||||
<dimen name="app_bar_height">200dp</dimen>
|
||||
<integer name="column_width_dp">400</integer>
|
||||
<integer name="smallest_width_dp">600</integer>
|
||||
</resources>
|
||||
|
||||
@@ -76,6 +76,8 @@
|
||||
<string name="always_proxy_requests_description">Always proxy requests when casting data through the device.</string>
|
||||
<string name="allow_ipv6">Allow IPV6</string>
|
||||
<string name="allow_ipv6_description">If casting over IPV6 is allowed, can cause issues on some networks</string>
|
||||
<string name="allow_ipv4">Allow Link Local IPV4</string>
|
||||
<string name="allow_ipv4_description">If casting over IPV4 link local is allowed, can cause issues on some networks</string>
|
||||
<string name="discover">Discover</string>
|
||||
<string name="find_new_video_sources_to_add">Find new video sources to add</string>
|
||||
<string name="these_sources_have_been_disabled">These sources have been disabled</string>
|
||||
@@ -298,6 +300,7 @@
|
||||
<string name="check_disabled_plugin_updates_description">Check disabled plugins for updates</string>
|
||||
<string name="planned_content_notifications">Planned Content Notifications</string>
|
||||
<string name="planned_content_notifications_description">Schedules discovered planned content as notifications, resulting in more accurate notifications for this content.</string>
|
||||
<string name="show_advanced_media_source_metadata_desc">When displaying media sources show advanced metadata like container and codec</string>
|
||||
<string name="attempt_to_utilize_byte_ranges">Attempt to utilize byte ranges</string>
|
||||
<string name="auto_update">Auto Update</string>
|
||||
<string name="always_allow_reverse_landscape_auto_rotate">Always allow reverse landscape auto-rotate</string>
|
||||
@@ -346,6 +349,7 @@
|
||||
<string name="default_audio_quality">Default Audio Quality</string>
|
||||
<string name="default_playback_speed">Default Playback Speed</string>
|
||||
<string name="default_video_quality">Default Video Quality</string>
|
||||
<string name="show_advanced_media_source_metadata">Show Advanced Media Source Metadata</string>
|
||||
<string name="deletes_license_keys_from_app">Deletes license keys from app</string>
|
||||
<string name="download_when">Download when</string>
|
||||
<string name="enable_video_cache">Enable Video Cache</string>
|
||||
@@ -427,6 +431,14 @@
|
||||
<string name="delete_watchlist_on_finish">Delete from WatchLater when watched</string>
|
||||
<string name="delete_watchlist_on_finish_description">After you leave a video that you mostly watched, it will be removed from watch later.</string>
|
||||
<string name="seek_offset">Seek duration</string>
|
||||
<string name="min_playback_speed">Minimum Playback Speed</string>
|
||||
<string name="min_playback_speed_description">Minimum Available Speed</string>
|
||||
<string name="max_playback_speed">Maximum Playback Speed</string>
|
||||
<string name="max_playback_speed_description">Maximum Available Speed</string>
|
||||
<string name="hold_playback_speed">Hold playback speed</string>
|
||||
<string name="hold_playback_speed_description">Playback speed when pressing down on the video</string>
|
||||
<string name="step_playback_speed">Playback Speed Step Size</string>
|
||||
<string name="step_playback_speed_description">The step size of playback speeds, may not affect higher playback speeds.</string>
|
||||
<string name="seek_offset_description">Fast-Forward / Fast-Rewind duration</string>
|
||||
<string name="background_switch_audio">Switch to Audio in Background</string>
|
||||
<string name="subscription_group_menu">Groups</string>
|
||||
@@ -458,6 +470,9 @@
|
||||
<string name="playlist_delete_confirmation_description">Show confirmation dialog when deleting media from a playlist</string>
|
||||
<string name="playlist_allow_dups">Allow duplicate playlist videos</string>
|
||||
<string name="playlist_allow_dups_description">Allow adding duplicate videos to playlists</string>
|
||||
<string name="add_to_beginning_of_watch_later">Add new videos to the beginning of Watch Later</string>
|
||||
<string name="add_to_beginning_description">When adding videos to Watch Later add them to the beginning of the list instead of the end</string>
|
||||
<string name="already_in_watch_later">Already in watch later</string>
|
||||
<string name="enable_polycentric">Enable Polycentric</string>
|
||||
<string name="polycentric_local_cache">Enable Polycentric Local Caching</string>
|
||||
<string name="polycentric_local_cache_description">Caches polycentric results on-device to reduce load times, changing requires app reboot</string>
|
||||
@@ -1091,6 +1106,33 @@
|
||||
<item>30 seconds</item>
|
||||
<item>60 seconds</item>
|
||||
</string-array>
|
||||
<string-array name="max_playback_speed">
|
||||
<item>2.0</item>
|
||||
<item>2.25</item>
|
||||
<item>3.0</item>
|
||||
<item>4.0</item>
|
||||
<item>5.0</item>
|
||||
</string-array>
|
||||
<string-array name="hold_playback_speeds">
|
||||
<item>1.25</item>
|
||||
<item>1.5</item>
|
||||
<item>1.75</item>
|
||||
<item>2.0</item>
|
||||
<item>2.25</item>
|
||||
<item>2.5</item>
|
||||
<item>2.75</item>
|
||||
<item>3.0</item>
|
||||
</string-array>
|
||||
<string-array name="min_playback_speed">
|
||||
<item>0.25</item>
|
||||
<item>0.5</item>
|
||||
<item>1.0</item>
|
||||
</string-array>
|
||||
<string-array name="step_playback_speed">
|
||||
<item>0.05</item>
|
||||
<item>0.1</item>
|
||||
<item>0.25</item>
|
||||
</string-array>
|
||||
<string-array name="rotation_zone">
|
||||
<item>15</item>
|
||||
<item>30</item>
|
||||
|
||||
Submodule app/src/stable/assets/sources/nebula updated: 97a5ad5a37...880da6a015
Submodule app/src/stable/assets/sources/rumble updated: 3bbce81794...401274b1ec
Submodule app/src/stable/assets/sources/spotify updated: 1d884f50ab...d025804364
Submodule app/src/stable/assets/sources/youtube updated: 6d6838e2a4...2e25829494
@@ -7,7 +7,7 @@
|
||||
<application>
|
||||
<receiver android:name=".receivers.InstallReceiver" />
|
||||
|
||||
<activity android:name=".activities.MainActivity">
|
||||
<activity android:name=".activities.MainActivity" android:launchMode="singleInstance">
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
|
||||
Submodule app/src/unstable/assets/sources/nebula updated: 97a5ad5a37...880da6a015
Submodule app/src/unstable/assets/sources/rumble updated: 3bbce81794...401274b1ec
Submodule app/src/unstable/assets/sources/spotify updated: 1d884f50ab...d025804364
Submodule app/src/unstable/assets/sources/youtube updated: 6d6838e2a4...2e25829494
Reference in New Issue
Block a user