mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-29 19:13:01 +02:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 231d2461b3 | |||
| 3b457f87c4 | |||
| de3ced4d3c | |||
| 891777e89e | |||
| 287239dd1c | |||
| 7cdded8fd7 | |||
| 8c9d045e1d | |||
| 620f5a0459 | |||
| 178d874ba0 | |||
| d44f30c8a6 | |||
| ce66937429 | |||
| 9823337375 | |||
| 11f5f0dfe1 | |||
| e1882f19e8 | |||
| 6a8b9f06c2 | |||
| 752fc8787d | |||
| 90a1cd8280 | |||
| aa570ac29d | |||
| fb7b6363f9 | |||
| 23afe7994c | |||
| 7557e6f6ba | |||
| 86b6938911 | |||
| 8f30a45fa8 | |||
| 7c9e9d5f52 | |||
| 4066ce73a8 | |||
| b5722dba1a | |||
| 81765ecafc |
@@ -36,6 +36,12 @@
|
|||||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<receiver android:name=".receivers.MediaButtonReceiver" android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service android:name=".services.MediaPlaybackService"
|
<service android:name=".services.MediaPlaybackService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:foregroundServiceType="mediaPlayback" />
|
android:foregroundServiceType="mediaPlayback" />
|
||||||
@@ -52,7 +58,7 @@
|
|||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleInstance"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:supportsPictureInPicture="true">
|
android:supportsPictureInPicture="true">
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.futo.platformplayer.activities
|
package com.futo.platformplayer.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.media.AudioManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
@@ -72,6 +74,7 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
|||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.ImportCache
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||||
|
import com.futo.platformplayer.receivers.MediaButtonReceiver
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateBackup
|
import com.futo.platformplayer.states.StateBackup
|
||||||
@@ -107,6 +110,7 @@ import java.util.LinkedList
|
|||||||
import java.util.Queue
|
import java.util.Queue
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
|
|
||||||
//TODO: Move to dimensions
|
//TODO: Move to dimensions
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.graphics.Color
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
@@ -57,11 +58,21 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
_editComment = findViewById(R.id.edit_comment);
|
_editComment = findViewById(R.id.edit_comment);
|
||||||
_textCharacterCount = findViewById(R.id.character_count);
|
_textCharacterCount = findViewById(R.id.character_count);
|
||||||
_textCharacterCountMax = findViewById(R.id.character_count_max);
|
_textCharacterCountMax = findViewById(R.id.character_count_max);
|
||||||
|
setCanceledOnTouchOutside(false)
|
||||||
|
setOnKeyListener { _, keyCode, event ->
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
|
||||||
|
handleCloseAttempt()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_editComment.addTextChangedListener(object : TextWatcher {
|
_editComment.addTextChangedListener(object : TextWatcher {
|
||||||
override fun afterTextChanged(s: Editable?) = Unit
|
override fun afterTextChanged(s: Editable?) = Unit
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, c: Int) {
|
||||||
|
val count = s?.length ?: 0;
|
||||||
_textCharacterCount.text = count.toString();
|
_textCharacterCount.text = count.toString();
|
||||||
|
|
||||||
if (count > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
|
if (count > PolycentricPlatformComment.MAX_COMMENT_SIZE) {
|
||||||
@@ -79,10 +90,13 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
_inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager;
|
||||||
|
|
||||||
_buttonCancel.setOnClickListener {
|
_buttonCancel.setOnClickListener {
|
||||||
clearFocus();
|
handleCloseAttempt()
|
||||||
dismiss();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setOnCancelListener {
|
||||||
|
handleCloseAttempt()
|
||||||
|
}
|
||||||
|
|
||||||
_buttonCreate.setOnClickListener {
|
_buttonCreate.setOnClickListener {
|
||||||
clearFocus();
|
clearFocus();
|
||||||
|
|
||||||
@@ -134,6 +148,22 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
focus();
|
focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleCloseAttempt() {
|
||||||
|
if (_editComment.text.isEmpty()) {
|
||||||
|
clearFocus()
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
UIDialogs.showConfirmationDialog(
|
||||||
|
context,
|
||||||
|
context.resources.getString(R.string.not_empty_close),
|
||||||
|
action = {
|
||||||
|
clearFocus()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun focus() {
|
private fun focus() {
|
||||||
_editComment.requestFocus();
|
_editComment.requestFocus();
|
||||||
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
var requireVideoSource: Boolean = false;
|
var requireVideoSource: Boolean = false;
|
||||||
var requireAudioSource: Boolean = false;
|
var requireAudioSource: Boolean = false;
|
||||||
|
var requiredCheck: Boolean = false;
|
||||||
|
|
||||||
@Contextual
|
@Contextual
|
||||||
@Transient
|
@Transient
|
||||||
@@ -164,7 +165,7 @@ class VideoDownload {
|
|||||||
onStateChanged.emit(newState);
|
onStateChanged.emit(newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(video: IPlatformVideo, targetPixelCount: Long? = null, targetBitrate: Long? = null) {
|
constructor(video: IPlatformVideo, targetPixelCount: Long? = null, targetBitrate: Long? = null, optionalSources: Boolean = false) {
|
||||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||||
this.videoSource = null;
|
this.videoSource = null;
|
||||||
this.audioSource = null;
|
this.audioSource = null;
|
||||||
@@ -175,8 +176,9 @@ class VideoDownload {
|
|||||||
this.requiresLiveVideoSource = false;
|
this.requiresLiveVideoSource = false;
|
||||||
this.requiresLiveAudioSource = false;
|
this.requiresLiveAudioSource = false;
|
||||||
this.targetVideoName = videoSource?.name;
|
this.targetVideoName = videoSource?.name;
|
||||||
this.requireVideoSource = targetPixelCount != null
|
this.requireVideoSource = targetPixelCount != null;
|
||||||
this.requireAudioSource = targetBitrate != null; //TODO: May not be a valid check.. can only be determined after live fetch?
|
this.requireAudioSource = targetBitrate != null; //TODO: May not be a valid check.. can only be determined after live fetch?
|
||||||
|
this.requiredCheck = optionalSources;
|
||||||
}
|
}
|
||||||
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
||||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||||
@@ -250,6 +252,30 @@ class VideoDownload {
|
|||||||
if(original !is IPlatformVideoDetails)
|
if(original !is IPlatformVideoDetails)
|
||||||
throw IllegalStateException("Original content is not media?");
|
throw IllegalStateException("Original content is not media?");
|
||||||
|
|
||||||
|
if(requiredCheck) {
|
||||||
|
if(original.video is VideoUnMuxedSourceDescriptor) {
|
||||||
|
if(requireVideoSource) {
|
||||||
|
if((original.video as VideoUnMuxedSourceDescriptor).audioSources.any() && !original.video.videoSources.any()) {
|
||||||
|
requireVideoSource = false;
|
||||||
|
targetPixelCount = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(requireAudioSource) {
|
||||||
|
if(!(original.video as VideoUnMuxedSourceDescriptor).audioSources.any() && original.video.videoSources.any()) {
|
||||||
|
requireAudioSource = false;
|
||||||
|
targetBitrate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(requireAudioSource) {
|
||||||
|
requireAudioSource = false;
|
||||||
|
targetBitrate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requiredCheck = false;
|
||||||
|
}
|
||||||
|
|
||||||
if(original.video.hasAnySource() && !original.isDownloadable()) {
|
if(original.video.hasAnySource() && !original.isDownloadable()) {
|
||||||
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
|
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
|
||||||
throw DownloadException("Unsupported video for downloading", false);
|
throw DownloadException("Unsupported video for downloading", false);
|
||||||
|
|||||||
+15
-6
@@ -1,12 +1,13 @@
|
|||||||
package com.futo.platformplayer.fragment.channel.tab
|
package com.futo.platformplayer.fragment.channel.tab
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
@@ -15,7 +16,6 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSPager
|
||||||
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
@@ -41,10 +41,11 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
|||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||||
private var _recyclerResults: RecyclerView? = null;
|
private var _recyclerResults: RecyclerView? = null;
|
||||||
private var _llmVideo: LinearLayoutManager? = null;
|
private var _glmVideo: GridLayoutManager? = null;
|
||||||
private var _loading = false;
|
private var _loading = false;
|
||||||
private var _pager_parent: IPager<IPlatformContent>? = null;
|
private var _pager_parent: IPager<IPlatformContent>? = null;
|
||||||
private var _pager: IPager<IPlatformContent>? = null;
|
private var _pager: IPager<IPlatformContent>? = null;
|
||||||
@@ -118,7 +119,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
super.onScrolled(recyclerView, dx, dy);
|
super.onScrolled(recyclerView, dx, dy);
|
||||||
|
|
||||||
val recyclerResults = _recyclerResults ?: return;
|
val recyclerResults = _recyclerResults ?: return;
|
||||||
val llmVideo = _llmVideo ?: return;
|
val llmVideo = _glmVideo ?: return;
|
||||||
|
|
||||||
val visibleItemCount = recyclerResults.childCount;
|
val visibleItemCount = recyclerResults.childCount;
|
||||||
val firstVisibleItem = llmVideo.findFirstVisibleItemPosition();
|
val firstVisibleItem = llmVideo.findFirstVisibleItemPosition();
|
||||||
@@ -163,9 +164,10 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
||||||
}
|
}
|
||||||
|
|
||||||
_llmVideo = LinearLayoutManager(view.context);
|
val numColumns = max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
_glmVideo = GridLayoutManager(view.context, numColumns);
|
||||||
_recyclerResults?.adapter = _adapterResults;
|
_recyclerResults?.adapter = _adapterResults;
|
||||||
_recyclerResults?.layoutManager = _llmVideo;
|
_recyclerResults?.layoutManager = _glmVideo;
|
||||||
_recyclerResults?.addOnScrollListener(_scrollListener);
|
_recyclerResults?.addOnScrollListener(_scrollListener);
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
@@ -181,6 +183,13 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
_nextPageHandler.cancel();
|
_nextPageHandler.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
_glmVideo?.spanCount =
|
||||||
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
private fun setPager(pager: IPager<IPlatformContent>, cache: FeedFragment.ItemCache<IPlatformContent>? = null) {
|
private fun setPager(pager: IPager<IPlatformContent>, cache: FeedFragment.ItemCache<IPlatformContent>? = null) {
|
||||||
if (_pager_parent != null && _pager_parent is IRefreshPager<*>) {
|
if (_pager_parent != null && _pager_parent is IRefreshPager<*>) {
|
||||||
|
|||||||
+15
-5
@@ -1,12 +1,13 @@
|
|||||||
package com.futo.platformplayer.fragment.channel.tab
|
package com.futo.platformplayer.fragment.channel.tab
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
@@ -36,10 +37,11 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
|||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
||||||
private var _recyclerResults: RecyclerView? = null
|
private var _recyclerResults: RecyclerView? = null
|
||||||
private var _llmPlaylist: LinearLayoutManager? = null
|
private var _glmPlaylist: GridLayoutManager? = null
|
||||||
private var _loading = false
|
private var _loading = false
|
||||||
private var _pagerParent: IPager<IPlatformPlaylist>? = null
|
private var _pagerParent: IPager<IPlatformPlaylist>? = null
|
||||||
private var _pager: IPager<IPlatformPlaylist>? = null
|
private var _pager: IPager<IPlatformPlaylist>? = null
|
||||||
@@ -109,7 +111,7 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
|||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
|
||||||
val recyclerResults = _recyclerResults ?: return
|
val recyclerResults = _recyclerResults ?: return
|
||||||
val llmPlaylist = _llmPlaylist ?: return
|
val llmPlaylist = _glmPlaylist ?: return
|
||||||
|
|
||||||
val visibleItemCount = recyclerResults.childCount
|
val visibleItemCount = recyclerResults.childCount
|
||||||
val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition()
|
val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition()
|
||||||
@@ -158,9 +160,10 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
|||||||
this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit)
|
this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit)
|
||||||
}
|
}
|
||||||
|
|
||||||
_llmPlaylist = LinearLayoutManager(view.context)
|
val numColumns = max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
_glmPlaylist = GridLayoutManager(view.context, numColumns)
|
||||||
_recyclerResults?.adapter = _adapterResults
|
_recyclerResults?.adapter = _adapterResults
|
||||||
_recyclerResults?.layoutManager = _llmPlaylist
|
_recyclerResults?.layoutManager = _glmPlaylist
|
||||||
_recyclerResults?.addOnScrollListener(_scrollListener)
|
_recyclerResults?.addOnScrollListener(_scrollListener)
|
||||||
|
|
||||||
return view
|
return view
|
||||||
@@ -176,6 +179,13 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
|
|||||||
_nextPageHandler.cancel()
|
_nextPageHandler.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
_glmPlaylist?.spanCount =
|
||||||
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
private fun setPager(
|
private fun setPager(
|
||||||
pager: IPager<IPlatformPlaylist>
|
pager: IPager<IPlatformPlaylist>
|
||||||
) {
|
) {
|
||||||
|
|||||||
+2
-1
@@ -33,6 +33,7 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
|||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
import com.futo.platformplayer.withTimestamp
|
import com.futo.platformplayer.withTimestamp
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
|
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
|
||||||
private var _exoPlayer: PlayerManager? = null;
|
private var _exoPlayer: PlayerManager? = null;
|
||||||
@@ -168,7 +169,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
val glmResults =
|
val glmResults =
|
||||||
GridLayoutManager(
|
GridLayoutManager(
|
||||||
context,
|
context,
|
||||||
(resources.configuration.screenWidthDp / resources.getDimension(R.dimen.landscape_threshold)).toInt() + 1
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
);
|
);
|
||||||
return glmResults
|
return glmResults
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-2
@@ -8,6 +8,7 @@ import android.widget.AdapterView
|
|||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@@ -25,11 +26,20 @@ class CreatorsFragment : MainFragment() {
|
|||||||
private var _overlayContainer: FrameLayout? = null;
|
private var _overlayContainer: FrameLayout? = null;
|
||||||
private var _containerSearch: FrameLayout? = null;
|
private var _containerSearch: FrameLayout? = null;
|
||||||
private var _editSearch: EditText? = null;
|
private var _editSearch: EditText? = null;
|
||||||
|
private var _buttonClearSearch: ImageButton? = null
|
||||||
|
|
||||||
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);
|
||||||
_containerSearch = view.findViewById(R.id.container_search);
|
_containerSearch = view.findViewById(R.id.container_search);
|
||||||
_editSearch = view.findViewById(R.id.edit_search);
|
val editSearch: EditText = view.findViewById(R.id.edit_search);
|
||||||
|
val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search)
|
||||||
|
_editSearch = editSearch
|
||||||
|
_buttonClearSearch = buttonClearSearch
|
||||||
|
buttonClearSearch.setOnClickListener {
|
||||||
|
editSearch.text.clear()
|
||||||
|
editSearch.requestFocus()
|
||||||
|
_buttonClearSearch?.visibility = View.INVISIBLE;
|
||||||
|
}
|
||||||
|
|
||||||
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
|
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
|
||||||
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
|
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
|
||||||
@@ -51,7 +61,12 @@ class CreatorsFragment : MainFragment() {
|
|||||||
_spinnerSortBy = spinnerSortBy;
|
_spinnerSortBy = spinnerSortBy;
|
||||||
|
|
||||||
_editSearch?.addTextChangedListener {
|
_editSearch?.addTextChangedListener {
|
||||||
adapter.query = it.toString();
|
adapter.query = it.toString()
|
||||||
|
if (it?.isEmpty() == true) {
|
||||||
|
_buttonClearSearch?.visibility = View.INVISIBLE
|
||||||
|
} else {
|
||||||
|
_buttonClearSearch?.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_subscriptions);
|
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_subscriptions);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import kotlinx.coroutines.CancellationException
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment {
|
abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : LinearLayout where TPager : IPager<TResult>, TViewHolder : RecyclerView.ViewHolder, TFragment : MainFragment {
|
||||||
protected val _recyclerResults: RecyclerView;
|
protected val _recyclerResults: RecyclerView;
|
||||||
@@ -234,7 +235,8 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
}
|
}
|
||||||
|
|
||||||
open fun updateSpanCount() {
|
open fun updateSpanCount() {
|
||||||
recyclerData.layoutManager.spanCount = (resources.configuration.screenWidthDp / resources.getDimension(R.dimen.landscape_threshold)).toInt() + 1
|
recyclerData.layoutManager.spanCount =
|
||||||
|
max((resources.configuration.screenWidthDp.toDouble() / resources.getInteger(R.integer.column_width_dp)).toInt(), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||||
|
|||||||
+1
-1
@@ -91,7 +91,7 @@ class VideoDetailFragment : MainFragment {
|
|||||||
return min(
|
return min(
|
||||||
resources.configuration.screenWidthDp,
|
resources.configuration.screenWidthDp,
|
||||||
resources.configuration.screenHeightDp
|
resources.configuration.screenHeightDp
|
||||||
) < resources.getDimension(R.dimen.landscape_threshold)
|
) < resources.getInteger(R.integer.column_width_dp) * 2
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
|||||||
+11
-9
@@ -1799,8 +1799,13 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean){
|
private fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean){
|
||||||
Logger.i(TAG, "onSourceChanged(videoSource=$videoSource, audioSource=$audioSource, resume=$resume)")
|
Logger.i(TAG, "onSourceChanged(videoSource=$videoSource, audioSource=$audioSource, resume=$resume)")
|
||||||
|
|
||||||
if((videoSource == null || videoSource is LocalVideoSource) && (audioSource == null || audioSource is LocalAudioSource))
|
if((videoSource == null || videoSource is LocalVideoSource) && (audioSource == null || audioSource is LocalAudioSource)) {
|
||||||
UIDialogs.toast(context, context.getString(R.string.offline_playback), false);
|
Logger.i(TAG, "Time since last offline playback toast: " + (System.currentTimeMillis() - _lastOfflinePlaybackToastTime).toString())
|
||||||
|
if (System.currentTimeMillis() - _lastOfflinePlaybackToastTime > 5000) {
|
||||||
|
UIDialogs.toast(context, context.getString(R.string.offline_playback), false);
|
||||||
|
_lastOfflinePlaybackToastTime = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
//If LiveStream, set to end
|
//If LiveStream, set to end
|
||||||
if(videoSource is IDashManifestSource || videoSource is IHLSManifestSource) {
|
if(videoSource is IDashManifestSource || videoSource is IHLSManifestSource) {
|
||||||
if (video?.isLive == true) {
|
if (video?.isLive == true) {
|
||||||
@@ -2379,8 +2384,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isLandscapeVideo(): Boolean? {
|
fun isLandscapeVideo(): Boolean? {
|
||||||
var videoSourceWidth = _player.exoPlayer?.player?.videoSize?.width
|
val videoSourceWidth = _player.exoPlayer?.player?.videoSize?.width
|
||||||
var videoSourceHeight = _player.exoPlayer?.player?.videoSize?.height
|
val videoSourceHeight = _player.exoPlayer?.player?.videoSize?.height
|
||||||
|
|
||||||
return if (videoSourceWidth == null || videoSourceHeight == null || videoSourceWidth == 0 || videoSourceHeight == 0){
|
return if (videoSourceWidth == null || videoSourceHeight == null || videoSourceWidth == 0 || videoSourceHeight == 0){
|
||||||
null
|
null
|
||||||
@@ -2582,7 +2587,6 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_overlayContainer.removeAllViews();
|
_overlayContainer.removeAllViews();
|
||||||
_overlay_quality_selector?.hide();
|
_overlay_quality_selector?.hide();
|
||||||
|
|
||||||
_player.setFullScreen(true)
|
|
||||||
_player.fillHeight(false)
|
_player.fillHeight(false)
|
||||||
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
|
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
@@ -2797,7 +2801,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
if (fragment.state == VideoDetailFragment.State.MINIMIZED) {
|
if (fragment.state == VideoDetailFragment.State.MINIMIZED) {
|
||||||
_player.fillHeight(true)
|
_player.fillHeight(true)
|
||||||
} else if (!fragment.isFullscreen) {
|
} else if (!fragment.isFullscreen && !fragment.isInPictureInPicture) {
|
||||||
_player.fitHeight()
|
_player.fitHeight()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3030,8 +3034,6 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
const val TAG_MORE = "MORE";
|
const val TAG_MORE = "MORE";
|
||||||
|
|
||||||
private val _buttonPinStore = FragmentedStorage.get<StringArrayStorage>("videoPinnedButtons");
|
private val _buttonPinStore = FragmentedStorage.get<StringArrayStorage>("videoPinnedButtons");
|
||||||
|
private var _lastOfflinePlaybackToastTime: Long = 0
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,70 +12,109 @@ 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 com.futo.platformplayer.views.behavior.NonScrollingTextView
|
|
||||||
import com.futo.platformplayer.views.behavior.NonScrollingTextView.Companion
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class PlatformLinkMovementMethod : LinkMovementMethod {
|
class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMethod() {
|
||||||
private val _context: Context;
|
|
||||||
|
|
||||||
constructor(context: Context) : super() {
|
private var pressedLinks: Array<URLSpan>? = null
|
||||||
_context = context;
|
private var linkPressed = false
|
||||||
}
|
private var downX = 0f
|
||||||
|
private var downY = 0f
|
||||||
|
private val touchSlop = 20
|
||||||
|
|
||||||
override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean {
|
override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean {
|
||||||
val action = event.action;
|
val action = event.actionMasked
|
||||||
Logger.i(TAG, "onTouchEvent (action = $action)")
|
|
||||||
if (action == MotionEvent.ACTION_UP) {
|
|
||||||
val x = event.x.toInt() - widget.totalPaddingLeft + widget.scrollX;
|
|
||||||
val y = event.y.toInt() - widget.totalPaddingTop + widget.scrollY;
|
|
||||||
|
|
||||||
val layout = widget.layout;
|
when (action) {
|
||||||
val line = layout.getLineForVertical(y);
|
MotionEvent.ACTION_DOWN -> {
|
||||||
val off = layout.getOffsetForHorizontal(line, x.toFloat());
|
val links = findLinksAtTouchPosition(widget, buffer, event)
|
||||||
val links = buffer.getSpans(off, off, URLSpan::class.java);
|
if (links.isNotEmpty()) {
|
||||||
|
pressedLinks = links
|
||||||
|
linkPressed = true
|
||||||
|
downX = event.x
|
||||||
|
downY = event.y
|
||||||
|
widget.parent?.requestDisallowInterceptTouchEvent(true)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
linkPressed = false
|
||||||
|
pressedLinks = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (links.isNotEmpty()) {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
runBlocking {
|
if (linkPressed) {
|
||||||
for (link in links) {
|
val dx = event.x - downX
|
||||||
Logger.i(TAG) { "Link clicked '${link.url}'." };
|
val dy = event.y - downY
|
||||||
|
if (Math.abs(dx) > touchSlop || Math.abs(dy) > touchSlop) {
|
||||||
|
linkPressed = false
|
||||||
|
pressedLinks = null
|
||||||
|
widget.parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_context is MainActivity) {
|
MotionEvent.ACTION_UP -> {
|
||||||
if (_context.handleUrl(link.url)) {
|
if (linkPressed && pressedLinks != null) {
|
||||||
continue;
|
val dx = event.x - downX
|
||||||
}
|
val dy = event.y - downY
|
||||||
|
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop) {
|
||||||
|
runBlocking {
|
||||||
|
for (link in pressedLinks!!) {
|
||||||
|
Logger.i(TAG) { "Link clicked '${link.url}'." }
|
||||||
|
|
||||||
if (timestampRegex.matches(link.url)) {
|
if (_context is MainActivity) {
|
||||||
val tokens = link.url.split(':');
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
var time_s = -1L;
|
if (time_s != -1L) {
|
||||||
if (tokens.size == 2) {
|
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000)
|
||||||
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
continue
|
||||||
} else if (tokens.size == 3) {
|
}
|
||||||
time_s =
|
}
|
||||||
tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (time_s != -1L) {
|
|
||||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pressedLinks = null
|
||||||
|
linkPressed = false
|
||||||
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
return true
|
||||||
|
} else {
|
||||||
|
pressedLinks = null
|
||||||
|
linkPressed = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
MotionEvent.ACTION_CANCEL -> {
|
||||||
|
linkPressed = false
|
||||||
|
pressedLinks = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onTouchEvent(widget, buffer, event);
|
return super.onTouchEvent(widget, buffer, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findLinksAtTouchPosition(widget: TextView, buffer: Spannable, event: MotionEvent): Array<URLSpan> {
|
||||||
|
val x = (event.x - widget.totalPaddingLeft + widget.scrollX).toInt()
|
||||||
|
val y = (event.y - widget.totalPaddingTop + widget.scrollY).toInt()
|
||||||
|
|
||||||
|
val layout = widget.layout ?: return emptyArray()
|
||||||
|
val line = layout.getLineForVertical(y)
|
||||||
|
val off = layout.getOffsetForHorizontal(line, x.toFloat())
|
||||||
|
return buffer.getSpans(off, off, URLSpan::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "PlatformLinkMovementMethod";
|
const val TAG = "PlatformLinkMovementMethod"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.futo.platformplayer.receivers
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
|
||||||
|
|
||||||
|
class MediaButtonReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
val keyEvent: KeyEvent? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
intent?.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
(intent?.getParcelableExtra(Intent.EXTRA_KEY_EVENT))
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "Received media button intent, keyCode: " + keyEvent?.keyCode)
|
||||||
|
if (keyEvent != null && keyEvent.action == KeyEvent.ACTION_DOWN) {
|
||||||
|
when (keyEvent.keyCode) {
|
||||||
|
KeyEvent.KEYCODE_MEDIA_PLAY -> MediaControlReceiver.onPlayReceived.emit()
|
||||||
|
KeyEvent.KEYCODE_MEDIA_PAUSE -> MediaControlReceiver.onPauseReceived.emit()
|
||||||
|
KeyEvent.KEYCODE_MEDIA_NEXT -> MediaControlReceiver.onNextReceived.emit()
|
||||||
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> MediaControlReceiver.onPreviousReceived.emit()
|
||||||
|
KeyEvent.KEYCODE_MEDIA_STOP -> MediaControlReceiver.onCloseReceived.emit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "MediaButtonReceiver"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import android.support.v4.media.MediaMetadataCompat
|
|||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.KeyEvent
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.request.target.CustomTarget
|
import com.bumptech.glide.request.target.CustomTarget
|
||||||
@@ -32,6 +33,7 @@ import com.futo.platformplayer.Settings
|
|||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.receivers.MediaButtonReceiver
|
||||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
@@ -91,6 +93,7 @@ class MediaPlaybackService : Service() {
|
|||||||
|
|
||||||
return START_STICKY;
|
return START_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setupNotificationRequirements() {
|
fun setupNotificationRequirements() {
|
||||||
_audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager;
|
_audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager;
|
||||||
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||||
@@ -101,6 +104,7 @@ class MediaPlaybackService : Service() {
|
|||||||
_notificationManager!!.createNotificationChannel(_notificationChannel!!);
|
_notificationManager!!.createNotificationChannel(_notificationChannel!!);
|
||||||
|
|
||||||
_mediaSession = MediaSessionCompat(this, "PlayerState");
|
_mediaSession = MediaSessionCompat(this, "PlayerState");
|
||||||
|
_mediaSession?.isActive = true
|
||||||
_mediaSession?.setPlaybackState(PlaybackStateCompat.Builder()
|
_mediaSession?.setPlaybackState(PlaybackStateCompat.Builder()
|
||||||
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 1f)
|
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 1f)
|
||||||
.build());
|
.build());
|
||||||
@@ -143,6 +147,12 @@ class MediaPlaybackService : Service() {
|
|||||||
MediaControlReceiver.onNextReceived.emit();
|
MediaControlReceiver.onNextReceived.emit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
_mediaSession?.setMediaButtonReceiver(PendingIntent.getBroadcast(
|
||||||
|
this@MediaPlaybackService,
|
||||||
|
0,
|
||||||
|
Intent(Intent.ACTION_MEDIA_BUTTON).setClass(this@MediaPlaybackService, MediaButtonReceiver::class.java),
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ class StateDownloads {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Logger.i(TAG, "New watchlater video ${item.name}");
|
Logger.i(TAG, "New watchlater video ${item.name}");
|
||||||
download(VideoDownload(item, playlistDownload.targetPxCount, playlistDownload.targetBitrate)
|
download(VideoDownload(item, playlistDownload.targetPxCount, playlistDownload.targetBitrate, true)
|
||||||
.withGroup(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER), false);
|
.withGroup(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER), false);
|
||||||
hasNew = true;
|
hasNew = true;
|
||||||
}
|
}
|
||||||
@@ -296,7 +296,7 @@ class StateDownloads {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Logger.i(TAG, "New playlist video ${item.name}");
|
Logger.i(TAG, "New playlist video ${item.name}");
|
||||||
download(VideoDownload(item, playlistDownload.targetPxCount, playlistDownload.targetBitrate)
|
download(VideoDownload(item, playlistDownload.targetPxCount, playlistDownload.targetBitrate, true)
|
||||||
.withGroup(VideoDownload.GROUP_PLAYLIST, playlist.id), false);
|
.withGroup(VideoDownload.GROUP_PLAYLIST, playlist.id), false);
|
||||||
hasNew = true;
|
hasNew = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,18 +112,23 @@ class StateSync {
|
|||||||
Logger.i(TAG, "Sync key pair initialized (public key = ${publicKey})")
|
Logger.i(TAG, "Sync key pair initialized (public key = ${publicKey})")
|
||||||
|
|
||||||
_thread = Thread {
|
_thread = Thread {
|
||||||
val serverSocket = ServerSocket(PORT)
|
try {
|
||||||
_serverSocket = serverSocket
|
val serverSocket = ServerSocket(PORT)
|
||||||
|
_serverSocket = serverSocket
|
||||||
|
|
||||||
Log.i(TAG, "Running on port ${PORT} (TCP)")
|
Log.i(TAG, "Running on port ${PORT} (TCP)")
|
||||||
|
|
||||||
while (_started) {
|
while (_started) {
|
||||||
val socket = serverSocket.accept()
|
val socket = serverSocket.accept()
|
||||||
val session = createSocketSession(socket, true) { session, socketSession ->
|
val session = createSocketSession(socket, true) { session, socketSession ->
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
session.startAsResponder()
|
||||||
}
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
session.startAsResponder()
|
Logger.e(TAG, "Failed to bind server socket to port ${PORT}", e)
|
||||||
|
UIDialogs.toast("Failed to start sync, port in use")
|
||||||
}
|
}
|
||||||
}.apply { start() }
|
}.apply { start() }
|
||||||
|
|
||||||
|
|||||||
@@ -16,73 +16,113 @@ import com.futo.platformplayer.timestampRegex
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
||||||
|
private var _lastTouchedLinks: Array<URLSpan>? = null
|
||||||
|
private var downX = 0f
|
||||||
|
private var downY = 0f
|
||||||
|
private var linkPressed = false
|
||||||
|
private val touchSlop = 20
|
||||||
|
|
||||||
constructor(context: Context) : super(context) {}
|
constructor(context: Context) : super(context) {}
|
||||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {}
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {}
|
||||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}
|
||||||
|
|
||||||
override fun scrollTo(x: Int, y: Int) {
|
override fun scrollTo(x: Int, y: Int) {
|
||||||
//do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||||
val action = event?.action
|
val action = event?.actionMasked
|
||||||
Logger.i(TAG, "onTouchEvent (action = $action)");
|
if (event == null) return super.onTouchEvent(event)
|
||||||
|
|
||||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
|
when (action) {
|
||||||
val x = event.x.toInt()
|
MotionEvent.ACTION_DOWN -> {
|
||||||
val y = event.y.toInt()
|
val x = event.x.toInt()
|
||||||
|
val y = event.y.toInt()
|
||||||
|
|
||||||
val layout: Layout? = this.layout
|
val layout: Layout? = this.layout
|
||||||
if (layout != null) {
|
if (layout != null && this.text is Spannable) {
|
||||||
val line = layout.getLineForVertical(y)
|
val offset = layout.getOffsetForHorizontal(layout.getLineForVertical(y), x.toFloat())
|
||||||
val offset = layout.getOffsetForHorizontal(line, x.toFloat())
|
val text = this.text as Spannable
|
||||||
|
|
||||||
val text = this.text
|
|
||||||
if (text is Spannable) {
|
|
||||||
val links = text.getSpans(offset, offset, URLSpan::class.java)
|
val links = text.getSpans(offset, offset, URLSpan::class.java)
|
||||||
if (links.isNotEmpty()) {
|
if (links.isNotEmpty()) {
|
||||||
runBlocking {
|
parent?.requestDisallowInterceptTouchEvent(true)
|
||||||
for (link in links) {
|
_lastTouchedLinks = links
|
||||||
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." };
|
downX = event.x
|
||||||
|
downY = event.y
|
||||||
val c = context;
|
linkPressed = true
|
||||||
if (c is MainActivity) {
|
|
||||||
if (c.handleUrl(link.url)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timestampRegex.matches(link.url)) {
|
|
||||||
val tokens = link.url.split(':');
|
|
||||||
|
|
||||||
var time_s = -1L;
|
|
||||||
if (tokens.size == 2) {
|
|
||||||
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
|
||||||
} else if (tokens.size == 3) {
|
|
||||||
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (time_s != -1L) {
|
|
||||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
} else {
|
||||||
|
linkPressed = false
|
||||||
|
_lastTouchedLinks = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MotionEvent.ACTION_MOVE -> {
|
||||||
|
if (linkPressed) {
|
||||||
|
val dx = event.x - downX
|
||||||
|
val dy = event.y - downY
|
||||||
|
if (Math.abs(dx) > touchSlop || Math.abs(dy) > touchSlop) {
|
||||||
|
linkPressed = false
|
||||||
|
_lastTouchedLinks = null
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MotionEvent.ACTION_UP -> {
|
||||||
|
if (linkPressed && _lastTouchedLinks != null) {
|
||||||
|
val dx = event.x - downX
|
||||||
|
val dy = event.y - downY
|
||||||
|
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop) {
|
||||||
|
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
|
||||||
|
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) {
|
||||||
|
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
|
||||||
|
} else {
|
||||||
|
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_lastTouchedLinks = null
|
||||||
|
linkPressed = false
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
linkPressed = false
|
||||||
|
_lastTouchedLinks = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MotionEvent.ACTION_CANCEL -> {
|
||||||
|
linkPressed = false
|
||||||
|
_lastTouchedLinks = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onTouchEvent(event)
|
return super.onTouchEvent(event)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "NonScrollingTextView"
|
private const val TAG = "NonScrollingTextView"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.LazyComment
|
||||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
import com.futo.platformplayer.api.media.structures.IAsyncPager
|
||||||
@@ -267,9 +268,13 @@ class CommentsList : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun replaceComment(c: PolycentricPlatformComment, newComment: PolycentricPlatformComment) {
|
fun replaceComment(c: PolycentricPlatformComment, newComment: PolycentricPlatformComment) {
|
||||||
val index = _comments.indexOf(c);
|
val index = _comments.indexOfFirst { it == c || (it is LazyComment && it.getUnderlyingComment() == c) };
|
||||||
_comments[index] = newComment;
|
if (index >= 0) {
|
||||||
_adapterComments.notifyItemChanged(_adapterComments.childToParentPosition(index));
|
_comments[index] = newComment;
|
||||||
|
_adapterComments.notifyItemChanged(_adapterComments.childToParentPosition(index));
|
||||||
|
} else {
|
||||||
|
Logger.w(TAG, "Parent comment not found")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -592,6 +592,11 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
fun setFullScreen(fullScreen: Boolean) {
|
fun setFullScreen(fullScreen: Boolean) {
|
||||||
|
// prevent fullscreen before the video has loaded to make sure we know whether it's a vertical or horizontal video
|
||||||
|
if(exoPlayer?.player?.videoSize?.height ?: 0 == 0 && fullScreen){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
updateRotateLock()
|
updateRotateLock()
|
||||||
|
|
||||||
if (isFullScreen == fullScreen) {
|
if (isFullScreen == fullScreen) {
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<dimen name="minimized_player_max_width">500dp</dimen>
|
<dimen name="minimized_player_max_width">500dp</dimen>
|
||||||
<dimen name="app_bar_height">200dp</dimen>
|
<dimen name="app_bar_height">200dp</dimen>
|
||||||
<dimen name="landscape_threshold">300dp</dimen>
|
<integer name="column_width_dp">400</integer>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -199,6 +199,7 @@
|
|||||||
<string name="previous">Previous</string>
|
<string name="previous">Previous</string>
|
||||||
<string name="next">Next</string>
|
<string name="next">Next</string>
|
||||||
<string name="comment">Comment</string>
|
<string name="comment">Comment</string>
|
||||||
|
<string name="not_empty_close">Comment is not empty, close anyway?</string>
|
||||||
<string name="str_import">Import</string>
|
<string name="str_import">Import</string>
|
||||||
<string name="my_playlist_name">My Playlist Name</string>
|
<string name="my_playlist_name">My Playlist Name</string>
|
||||||
<string name="do_you_want_to_import_this_store">Do you want to import this store?</string>
|
<string name="do_you_want_to_import_this_store">Do you want to import this store?</string>
|
||||||
|
|||||||
Submodule app/src/stable/assets/sources/odysee updated: 77b9012590...8ddb2e2f15
Submodule app/src/stable/assets/sources/rumble updated: cbfe372bcc...6811ff4b41
Submodule app/src/stable/assets/sources/youtube updated: e1fa498059...59d694b619
Submodule app/src/unstable/assets/sources/odysee updated: 77b9012590...8ddb2e2f15
Submodule app/src/unstable/assets/sources/rumble updated: cbfe372bcc...6811ff4b41
Submodule app/src/unstable/assets/sources/youtube updated: e1fa498059...59d694b619
@@ -15,6 +15,7 @@ touch $DOCUMENT_ROOT/maintenance.file
|
|||||||
|
|
||||||
# Swap over the content
|
# Swap over the content
|
||||||
echo "Deploying content..."
|
echo "Deploying content..."
|
||||||
|
cp ./app/build/outputs/bundle/playstoreRelease/app-playstore-release.aab $DOCUMENT_ROOT/app-playstore-release.aab
|
||||||
aws s3 cp ./app/build/outputs/bundle/playstoreRelease/app-playstore-release.aab s3://artifacts-grayjay-app/app-playstore-release.aab
|
aws s3 cp ./app/build/outputs/bundle/playstoreRelease/app-playstore-release.aab s3://artifacts-grayjay-app/app-playstore-release.aab
|
||||||
|
|
||||||
# Notify Cloudflare to wipe the CDN cache
|
# Notify Cloudflare to wipe the CDN cache
|
||||||
|
|||||||
Reference in New Issue
Block a user