mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-29 19:13:01 +02:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8491d4da1a | |||
| 9bea1563ca | |||
| 9e7b936663 | |||
| 19c84475db | |||
| 4164b1a3f8 | |||
| a9dc038190 | |||
| 2825db88a5 | |||
| 363099b303 | |||
| 5e25a5054f | |||
| 2bc6127f6b | |||
| 064824aedf | |||
| 52044edb2e | |||
| fb12073a82 | |||
| de39451f67 | |||
| 8f28653b28 | |||
| 389798457b | |||
| dd1c04bea1 | |||
| e6159117f6 | |||
| 0d9e1cd3c5 | |||
| 10753eb879 | |||
| 29aec21095 | |||
| a810f82ce2 | |||
| 2c454a0ec5 | |||
| d3dca00482 | |||
| d08dffd9e2 | |||
| 5b50ac926e | |||
| 57a3be35d0 | |||
| 70f36e69e6 | |||
| 8e70f1b865 | |||
| f86fb0ee44 | |||
| fe0aac7c6e | |||
| b93447f712 | |||
| 84a5103526 |
@@ -241,8 +241,11 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sortedAddresses: List<InetAddress> = addresses
|
||||||
|
.sortedBy { addr -> addressScore(addr) }
|
||||||
|
|
||||||
val sockets: ArrayList<Socket> = arrayListOf();
|
val sockets: ArrayList<Socket> = arrayListOf();
|
||||||
for (i in addresses.indices) {
|
for (i in sortedAddresses.indices) {
|
||||||
sockets.add(Socket());
|
sockets.add(Socket());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +253,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
|
|||||||
var connectedSocket: Socket? = null;
|
var connectedSocket: Socket? = null;
|
||||||
val threads: ArrayList<Thread> = arrayListOf();
|
val threads: ArrayList<Thread> = arrayListOf();
|
||||||
for (i in 0 until sockets.size) {
|
for (i in 0 until sockets.size) {
|
||||||
val address = addresses[i];
|
val address = sortedAddresses[i];
|
||||||
val socket = sockets[i];
|
val socket = sockets[i];
|
||||||
val thread = Thread {
|
val thread = Thread {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -531,6 +531,59 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
else -> 10_000L;
|
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.comments, "group", R.string.comments_description, 6)
|
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||||
@@ -628,6 +681,11 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var allowIpv6: Boolean = true;
|
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?
|
/*TODO: Should we have a different casting quality?
|
||||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
|
|||||||
@@ -434,7 +434,7 @@ private fun interfaceScore(nif: NetworkInterface): Int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addressScore(addr: InetAddress): Int {
|
fun addressScore(addr: InetAddress): Int {
|
||||||
return when (addr) {
|
return when (addr) {
|
||||||
is Inet4Address -> {
|
is Inet4Address -> {
|
||||||
val octets = addr.address.map { it.toInt() and 0xFF }
|
val octets = addr.address.map { it.toInt() and 0xFF }
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ import java.io.StringWriter
|
|||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import java.util.Queue
|
import java.util.Queue
|
||||||
|
import java.util.UUID
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
|
||||||
@@ -218,6 +219,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val mainId = UUID.randomUUID().toString().substring(0, 5)
|
||||||
|
|
||||||
constructor() : super() {
|
constructor() : super() {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
StrictMode.setVmPolicy(
|
StrictMode.setVmPolicy(
|
||||||
@@ -269,8 +272,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Logger.i(TAG, "MainActivity Starting");
|
Logger.w(TAG, "MainActivity Starting [$mainId]");
|
||||||
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
StateApp.instance.setGlobalContext(this, lifecycleScope, mainId);
|
||||||
StateApp.instance.mainAppStarting(this);
|
StateApp.instance.mainAppStarting(this);
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -671,13 +674,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
Logger.v(TAG, "onResume")
|
Logger.w(TAG, "onResume [$mainId]")
|
||||||
_isVisible = true;
|
_isVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
Logger.v(TAG, "onPause")
|
Logger.w(TAG, "onPause [$mainId]")
|
||||||
_isVisible = false;
|
_isVisible = false;
|
||||||
|
|
||||||
_qrCodeLoadingDialog?.dismiss()
|
_qrCodeLoadingDialog?.dismiss()
|
||||||
@@ -686,7 +689,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
Logger.v(TAG, "_wasStopped = true");
|
Logger.w(TAG, "onStop [$mainId]");
|
||||||
_wasStopped = true;
|
_wasStopped = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1103,8 +1106,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
Logger.v(TAG, "onDestroy")
|
Logger.w(TAG, "onDestroy [$mainId]")
|
||||||
StateApp.instance.mainAppDestroyed(this);
|
StateApp.instance.mainAppDestroyed(this, mainId);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T> isFragmentActive(): Boolean {
|
inline fun <reified T> isFragmentActive(): Boolean {
|
||||||
|
|||||||
@@ -166,10 +166,11 @@ class StateCasting {
|
|||||||
Logger.i(TAG, "CastingService started.");
|
Logger.i(TAG, "CastingService started.");
|
||||||
|
|
||||||
_nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
_nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
||||||
|
startDiscovering()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun startDiscovering() {
|
private fun startDiscovering() {
|
||||||
_nsdManager?.apply {
|
_nsdManager?.apply {
|
||||||
_discoveryListeners.forEach {
|
_discoveryListeners.forEach {
|
||||||
discoverServices(it.key, NsdManager.PROTOCOL_DNS_SD, it.value)
|
discoverServices(it.key, NsdManager.PROTOCOL_DNS_SD, it.value)
|
||||||
@@ -178,7 +179,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun stopDiscovering() {
|
private fun stopDiscovering() {
|
||||||
_nsdManager?.apply {
|
_nsdManager?.apply {
|
||||||
_discoveryListeners.forEach {
|
_discoveryListeners.forEach {
|
||||||
try {
|
try {
|
||||||
@@ -1220,9 +1221,16 @@ class StateCasting {
|
|||||||
|
|
||||||
private fun getLocalUrl(ad: CastingDevice): String {
|
private fun getLocalUrl(ad: CastingDevice): String {
|
||||||
var address = ad.localAddress!!
|
var address = ad.localAddress!!
|
||||||
if (address.isLinkLocalAddress) {
|
if (Settings.instance.casting.allowLinkLocalIpv4) {
|
||||||
address = findPreferredAddress() ?: address
|
if (address.isLinkLocalAddress && address is Inet6Address) {
|
||||||
Logger.i(TAG, "Selected casting address: $address")
|
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}";
|
return "http://${address.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
super.show();
|
super.show();
|
||||||
Logger.i(TAG, "Dialog shown.");
|
Logger.i(TAG, "Dialog shown.");
|
||||||
|
|
||||||
StateCasting.instance.startDiscovering()
|
|
||||||
(_imageLoader.drawable as Animatable?)?.start();
|
(_imageLoader.drawable as Animatable?)?.start();
|
||||||
|
|
||||||
synchronized(StateCasting.instance.devices) {
|
synchronized(StateCasting.instance.devices) {
|
||||||
@@ -148,7 +147,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
override fun dismiss() {
|
override fun dismiss() {
|
||||||
super.dismiss()
|
super.dismiss()
|
||||||
(_imageLoader.drawable as Animatable?)?.stop()
|
(_imageLoader.drawable as Animatable?)?.stop()
|
||||||
StateCasting.instance.stopDiscovering()
|
|
||||||
StateCasting.instance.onDeviceAdded.remove(this)
|
StateCasting.instance.onDeviceAdded.remove(this)
|
||||||
StateCasting.instance.onDeviceChanged.remove(this)
|
StateCasting.instance.onDeviceChanged.remove(this)
|
||||||
StateCasting.instance.onDeviceRemoved.remove(this)
|
StateCasting.instance.onDeviceRemoved.remove(this)
|
||||||
|
|||||||
@@ -724,7 +724,7 @@ class VideoDownload {
|
|||||||
val t = cue.groupValues[1];
|
val t = cue.groupValues[1];
|
||||||
val d = cue.groupValues[2];
|
val d = cue.groupValues[2];
|
||||||
|
|
||||||
val url = foundTemplateUrl.replace("\$Number\$", indexCounter.toString());
|
val url = foundTemplateUrl.replace("\$Number\$", (indexCounter).toString());
|
||||||
|
|
||||||
val data = if(executor != null)
|
val data = if(executor != null)
|
||||||
executor.executeRequest("GET", url, null, mapOf());
|
executor.executeRequest("GET", url, null, mapOf());
|
||||||
|
|||||||
+6
-1
@@ -16,6 +16,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UISlideOverlays
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
|
import com.futo.platformplayer.views.adapters.SubscriptionAdapter
|
||||||
|
|
||||||
class CreatorsFragment : MainFragment() {
|
class CreatorsFragment : MainFragment() {
|
||||||
@@ -29,6 +31,8 @@ class CreatorsFragment : MainFragment() {
|
|||||||
private var _editSearch: EditText? = null;
|
private var _editSearch: EditText? = null;
|
||||||
private var _textMeta: TextView? = null;
|
private var _textMeta: TextView? = null;
|
||||||
private var _buttonClearSearch: ImageButton? = null
|
private var _buttonClearSearch: ImageButton? = null
|
||||||
|
private var _ordering = FragmentedStorage.get<StringStorage>("creators_ordering")
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
val view = inflater.inflate(R.layout.fragment_creators, container, false);
|
val view = inflater.inflate(R.layout.fragment_creators, container, false);
|
||||||
@@ -44,7 +48,7 @@ class CreatorsFragment : MainFragment() {
|
|||||||
_buttonClearSearch?.visibility = View.INVISIBLE;
|
_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 {
|
_textMeta?.let {
|
||||||
it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}";
|
it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}";
|
||||||
}
|
}
|
||||||
@@ -61,6 +65,7 @@ class CreatorsFragment : MainFragment() {
|
|||||||
spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
|
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
|
||||||
adapter.sortBy = pos;
|
adapter.sortBy = pos;
|
||||||
|
_ordering.setAndSave(pos.toString())
|
||||||
}
|
}
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||||
};
|
};
|
||||||
|
|||||||
+23
-12
@@ -10,7 +10,6 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
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.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
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)
|
StatePlaylists.instance.playlistStore.save(playlist)
|
||||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(
|
|
||||||
arrayListOf()
|
|
||||||
)
|
|
||||||
UIDialogs.toast("Playlist saved")
|
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?) {
|
fun onShown(parameter: Any?) {
|
||||||
_taskLoadPlaylist.cancel()
|
_taskLoadPlaylist.cancel()
|
||||||
|
|
||||||
@@ -188,12 +197,14 @@ class PlaylistFragment : MainFragment() {
|
|||||||
setButtonExportVisible(false)
|
setButtonExportVisible(false)
|
||||||
setButtonEditVisible(true)
|
setButtonEditVisible(true)
|
||||||
|
|
||||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
|
_fragment.topBar?.assume<NavigationTopBarFragment>()
|
||||||
_fragment.topBar?.assume<NavigationTopBarFragment>()
|
?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
|
||||||
?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
|
if (StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
|
||||||
copyPlaylist(parameter)
|
copyPlaylist(parameter)
|
||||||
}))
|
} else {
|
||||||
}
|
savePlaylist(parameter)
|
||||||
|
}
|
||||||
|
}))
|
||||||
} else {
|
} else {
|
||||||
setName(null)
|
setName(null)
|
||||||
setVideos(null, false)
|
setVideos(null, false)
|
||||||
@@ -259,7 +270,7 @@ class PlaylistFragment : MainFragment() {
|
|||||||
val playlist = _playlist ?: return
|
val playlist = _playlist ?: return
|
||||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
|
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
|
||||||
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to download", {
|
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to download", {
|
||||||
copyPlaylist(playlist)
|
savePlaylist(playlist)
|
||||||
download()
|
download()
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -292,7 +303,7 @@ class PlaylistFragment : MainFragment() {
|
|||||||
val playlist = _playlist ?: return
|
val playlist = _playlist ?: return
|
||||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
|
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == playlist.id }) {
|
||||||
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to edit the name", {
|
UIDialogs.showConfirmationDialog(context, "Playlist must be saved to edit the name", {
|
||||||
copyPlaylist(playlist)
|
savePlaylist(playlist)
|
||||||
onEditClick()
|
onEditClick()
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|||||||
+2
-2
@@ -191,7 +191,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
|
|
||||||
private var _bypassRateLimit = false;
|
private var _bypassRateLimit = false;
|
||||||
private val _lastExceptions: List<Throwable>? = null;
|
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;
|
val group = subGroup;
|
||||||
if(!_bypassRateLimit) {
|
if(!_bypassRateLimit) {
|
||||||
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(group);
|
val subRequestCounts = StateSubscriptions.instance.getSubscriptionRequestCount(group);
|
||||||
@@ -202,7 +202,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
throw RateLimitException(rateLimitPlugins.map { it.key.id });
|
throw RateLimitException(rateLimitPlugins.map { it.key.id });
|
||||||
}
|
}
|
||||||
_bypassRateLimit = false;
|
_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 feed = StateSubscriptions.instance.getFeed(group?.id);
|
||||||
|
|
||||||
val currentExs = feed?.exceptions ?: listOf();
|
val currentExs = feed?.exceptions ?: listOf();
|
||||||
|
|||||||
+64
-8
@@ -2,6 +2,8 @@ package com.futo.platformplayer.fragment.mainactivity.main
|
|||||||
|
|
||||||
import android.app.PictureInPictureParams
|
import android.app.PictureInPictureParams
|
||||||
import android.app.RemoteAction
|
import android.app.RemoteAction
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
@@ -172,6 +174,7 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
import java.util.Locale
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
@@ -408,6 +411,14 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
showChaptersUI();
|
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 {
|
_buttonSubscribe.onSubscribed.subscribe {
|
||||||
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||||
@@ -1399,8 +1410,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
onVideoChanged.emit(0, 0)
|
onVideoChanged.emit(0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val me = this;
|
||||||
if (video is JSVideoDetails) {
|
if (video is JSVideoDetails) {
|
||||||
val me = this;
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
//TODO: Implement video.getContentChapters()
|
//TODO: Implement video.getContentChapters()
|
||||||
@@ -1457,6 +1468,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 ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||||
val extraBytesRef = video.id.value?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
val extraBytesRef = video.id.value?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
||||||
@@ -2149,23 +2186,40 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
|
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
|
||||||
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
|
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(
|
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
|
||||||
R.string.quality), null, true,
|
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 {
|
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 ->
|
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) {
|
if (_isCasting) {
|
||||||
val ad = StateCasting.instance.activeDevice ?: return@subscribe
|
val ad = StateCasting.instance.activeDevice ?: return@subscribe
|
||||||
if (!ad.canSetSpeed) {
|
if (!ad.canSetSpeed) {
|
||||||
return@subscribe
|
return@subscribe
|
||||||
}
|
}
|
||||||
|
|
||||||
ad.changeSpeed(v.toDouble())
|
qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
|
||||||
setSelected(v);
|
ad.changeSpeed(newPlaybackSpeed)
|
||||||
|
setSelected(playbackSpeedString);
|
||||||
} else {
|
} else {
|
||||||
_player.setPlaybackRate(v.toFloat());
|
qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
|
||||||
setSelected(v);
|
_player.setPlaybackRate(playbackSpeedString.toFloat());
|
||||||
|
setSelected(playbackSpeedString);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else null,
|
} else null,
|
||||||
@@ -2522,7 +2576,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveBrightness() {
|
fun saveBrightness() {
|
||||||
_player.gestureControl.saveBrightness()
|
if (Settings.instance.gestureControls.useSystemBrightness) {
|
||||||
|
_player.gestureControl.saveBrightness()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fun restoreBrightness() {
|
fun restoreBrightness() {
|
||||||
_player.gestureControl.restoreBrightness()
|
_player.gestureControl.restoreBrightness()
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ class Playlist {
|
|||||||
this.videos = ArrayList(list);
|
this.videos = ArrayList(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun makeCopy(newName: String? = null): Playlist {
|
||||||
|
return Playlist(newName ?: name, videos)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(config: SourcePluginConfig, obj: V8ValueObject?): Playlist? {
|
fun fromV8(config: SourcePluginConfig, obj: V8ValueObject?): Playlist? {
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ import android.text.method.LinkMovementMethod
|
|||||||
import android.text.style.URLSpan
|
import android.text.style.URLSpan
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||||
import com.futo.platformplayer.timestampRegex
|
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() {
|
class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMethod() {
|
||||||
|
|
||||||
@@ -60,31 +63,39 @@ class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMe
|
|||||||
val dx = event.x - downX
|
val dx = event.x - downX
|
||||||
val dy = event.y - downY
|
val dy = event.y - downY
|
||||||
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(widget, event)) {
|
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(widget, event)) {
|
||||||
runBlocking {
|
for (link in pressedLinks!!) {
|
||||||
for (link in pressedLinks!!) {
|
Logger.i(TAG) { "Link clicked '${link.url}'." }
|
||||||
Logger.i(TAG) { "Link clicked '${link.url}'." }
|
|
||||||
|
|
||||||
if (_context is MainActivity) {
|
val c = _context
|
||||||
if (_context.handleUrl(link.url)) continue
|
if (c is MainActivity) {
|
||||||
if (timestampRegex.matches(link.url)) {
|
c.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val tokens = link.url.split(':')
|
if (c.handleUrl(link.url)) {
|
||||||
var time_s = -1L
|
return@launch
|
||||||
when (tokens.size) {
|
}
|
||||||
2 -> time_s = tokens[0].toLong() * 60 + tokens[1].toLong()
|
if (timestampRegex.matches(link.url)) {
|
||||||
3 -> time_s = tokens[0].toLong() * 3600 +
|
val tokens = link.url.split(':')
|
||||||
tokens[1].toLong() * 60 +
|
var time_s = -1L
|
||||||
tokens[2].toLong()
|
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)
|
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
|
pressedLinks = null
|
||||||
linkPressed = false
|
linkPressed = false
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -156,6 +156,8 @@ class StateApp {
|
|||||||
return thisContext;
|
return thisContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var _mainId: String? = null;
|
||||||
|
|
||||||
//Files
|
//Files
|
||||||
private var _tempDirectory: File? = null;
|
private var _tempDirectory: File? = null;
|
||||||
private var _cacheDirectory: File? = null;
|
private var _cacheDirectory: File? = null;
|
||||||
@@ -295,9 +297,12 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Lifecycle
|
//Lifecycle
|
||||||
fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null) {
|
fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null, mainId: String? = null) {
|
||||||
|
_mainId = mainId;
|
||||||
_context = context;
|
_context = context;
|
||||||
_scope = coroutineScope
|
_scope = coroutineScope
|
||||||
|
Logger.w(TAG, "Scope initialized ${(coroutineScope != null)}\n ${Log.getStackTraceString(Throwable())}")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initializeFiles(force: Boolean = false) {
|
fun initializeFiles(force: Boolean = false) {
|
||||||
@@ -719,7 +724,9 @@ class StateApp {
|
|||||||
migrateStores(context, managedStores, index + 1);
|
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");
|
Logger.i(TAG, "App ended");
|
||||||
_receiverBecomingNoisy?.let {
|
_receiverBecomingNoisy?.let {
|
||||||
_receiverBecomingNoisy = null;
|
_receiverBecomingNoisy = null;
|
||||||
@@ -743,7 +750,8 @@ class StateApp {
|
|||||||
|
|
||||||
fun dispose(){
|
fun dispose(){
|
||||||
_context = null;
|
_context = null;
|
||||||
_scope = null;
|
// _scope = null;
|
||||||
|
Logger.w(TAG, "StateApp disposed: ${Log.getStackTraceString(Throwable())}")
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _connectivityEvents = object : ConnectivityManager.NetworkCallback() {
|
private val _connectivityEvents = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
|||||||
@@ -31,10 +31,11 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
|||||||
updateDataset();
|
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;
|
_inflater = inflater;
|
||||||
_confirmationMessage = confirmationMessage;
|
_confirmationMessage = confirmationMessage;
|
||||||
_onDatasetChanged = onDatasetChanged;
|
_onDatasetChanged = onDatasetChanged;
|
||||||
|
sortBy = sortByDefault
|
||||||
|
|
||||||
StateSubscriptions.instance.onSubscriptionsChanged.subscribe { _, _ -> if(Looper.myLooper() != Looper.getMainLooper())
|
StateSubscriptions.instance.onSubscriptionsChanged.subscribe { _, _ -> if(Looper.myLooper() != Looper.getMainLooper())
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { updateDataset() }
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { updateDataset() }
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ class GestureControlView : LinearLayout {
|
|||||||
private var _surfaceView: View? = null
|
private var _surfaceView: View? = null
|
||||||
private var _layoutIndicatorFill: FrameLayout;
|
private var _layoutIndicatorFill: FrameLayout;
|
||||||
private var _layoutIndicatorFit: FrameLayout;
|
private var _layoutIndicatorFit: FrameLayout;
|
||||||
|
private var _speedHolding = false
|
||||||
|
|
||||||
private val _gestureController: GestureDetectorCompat;
|
private val _gestureController: GestureDetectorCompat;
|
||||||
|
|
||||||
@@ -103,6 +104,8 @@ class GestureControlView : LinearLayout {
|
|||||||
val onZoom = Event1<Float>();
|
val onZoom = Event1<Float>();
|
||||||
val onSoundAdjusted = Event1<Float>();
|
val onSoundAdjusted = Event1<Float>();
|
||||||
val onToggleFullscreen = Event0();
|
val onToggleFullscreen = Event0();
|
||||||
|
val onSpeedHoldStart = Event0()
|
||||||
|
val onSpeedHoldEnd = Event0()
|
||||||
|
|
||||||
var fullScreenGestureEnabled = true
|
var fullScreenGestureEnabled = true
|
||||||
|
|
||||||
@@ -216,7 +219,19 @@ class GestureControlView : LinearLayout {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
override fun onLongPress(p0: MotionEvent) = Unit
|
override fun onLongPress(p0: MotionEvent) {
|
||||||
|
if (!_isControlsLocked
|
||||||
|
&& !_skipping
|
||||||
|
&& !_adjustingBrightness
|
||||||
|
&& !_adjustingSound
|
||||||
|
&& !_adjustingFullscreenUp
|
||||||
|
&& !_adjustingFullscreenDown
|
||||||
|
&& !_isPanning
|
||||||
|
&& !_isZooming) {
|
||||||
|
_speedHolding = true
|
||||||
|
onSpeedHoldStart.emit()
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
|
_gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
|
||||||
@@ -309,6 +324,11 @@ class GestureControlView : LinearLayout {
|
|||||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||||
val ev = event ?: return super.onTouchEvent(event);
|
val ev = event ?: return super.onTouchEvent(event);
|
||||||
|
|
||||||
|
if (ev.action == MotionEvent.ACTION_UP && _speedHolding) {
|
||||||
|
_speedHolding = false
|
||||||
|
onSpeedHoldEnd.emit()
|
||||||
|
}
|
||||||
|
|
||||||
cancelHideJob();
|
cancelHideJob();
|
||||||
|
|
||||||
if (_skipping) {
|
if (_skipping) {
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ import android.text.Spannable
|
|||||||
import android.text.style.URLSpan
|
import android.text.style.URLSpan
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
||||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.timestampRegex
|
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 {
|
class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
||||||
private var _lastTouchedLinks: Array<URLSpan>? = null
|
private var _lastTouchedLinks: Array<URLSpan>? = null
|
||||||
@@ -77,12 +81,14 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
|||||||
val dx = event.x - downX
|
val dx = event.x - downX
|
||||||
val dy = event.y - downY
|
val dy = event.y - downY
|
||||||
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(event)) {
|
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(event)) {
|
||||||
runBlocking {
|
for (link in _lastTouchedLinks!!) {
|
||||||
for (link in _lastTouchedLinks!!) {
|
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." }
|
||||||
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." }
|
val c = context
|
||||||
val c = context
|
if (c is MainActivity) {
|
||||||
if (c is MainActivity) {
|
c.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
if (c.handleUrl(link.url)) continue
|
if (c.handleUrl(link.url)) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
if (timestampRegex.matches(link.url)) {
|
if (timestampRegex.matches(link.url)) {
|
||||||
val tokens = link.url.split(':')
|
val tokens = link.url.split(':')
|
||||||
var time_s = -1L
|
var time_s = -1L
|
||||||
@@ -92,13 +98,21 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
|||||||
tokens[1].toLong() * 60 +
|
tokens[1].toLong() * 60 +
|
||||||
tokens[2].toLong()
|
tokens[2].toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (time_s != -1L) {
|
if (time_s != -1L) {
|
||||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000)
|
withContext(Dispatchers.Main) {
|
||||||
continue
|
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)))
|
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ class ToggleField : TableRow, IField {
|
|||||||
|
|
||||||
val advancedFieldAttr = field.getAnnotation(AdvancedField::class.java)
|
val advancedFieldAttr = field.getAnnotation(AdvancedField::class.java)
|
||||||
if(advancedFieldAttr != null || advanced) {
|
if(advancedFieldAttr != null || advanced) {
|
||||||
Logger.w("ToggleField", "Found advanced field: " + field.name);
|
|
||||||
isAdvanced = true;
|
isAdvanced = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ class SlideUpMenuButtonList : LinearLayout {
|
|||||||
fun setButtons(texts: List<String>, activeText: String? = null) {
|
fun setButtons(texts: List<String>, activeText: String? = null) {
|
||||||
_root.removeAllViews();
|
_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;
|
val marginRight = marginLeft;
|
||||||
|
|
||||||
buttons.clear();
|
buttons.clear();
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
|
|
||||||
private var _isControlsLocked: Boolean = false;
|
private var _isControlsLocked: Boolean = false;
|
||||||
|
|
||||||
|
private var _speedHoldPrevRate = 1f
|
||||||
|
private var _speedHoldWasPlaying = false
|
||||||
|
|
||||||
private val _time_bar_listener: TimeBar.OnScrubListener;
|
private val _time_bar_listener: TimeBar.OnScrubListener;
|
||||||
|
|
||||||
var isFitMode : Boolean = false
|
var isFitMode : Boolean = false
|
||||||
@@ -254,6 +257,20 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
gestureControl = findViewById(R.id.gesture_control);
|
gestureControl = findViewById(R.id.gesture_control);
|
||||||
|
|
||||||
gestureControl.setupTouchArea(_layoutControls, background);
|
gestureControl.setupTouchArea(_layoutControls, background);
|
||||||
|
gestureControl.onSpeedHoldStart.subscribe {
|
||||||
|
exoPlayer?.player?.let { player ->
|
||||||
|
_speedHoldWasPlaying = player.isPlaying
|
||||||
|
_speedHoldPrevRate = getPlaybackRate()
|
||||||
|
setPlaybackRate(2f)
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gestureControl.onSpeedHoldEnd.subscribe {
|
||||||
|
exoPlayer?.player?.let { player ->
|
||||||
|
if (!_speedHoldWasPlaying) player.pause()
|
||||||
|
setPlaybackRate(_speedHoldPrevRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
gestureControl.onSeek.subscribe { seekFromCurrent(it); };
|
gestureControl.onSeek.subscribe { seekFromCurrent(it); };
|
||||||
gestureControl.onSoundAdjusted.subscribe {
|
gestureControl.onSoundAdjusted.subscribe {
|
||||||
if (Settings.instance.gestureControls.useSystemVolume) {
|
if (Settings.instance.gestureControls.useSystemVolume) {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
android:isScrollContainer="false"
|
android:isScrollContainer="false"
|
||||||
android:textColor="#CCCCCC"
|
android:textColor="#CCCCCC"
|
||||||
android:textSize="13sp"
|
android:textSize="13sp"
|
||||||
android:maxLines="100"
|
android:maxLines="150"
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_metadata"
|
app:layout_constraintTop_toBottomOf="@id/text_metadata"
|
||||||
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:id="@+id/root"
|
android:id="@+id/root"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingStart="6dp"
|
android:paddingStart="0dp"
|
||||||
android:paddingEnd="6dp">
|
android:paddingEnd="0dp">
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -76,6 +76,8 @@
|
|||||||
<string name="always_proxy_requests_description">Always proxy requests when casting data through the device.</string>
|
<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">Allow IPV6</string>
|
||||||
<string name="allow_ipv6_description">If casting over IPV6 is allowed, can cause issues on some networks</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="discover">Discover</string>
|
||||||
<string name="find_new_video_sources_to_add">Find new video sources to add</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>
|
<string name="these_sources_have_been_disabled">These sources have been disabled</string>
|
||||||
@@ -427,6 +429,12 @@
|
|||||||
<string name="delete_watchlist_on_finish">Delete from WatchLater when watched</string>
|
<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="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="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="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="seek_offset_description">Fast-Forward / Fast-Rewind duration</string>
|
||||||
<string name="background_switch_audio">Switch to Audio in Background</string>
|
<string name="background_switch_audio">Switch to Audio in Background</string>
|
||||||
<string name="subscription_group_menu">Groups</string>
|
<string name="subscription_group_menu">Groups</string>
|
||||||
@@ -1091,6 +1099,23 @@
|
|||||||
<item>30 seconds</item>
|
<item>30 seconds</item>
|
||||||
<item>60 seconds</item>
|
<item>60 seconds</item>
|
||||||
</string-array>
|
</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="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">
|
<string-array name="rotation_zone">
|
||||||
<item>15</item>
|
<item>15</item>
|
||||||
<item>30</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>
|
<application>
|
||||||
<receiver android:name=".receivers.InstallReceiver" />
|
<receiver android:name=".receivers.InstallReceiver" />
|
||||||
|
|
||||||
<activity android:name=".activities.MainActivity">
|
<activity android:name=".activities.MainActivity" android:launchMode="singleInstance">
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
<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