Compare commits

...

44 Commits

Author SHA1 Message Date
Kai DeLorenzo 2787e29a07 Merge branch 'delay-tweak' into 'master'
increase delay to prevent erroneous rotations

See merge request videostreaming/grayjay!58
2024-12-13 05:02:23 +00:00
Kai c77a4d08d6 increase delay to prevent erroneous rotations 2024-12-12 23:01:28 -06:00
Kai DeLorenzo 9b3f90f922 Merge branch 'auto-rotate-fixes' into 'master'
more rotation/orientation fixes

See merge request videostreaming/grayjay!57
2024-12-12 21:27:20 +00:00
Kai c88d457021 fix device getting stuck landscape or in portrait when entering and exiting full screen via the button
fix weird rotation behavior when locking and unlocking rotation via the player button
2024-12-12 15:25:02 -06:00
Koen J b20b625820 Fixed compiler errors. 2024-12-12 16:02:37 +01:00
Koen J fd95311920 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-12 14:13:34 +01:00
Koen J 6da5c11731 Fixed concurrent modification crash in ServiceRecordAggregator and link clicking in scroll. 2024-12-12 14:13:24 +01:00
Koen 4e58231308 Merge branch 'revamp-rotation-settings' into 'master'
Update and simplify rotation settings

See merge request videostreaming/grayjay!56
2024-12-12 12:56:21 +00:00
Kai ef0ecf249a update rotation settings 2024-12-11 14:38:58 -06:00
Kelvin 4981617f7a Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-11 20:24:17 +01:00
Kelvin 2070bc7007 Refs 2024-12-11 20:24:09 +01:00
Kai DeLorenzo 231d2461b3 Merge branch 'incorrect-number-of-columns-bug' into 'master'
fix the calculation that incorrectly sets the number of columns to display

See merge request videostreaming/grayjay!54
2024-12-11 17:48:17 +00:00
Kai DeLorenzo 3b457f87c4 Merge branch 'fix-fullscreen-from-pip' into 'master'
prevent going into full screen when entering pip mode

See merge request videostreaming/grayjay!55
2024-12-11 17:48:00 +00:00
Koen J de3ced4d3c Intent class should be MediaButtonReceiver. 2024-12-11 10:41:36 +01:00
Koen J 891777e89e Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-11 10:36:13 +01:00
Koen J 287239dd1c Added media button receiver. 2024-12-11 10:36:02 +01:00
Kelvin 7cdded8fd7 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-10 22:35:46 +01:00
Kelvin 8c9d045e1d Download playlist fix for videos without audio file 2024-12-10 22:35:38 +01:00
Kai 620f5a0459 prevent going into full screen when entering pip mode 2024-12-10 11:30:48 -06:00
Koen 178d874ba0 Merge branch 'spinner-block-player' into 'master'
Spinner block player go full screen

See merge request videostreaming/grayjay!53
2024-12-10 16:18:55 +00:00
Koen d44f30c8a6 Merge branch 'creator-filter-clear' into 'master'
enable creator filter clear

See merge request videostreaming/grayjay!52
2024-12-10 15:56:42 +00:00
Koen J ce66937429 Offline playback toast now doesn't show more than once every 5 seconds. 2024-12-10 14:01:34 +01:00
Koen J 9823337375 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-10 13:54:29 +01:00
Koen J 11f5f0dfe1 Fixed comment character counter. 2024-12-10 13:54:02 +01:00
Koen J e1882f19e8 Comment close now requires confirmation. Fixed comment character counter. 2024-12-10 13:52:24 +01:00
Koen J 6a8b9f06c2 Comment close now requires confirmation. 2024-12-10 13:52:02 +01:00
Koen J 752fc8787d Fixed link scrolling behaviour. 2024-12-10 13:29:09 +01:00
Koen J 90a1cd8280 Placing reply comments works again. 2024-12-10 13:07:03 +01:00
Koen J aa570ac29d Updated submodules. 2024-12-10 12:47:18 +01:00
Kai fb7b6363f9 added multi column support for channels 2024-12-09 18:00:24 -06:00
Kai 23afe7994c fix the calculation that incorrectly sets the number of columns to display 2024-12-08 16:47:13 -06:00
Kai 7557e6f6ba prevent going full screen before the video has loaded 2024-12-07 11:32:28 -06:00
Kai 86b6938911 added video detail check 2024-12-07 11:09:48 -06:00
Kai 8f30a45fa8 enable creator filter clear 2024-12-06 11:13:29 -06:00
Koen J 7c9e9d5f52 Should not crash app when StateSync fails to bind. 2024-12-06 17:49:18 +01:00
Koen J 4066ce73a8 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-12-06 14:55:10 +01:00
Koen J b5722dba1a MainActivity should be singleInstance. 2024-12-06 14:54:57 +01:00
Koen 81765ecafc Update deploy-playstore.sh 2024-12-06 10:54:36 +00:00
Kelvin 84b42e9d19 refs 2024-12-05 17:08:30 +01:00
Koen J ed319a0e5f Fixed invalid padding. 2024-12-05 17:03:45 +01:00
Koen J dd55d10194 Crashfix. 2024-12-05 17:00:23 +01:00
Koen J 2084b46090 Possible fix for sub bar distance. 2024-12-05 16:58:35 +01:00
Koen J 53443a6cf2 Only show announcements on subscriptions if home is not enabled. Add margin top to subscriptions. 2024-12-05 16:51:56 +01:00
Koen J 92715b5642 Fixed crash due to AnnouncementView. 2024-12-05 16:40:05 +01:00
48 changed files with 720 additions and 463 deletions
+7 -1
View File
@@ -36,6 +36,12 @@
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
</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"
android:enabled="true"
android:foregroundServiceType="mediaPlayback" />
@@ -52,7 +58,7 @@
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:exported="true"
android:theme="@style/Theme.FutoVideo.NoActionBar"
android:launchMode="singleTask"
android:launchMode="singleInstance"
android:resizeableActivity="true"
android:supportsPictureInPicture="true">
@@ -412,15 +412,13 @@ class Settings : FragmentedStorageFileJson() {
var preferredPreviewQuality: Int = 5;
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
var simplifySources: Boolean = true;
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5)
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
var autoRotate: Int = 2;
@FormField(R.string.force_enable_auto_rotate_in_full_screen, FieldForm.TOGGLE, R.string.force_enable_auto_rotate_in_full_screen_description, 5)
var forceAllowFullScreenRotation: Boolean = false
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
@DropdownFieldOptionsId(R.array.player_background_behavior)
var backgroundPlay: Int = 2;
@@ -1,11 +1,13 @@
package com.futo.platformplayer.activities
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.media.AudioManager
import android.net.Uri
import android.os.Bundle
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.models.ImportCache
import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.receivers.MediaButtonReceiver
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
@@ -107,6 +110,7 @@ import java.util.LinkedList
import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue
class MainActivity : AppCompatActivity, IWithResultLauncher {
//TODO: Move to dimensions
@@ -6,6 +6,7 @@ import android.graphics.Color
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.WindowManager
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);
_textCharacterCount = findViewById(R.id.character_count);
_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 {
override fun afterTextChanged(s: Editable?) = 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();
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;
_buttonCancel.setOnClickListener {
clearFocus();
dismiss();
handleCloseAttempt()
};
setOnCancelListener {
handleCloseAttempt()
}
_buttonCreate.setOnClickListener {
clearFocus();
@@ -134,6 +148,22 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
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() {
_editComment.requestFocus();
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
@@ -100,6 +100,7 @@ class VideoDownload {
var requireVideoSource: Boolean = false;
var requireAudioSource: Boolean = false;
var requiredCheck: Boolean = false;
@Contextual
@Transient
@@ -164,7 +165,7 @@ class VideoDownload {
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.videoSource = null;
this.audioSource = null;
@@ -175,8 +176,9 @@ class VideoDownload {
this.requiresLiveVideoSource = false;
this.requiresLiveAudioSource = false;
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.requiredCheck = optionalSources;
}
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
this.video = SerializedPlatformVideo.fromVideo(video);
@@ -250,6 +252,30 @@ class VideoDownload {
if(original !is IPlatformVideoDetails)
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()) {
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
throw DownloadException("Unsupported video for downloading", false);
@@ -1,12 +1,13 @@
package com.futo.platformplayer.fragment.channel.tab
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
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.contents.ContentType
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.structures.IAsyncPager
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.math.max
class ChannelContentsFragment : Fragment(), IChannelTabFragment {
private var _recyclerResults: RecyclerView? = null;
private var _llmVideo: LinearLayoutManager? = null;
private var _glmVideo: GridLayoutManager? = null;
private var _loading = false;
private var _pager_parent: IPager<IPlatformContent>? = null;
private var _pager: IPager<IPlatformContent>? = null;
@@ -118,7 +119,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
super.onScrolled(recyclerView, dx, dy);
val recyclerResults = _recyclerResults ?: return;
val llmVideo = _llmVideo ?: return;
val llmVideo = _glmVideo ?: return;
val visibleItemCount = recyclerResults.childCount;
val firstVisibleItem = llmVideo.findFirstVisibleItemPosition();
@@ -163,9 +164,10 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
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?.layoutManager = _llmVideo;
_recyclerResults?.layoutManager = _glmVideo;
_recyclerResults?.addOnScrollListener(_scrollListener);
return view;
@@ -181,6 +183,13 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
_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) {
if (_pager_parent != null && _pager_parent is IRefreshPager<*>) {
@@ -1,12 +1,13 @@
package com.futo.platformplayer.fragment.channel.tab
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.math.max
class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
private var _recyclerResults: RecyclerView? = null
private var _llmPlaylist: LinearLayoutManager? = null
private var _glmPlaylist: GridLayoutManager? = null
private var _loading = false
private var _pagerParent: IPager<IPlatformPlaylist>? = null
private var _pager: IPager<IPlatformPlaylist>? = null
@@ -109,7 +111,7 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
super.onScrolled(recyclerView, dx, dy)
val recyclerResults = _recyclerResults ?: return
val llmPlaylist = _llmPlaylist ?: return
val llmPlaylist = _glmPlaylist ?: return
val visibleItemCount = recyclerResults.childCount
val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition()
@@ -158,9 +160,10 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
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?.layoutManager = _llmPlaylist
_recyclerResults?.layoutManager = _glmPlaylist
_recyclerResults?.addOnScrollListener(_scrollListener)
return view
@@ -176,6 +179,13 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
_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(
pager: IPager<IPlatformPlaylist>
) {
@@ -90,7 +90,7 @@ class BuyFragment : MainFragment() {
try {
val currencies = StatePayment.instance.getAvailableCurrencies("grayjay");
val prices = StatePayment.instance.getAvailableCurrencyPrices("grayjay");
val country = StatePayment.instance.getPaymentCountryFromIP()?.let { c -> PaymentConfigurations.COUNTRIES.find { it.id.equals(c, ignoreCase = true) } };
val country = StatePayment.instance.getPaymentCountryFromIP(true)?.let { c -> PaymentConfigurations.COUNTRIES.find { it.id.equals(c, ignoreCase = true) } };
val currency = country?.let { c -> PaymentConfigurations.CURRENCIES.find { it.id == c.defaultCurrencyId && (currencies.contains(it.id)) } };
if(currency != null && prices.containsKey(currency.id)) {
@@ -33,6 +33,7 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.withTimestamp
import kotlin.math.floor
import kotlin.math.max
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
private var _exoPlayer: PlayerManager? = null;
@@ -168,7 +169,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
val glmResults =
GridLayoutManager(
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
}
@@ -8,6 +8,7 @@ import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.Spinner
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager
@@ -25,11 +26,20 @@ class CreatorsFragment : MainFragment() {
private var _overlayContainer: FrameLayout? = null;
private var _containerSearch: FrameLayout? = null;
private var _editSearch: EditText? = null;
private var _buttonClearSearch: ImageButton? = null
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_creators, container, false);
_containerSearch = view.findViewById(R.id.container_search);
_editSearch = view.findViewById(R.id.edit_search);
val 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));
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
@@ -51,7 +61,12 @@ class CreatorsFragment : MainFragment() {
_spinnerSortBy = spinnerSortBy;
_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);
@@ -25,10 +25,12 @@ import com.futo.platformplayer.views.others.ProgressBar
import com.futo.platformplayer.views.others.TagsView
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder
import com.futo.platformplayer.views.announcements.AnnouncementView
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
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 {
protected val _recyclerResults: RecyclerView;
@@ -37,6 +39,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
private val _progressBar: ProgressBar;
private val _spinnerSortBy: Spinner;
private val _containerSortBy: LinearLayout;
private val _announcementView: AnnouncementView;
private val _tagsView: TagsView;
private val _textCentered: TextView;
private val _emptyPagerContainer: FrameLayout;
@@ -73,6 +76,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
_textCentered = findViewById(R.id.text_centered);
_emptyPagerContainer = findViewById(R.id.empty_pager_container);
_progressBar = findViewById(R.id.progress_bar);
_announcementView = findViewById(R.id.announcement_view)
_progressBar.inactiveColor = Color.TRANSPARENT;
_swipeRefresh = findViewById(R.id.swipe_refresh);
@@ -172,6 +176,10 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
_recyclerResults.addOnScrollListener(_scrollListener);
}
protected fun showAnnouncementView() {
_announcementView.visibility = View.VISIBLE
}
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
val canScroll = if (recyclerData.results.isEmpty()) false else {
val layoutManager = recyclerData.layoutManager
@@ -227,7 +235,8 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
}
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?) {
@@ -94,20 +94,10 @@ class HomeFragment : MainFragment() {
class HomeView : ContentFeedView<HomeFragment> {
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
private var _announcementsView: AnnouncementView = AnnouncementView(context, null).apply {
if(!this.isClosed()) {
recyclerData.adapter.viewsToPrepend.add(this)
this.onClose.subscribe {
recyclerData.adapter.viewsToPrepend.remove(this)
}
}
};
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
_taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({ fragment.lifecycleScope }, {
StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope)
})
@@ -138,6 +128,7 @@ class HomeFragment : MainFragment() {
};
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
showAnnouncementView()
}
fun onShown() {
@@ -35,7 +35,6 @@ import com.futo.platformplayer.views.ToastView
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder
import com.futo.platformplayer.views.announcements.AnnouncementView
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.subscriptions.SubscriptionBar
import kotlinx.coroutines.CancellationException
@@ -125,6 +124,9 @@ class SubscriptionsFeedFragment : MainFragment() {
initializeToolbarContent();
setPreviewsEnabled(Settings.instance.subscriptions.previewFeedItems);
if (Settings.instance.tabs.find { it.id == 0 }?.enabled != true) {
showAnnouncementView()
}
}
fun onShown() {
@@ -145,26 +147,6 @@ class SubscriptionsFeedFragment : MainFragment() {
}
}
val announcementsView = _announcementsView;
val homeTab = Settings.instance.tabs.find { it.id == 0 };
val isHomeEnabled = homeTab?.enabled == true;
if (announcementsView != null && isHomeEnabled) {
recyclerData.adapter.viewsToPrepend.remove(announcementsView)
_announcementsView = null
}
if (announcementsView == null && !isHomeEnabled) {
val c = context;
if (c != null) {
_announcementsView = AnnouncementView(c, null).apply {
recyclerData.adapter.viewsToPrepend.add(this)
this.onClose.subscribe {
recyclerData.adapter.viewsToPrepend.remove(this)
}
}
}
}
if (!StateSubscriptions.instance.global.isGlobalUpdating) {
finishRefreshLayoutLoader();
}
@@ -192,8 +174,6 @@ class SubscriptionsFeedFragment : MainFragment() {
private var _subscriptionBar: SubscriptionBar? = null;
private var _announcementsView: AnnouncementView? = null;
@Serializable
class FeedFilterSettings: FragmentedStorageFileJson() {
val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST);
@@ -1,11 +1,12 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.OrientationEventListener
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
@@ -28,13 +29,18 @@ import com.futo.platformplayer.models.PlatformVideoWithTime
import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.min
//region Fragment
@UnstableApi
class VideoDetailFragment : MainFragment {
override val isMainView : Boolean = false;
class VideoDetailFragment() : MainFragment() {
override val isMainView: Boolean = false;
override val hasBottomBar: Boolean = true;
override val isOverlay : Boolean = true;
override val isOverlay: Boolean = true;
override val isHistory: Boolean = false;
private var _isActive: Boolean = false;
@@ -76,8 +82,10 @@ class VideoDetailFragment : MainFragment {
private var _loadUrlOnCreate: UrlVideoWithTime? = null;
private var _leavingPiP = false;
//region Fragment
constructor() : super()
private var _landscapeOrientationListener: LandscapeOrientationListener? = null
private var _portraitOrientationListener: PortraitOrientationListener? = null
private var _lastSetOrientation: Int = Configuration.ORIENTATION_UNDEFINED
private var _ignoreNextNewOrientation = false
fun nextVideo() {
_viewDetail?.nextVideo(true, true, true);
@@ -91,7 +99,7 @@ class VideoDetailFragment : MainFragment {
return min(
resources.configuration.screenWidthDp,
resources.configuration.screenHeightDp
) < resources.getDimension(R.dimen.landscape_threshold)
) < resources.getInteger(R.integer.column_width_dp) * 2
}
override fun onConfigurationChanged(newConfig: Configuration) {
@@ -101,6 +109,15 @@ class VideoDetailFragment : MainFragment {
val isSmallWindow = isSmallWindow()
val temp = _lastSetOrientation
if (_ignoreNextNewOrientation) {
_ignoreNextNewOrientation = false
} else {
// the device has rotated so update our state tracking what the physical orientation of the device is
_lastSetOrientation = newConfig.orientation
}
if (
isSmallWindow
&& newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
@@ -113,6 +130,7 @@ class VideoDetailFragment : MainFragment {
&& isFullscreen
&& !Settings.instance.playback.fullscreenPortrait
&& newConfig.orientation == Configuration.ORIENTATION_PORTRAIT
&& temp == Configuration.ORIENTATION_LANDSCAPE
&& isLandscapeVideo
) {
_viewDetail?.setFullscreen(false)
@@ -143,7 +161,6 @@ class VideoDetailFragment : MainFragment {
}
}
@SuppressLint("SourceLockedOrientationActivity")
fun updateOrientation() {
val a = activity ?: return
val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait
@@ -154,36 +171,52 @@ class VideoDetailFragment : MainFragment {
val isSmallWindow = isSmallWindow()
val autoRotateEnabled = android.provider.Settings.System.getInt(
context?.contentResolver,
android.provider.Settings.System.ACCELEROMETER_ROTATION, 0
) == 1
// For small windows if the device isn't landscape right now and full screen portrait isn't allowed then we should force landscape
if (isSmallWindow && isFullscreen && !isFullScreenPortraitAllowed && resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT && !rotationLock && isLandscapeVideo) {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
if (isSmallWindow && isFullscreen && !isFullScreenPortraitAllowed && _lastSetOrientation != Configuration.ORIENTATION_LANDSCAPE && !rotationLock && isLandscapeVideo) {
if (Settings.instance.playback.forceAllowFullScreenRotation) {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
} else {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}
// the next orientation change will not reflect the device because we are manually setting the orientation to landscape
_ignoreNextNewOrientation = true
if (autoRotateEnabled
) {
// start listening for the device to rotate to landscape
// at which point we'll be able to set requestedOrientation to back to UNSPECIFIED
_landscapeOrientationListener?.enableListener()
}
}
// For small windows if the device isn't in a portrait orientation and we're in the maximized state then we should force portrait
else if (isSmallWindow && !isMinimizingFromFullScreen && !isFullscreen && state == State.MAXIMIZED && resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
else if (isSmallWindow && !isMinimizingFromFullScreen && !isFullscreen && state == State.MAXIMIZED && _lastSetOrientation == Configuration.ORIENTATION_LANDSCAPE) {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
// the next orientation change will not reflect the device because we are manually setting the orientation to portrait
_ignoreNextNewOrientation = true
if (autoRotateEnabled
) {
// start listening for the device to rotate to portrait
// at which point we'll be able to set requestedOrientation to back to UNSPECIFIED
_portraitOrientationListener?.enableListener()
} else {
// the rotation state resets to portrait when changing requestedOrientation
_lastSetOrientation = Configuration.ORIENTATION_PORTRAIT
}
} else if (rotationLock) {
_portraitOrientationListener?.disableListener()
_landscapeOrientationListener?.disableListener()
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
} else {
when (Settings.instance.playback.autoRotate) {
0 -> {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
}
1 -> {
a.requestedOrientation = if (isReversePortraitAllowed) {
ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
} else {
ActivityInfo.SCREEN_ORIENTATION_SENSOR
}
}
2 -> {
a.requestedOrientation = if (isReversePortraitAllowed) {
ActivityInfo.SCREEN_ORIENTATION_FULL_USER
} else {
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
_portraitOrientationListener?.disableListener()
_landscapeOrientationListener?.disableListener()
a.requestedOrientation = if (isReversePortraitAllowed) {
ActivityInfo.SCREEN_ORIENTATION_FULL_USER
} else {
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
}
@@ -354,6 +387,28 @@ class VideoDetailFragment : MainFragment {
StatePlayer.instance.onRotationLockChanged.subscribe(this) {
updateOrientation()
}
val delayBeforeRemoveRotationLock = 800L
_landscapeOrientationListener = LandscapeOrientationListener(requireContext())
{
CoroutineScope(Dispatchers.Main).launch {
// delay to make sure that the system auto rotate updates
delay(delayBeforeRemoveRotationLock)
_lastSetOrientation = Configuration.ORIENTATION_LANDSCAPE
updateOrientation()
}
}
_portraitOrientationListener = PortraitOrientationListener(requireContext())
{
CoroutineScope(Dispatchers.Main).launch {
// delay to make sure that the system auto rotate updates
delay(delayBeforeRemoveRotationLock)
_lastSetOrientation = Configuration.ORIENTATION_PORTRAIT
updateOrientation()
}
}
return _view!!;
}
@@ -455,6 +510,9 @@ class VideoDetailFragment : MainFragment {
SettingsActivity.settingsActivityClosed.remove(this)
StatePlayer.instance.onRotationLockChanged.remove(this)
_landscapeOrientationListener?.disableListener()
_portraitOrientationListener?.disableListener()
_viewDetail?.let {
_viewDetail = null;
it.onDestroy();
@@ -547,4 +605,66 @@ class VideoDetailFragment : MainFragment {
//region View
//TODO: Determine if encapsulated would be readable enough
//endregion
}
}
class LandscapeOrientationListener(
context: Context,
private val onLandscapeDetected: () -> Unit
) : OrientationEventListener(context) {
private var isListening = false
override fun onOrientationChanged(orientation: Int) {
if (!isListening) return
if (orientation in 60..120 || orientation in 240..300) {
onLandscapeDetected()
disableListener()
}
}
fun enableListener() {
if (!isListening) {
isListening = true
enable()
}
}
fun disableListener() {
if (isListening) {
isListening = false
disable()
}
}
}
class PortraitOrientationListener(
context: Context,
private val onPortraitDetected: () -> Unit
) : OrientationEventListener(context) {
private var isListening = false
override fun onOrientationChanged(orientation: Int) {
if (!isListening) return
if (orientation in 0..30 || orientation in 330..360 || orientation in 150..210) {
onPortraitDetected()
disableListener()
}
}
fun enableListener() {
if (!isListening) {
isListening = true
enable()
}
}
fun disableListener() {
if (isListening) {
isListening = false
disable()
}
}
}
@@ -1799,8 +1799,13 @@ class VideoDetailView : ConstraintLayout {
private fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean){
Logger.i(TAG, "onSourceChanged(videoSource=$videoSource, audioSource=$audioSource, resume=$resume)")
if((videoSource == null || videoSource is LocalVideoSource) && (audioSource == null || audioSource is LocalAudioSource))
UIDialogs.toast(context, context.getString(R.string.offline_playback), false);
if((videoSource == null || videoSource is LocalVideoSource) && (audioSource == null || audioSource is LocalAudioSource)) {
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(videoSource is IDashManifestSource || videoSource is IHLSManifestSource) {
if (video?.isLive == true) {
@@ -2379,8 +2384,8 @@ class VideoDetailView : ConstraintLayout {
}
fun isLandscapeVideo(): Boolean? {
var videoSourceWidth = _player.exoPlayer?.player?.videoSize?.width
var videoSourceHeight = _player.exoPlayer?.player?.videoSize?.height
val videoSourceWidth = _player.exoPlayer?.player?.videoSize?.width
val videoSourceHeight = _player.exoPlayer?.player?.videoSize?.height
return if (videoSourceWidth == null || videoSourceHeight == null || videoSourceWidth == 0 || videoSourceHeight == 0){
null
@@ -2582,7 +2587,6 @@ class VideoDetailView : ConstraintLayout {
_overlayContainer.removeAllViews();
_overlay_quality_selector?.hide();
_player.setFullScreen(true)
_player.fillHeight(false)
_layoutPlayerContainer.setPadding(0, 0, 0, 0);
}
@@ -2797,7 +2801,7 @@ class VideoDetailView : ConstraintLayout {
super.onConfigurationChanged(newConfig)
if (fragment.state == VideoDetailFragment.State.MINIMIZED) {
_player.fillHeight(true)
} else if (!fragment.isFullscreen) {
} else if (!fragment.isFullscreen && !fragment.isInPictureInPicture) {
_player.fitHeight()
}
}
@@ -3030,8 +3034,6 @@ class VideoDetailView : ConstraintLayout {
const val TAG_MORE = "MORE";
private val _buttonPinStore = FragmentedStorage.get<StringArrayStorage>("videoPinnedButtons");
private var _lastOfflinePlaybackToastTime: Long = 0
}
}
@@ -55,21 +55,25 @@ class ServiceRecordAggregator {
if (_cts != null) throw Exception("Already started.")
_cts = CoroutineScope(Dispatchers.Default).launch {
while (isActive) {
val now = Date()
synchronized(_currentServices) {
_cachedAddressRecords.forEach { it.value.removeAll { record -> now.after(record.expirationTime) } }
_cachedTxtRecords.entries.removeIf { now.after(it.value.expirationTime) }
_cachedSrvRecords.entries.removeIf { now.after(it.value.expirationTime) }
_cachedPtrRecords.forEach { it.value.removeAll { record -> now.after(record.expirationTime) } }
try {
while (isActive) {
val now = Date()
synchronized(_currentServices) {
_cachedAddressRecords.forEach { it.value.removeAll { record -> now.after(record.expirationTime) } }
_cachedTxtRecords.entries.removeIf { now.after(it.value.expirationTime) }
_cachedSrvRecords.entries.removeIf { now.after(it.value.expirationTime) }
_cachedPtrRecords.forEach { it.value.removeAll { record -> now.after(record.expirationTime) } }
val newServices = getCurrentServices()
_currentServices.clear()
_currentServices.addAll(newServices)
val newServices = getCurrentServices()
_currentServices.clear()
_currentServices.addAll(newServices)
}
onServicesUpdated?.invoke(_currentServices.toList())
delay(5000)
}
onServicesUpdated?.invoke(_currentServices.toList())
delay(5000)
} catch (e: Throwable) {
Logger.e(TAG, "Unexpected failure in MDNS loop", e)
}
}
}
@@ -83,6 +87,7 @@ class ServiceRecordAggregator {
}
fun add(packet: DnsPacket) {
val currentServices: List<DnsService>
val dnsResourceRecords = packet.answers + packet.additionals + packet.authorities
val txtRecords = dnsResourceRecords.filter { it.type == ResourceRecordType.TXT.value.toInt() }.map { it to it.getDataReader().readTXTRecord() }
val aRecords = dnsResourceRecords.filter { it.type == ResourceRecordType.A.value.toInt() }.map { it to it.getDataReader().readARecord() }
@@ -99,35 +104,33 @@ class ServiceRecordAggregator {
aaaaRecords.forEach { builder.appendLine("AAAA ${it.first.name} ${it.first.type} ${it.first.clazz} TTL ${it.first.timeToLive}: ${it.second.address}") }
Logger.i(TAG, "$builder")*/
val currentServices: MutableList<DnsService>
ptrRecords.forEach { record ->
val cachedPtrRecord = _cachedPtrRecords.getOrPut(record.first.name) { mutableListOf() }
val newPtrRecord = CachedDnsPtrRecord(Date(System.currentTimeMillis() + record.first.timeToLive.toLong() * 1000L), record.second.domainName)
cachedPtrRecord.replaceOrAdd(newPtrRecord) { it.target == record.second.domainName }
}
aRecords.forEach { aRecord ->
val cachedARecord = _cachedAddressRecords.getOrPut(aRecord.first.name) { mutableListOf() }
val newARecord = CachedDnsAddressRecord(Date(System.currentTimeMillis() + aRecord.first.timeToLive.toLong() * 1000L), aRecord.second.address)
cachedARecord.replaceOrAdd(newARecord) { it.address == newARecord.address }
}
aaaaRecords.forEach { aaaaRecord ->
val cachedAaaaRecord = _cachedAddressRecords.getOrPut(aaaaRecord.first.name) { mutableListOf() }
val newAaaaRecord = CachedDnsAddressRecord(Date(System.currentTimeMillis() + aaaaRecord.first.timeToLive.toLong() * 1000L), aaaaRecord.second.address)
cachedAaaaRecord.replaceOrAdd(newAaaaRecord) { it.address == newAaaaRecord.address }
}
txtRecords.forEach { txtRecord ->
_cachedTxtRecords[txtRecord.first.name] = CachedDnsTxtRecord(Date(System.currentTimeMillis() + txtRecord.first.timeToLive.toLong() * 1000L), txtRecord.second.texts)
}
srvRecords.forEach { srvRecord ->
_cachedSrvRecords[srvRecord.first.name] = CachedDnsSrvRecord(Date(System.currentTimeMillis() + srvRecord.first.timeToLive.toLong() * 1000L), srvRecord.second)
}
//TODO: Maybe this can be debounced?
synchronized(this._currentServices) {
ptrRecords.forEach { record ->
val cachedPtrRecord = _cachedPtrRecords.getOrPut(record.first.name) { mutableListOf() }
val newPtrRecord = CachedDnsPtrRecord(Date(System.currentTimeMillis() + record.first.timeToLive.toLong() * 1000L), record.second.domainName)
cachedPtrRecord.replaceOrAdd(newPtrRecord) { it.target == record.second.domainName }
}
aRecords.forEach { aRecord ->
val cachedARecord = _cachedAddressRecords.getOrPut(aRecord.first.name) { mutableListOf() }
val newARecord = CachedDnsAddressRecord(Date(System.currentTimeMillis() + aRecord.first.timeToLive.toLong() * 1000L), aRecord.second.address)
cachedARecord.replaceOrAdd(newARecord) { it.address == newARecord.address }
}
aaaaRecords.forEach { aaaaRecord ->
val cachedAaaaRecord = _cachedAddressRecords.getOrPut(aaaaRecord.first.name) { mutableListOf() }
val newAaaaRecord = CachedDnsAddressRecord(Date(System.currentTimeMillis() + aaaaRecord.first.timeToLive.toLong() * 1000L), aaaaRecord.second.address)
cachedAaaaRecord.replaceOrAdd(newAaaaRecord) { it.address == newAaaaRecord.address }
}
txtRecords.forEach { txtRecord ->
_cachedTxtRecords[txtRecord.first.name] = CachedDnsTxtRecord(Date(System.currentTimeMillis() + txtRecord.first.timeToLive.toLong() * 1000L), txtRecord.second.texts)
}
srvRecords.forEach { srvRecord ->
_cachedSrvRecords[srvRecord.first.name] = CachedDnsSrvRecord(Date(System.currentTimeMillis() + srvRecord.first.timeToLive.toLong() * 1000L), srvRecord.second)
}
currentServices = getCurrentServices()
this._currentServices.clear()
this._currentServices.addAll(currentServices)
@@ -12,70 +12,113 @@ import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.timestampRegex
import com.futo.platformplayer.views.behavior.NonScrollingTextView
import com.futo.platformplayer.views.behavior.NonScrollingTextView.Companion
import kotlinx.coroutines.runBlocking
class PlatformLinkMovementMethod : LinkMovementMethod {
private val _context: Context;
class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMethod() {
constructor(context: Context) : super() {
_context = context;
}
private var pressedLinks: Array<URLSpan>? = null
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 {
val action = event.action;
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 action = event.actionMasked
val layout = widget.layout;
val line = layout.getLineForVertical(y);
val off = layout.getOffsetForHorizontal(line, x.toFloat());
val links = buffer.getSpans(off, off, URLSpan::class.java);
when (action) {
MotionEvent.ACTION_DOWN -> {
val links = findLinksAtTouchPosition(widget, buffer, event)
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()) {
runBlocking {
for (link in links) {
Logger.i(TAG) { "Link clicked '${link.url}'." };
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
pressedLinks = null
widget.parent?.requestDisallowInterceptTouchEvent(false)
return false
}
return true
}
}
if (_context is MainActivity) {
if (_context.handleUrl(link.url)) {
continue;
}
MotionEvent.ACTION_UP -> {
if (linkPressed && pressedLinks != null) {
val dx = event.x - downX
val dy = event.y - downY
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(widget, event)) {
runBlocking {
for (link in pressedLinks!!) {
Logger.i(TAG) { "Link clicked '${link.url}'." }
if (timestampRegex.matches(link.url)) {
val tokens = link.url.split(':');
if (_context is MainActivity) {
if (_context.handleUrl(link.url)) continue
if (timestampRegex.matches(link.url)) {
val tokens = link.url.split(':')
var time_s = -1L
when (tokens.size) {
2 -> time_s = tokens[0].toLong() * 60 + tokens[1].toLong()
3 -> time_s = tokens[0].toLong() * 3600 +
tokens[1].toLong() * 60 +
tokens[2].toLong()
}
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;
if (time_s != -1L) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000)
continue
}
}
}
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
}
}
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
pressedLinks = null
linkPressed = false
return true
} else {
pressedLinks = null
linkPressed = false
}
}
}
return true;
MotionEvent.ACTION_CANCEL -> {
linkPressed = false
pressedLinks = null
}
}
return super.onTouchEvent(widget, buffer, event);
return false
}
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)
}
private fun isTouchInside(widget: TextView, event: MotionEvent): Boolean {
return event.x >= 0 && event.x <= widget.width && event.y >= 0 && event.y <= widget.height
}
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.PlaybackStateCompat
import android.util.Log
import android.view.KeyEvent
import androidx.core.app.NotificationCompat
import com.bumptech.glide.Glide
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.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.receivers.MediaButtonReceiver
import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer
@@ -91,6 +93,7 @@ class MediaPlaybackService : Service() {
return START_STICKY;
}
fun setupNotificationRequirements() {
_audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager;
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
@@ -101,6 +104,7 @@ class MediaPlaybackService : Service() {
_notificationManager!!.createNotificationChannel(_notificationChannel!!);
_mediaSession = MediaSessionCompat(this, "PlayerState");
_mediaSession?.isActive = true
_mediaSession?.setPlaybackState(PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 1f)
.build());
@@ -143,6 +147,12 @@ class MediaPlaybackService : Service() {
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() {
@@ -160,10 +160,6 @@ class StateApp {
private var _cacheDirectory: File? = null;
private var _persistentDirectory: File? = null;
//AutoRotate
var systemAutoRotate: Boolean = false;
//Network
private var _lastMeteredState: Boolean = false;
private var _connectivityManager: ConnectivityManager? = null;
@@ -201,17 +197,6 @@ class StateApp {
return File(_persistentDirectory, name);
}
fun getCurrentSystemAutoRotate(): Boolean {
_context?.let {
systemAutoRotate = android.provider.Settings.System.getInt(
it.contentResolver,
android.provider.Settings.System.ACCELEROMETER_ROTATION, 0
) == 1;
};
return systemAutoRotate;
}
fun isCurrentMetered(): Boolean {
ensureConnectivityManager();
return _connectivityManager?.isActiveNetworkMetered ?: throw IllegalStateException("Connectivity manager not available");
@@ -312,9 +297,6 @@ class StateApp {
fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null) {
_context = context;
_scope = coroutineScope
//System checks
systemAutoRotate = getCurrentSystemAutoRotate();
}
fun initializeFiles(force: Boolean = false) {
@@ -251,7 +251,7 @@ class StateDownloads {
}
else {
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);
hasNew = true;
}
@@ -296,7 +296,7 @@ class StateDownloads {
}
else {
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);
hasNew = true;
}
@@ -112,18 +112,23 @@ class StateSync {
Logger.i(TAG, "Sync key pair initialized (public key = ${publicKey})")
_thread = Thread {
val serverSocket = ServerSocket(PORT)
_serverSocket = serverSocket
try {
val serverSocket = ServerSocket(PORT)
_serverSocket = serverSocket
Log.i(TAG, "Running on port ${PORT} (TCP)")
Log.i(TAG, "Running on port ${PORT} (TCP)")
while (_started) {
val socket = serverSocket.accept()
val session = createSocketSession(socket, true) { session, socketSession ->
while (_started) {
val socket = serverSocket.accept()
val session = createSocketSession(socket, true) { session, socketSession ->
}
session.startAsResponder()
}
session.startAsResponder()
} catch (e: Throwable) {
Logger.e(TAG, "Failed to bind server socket to port ${PORT}", e)
UIDialogs.toast("Failed to start sync, port in use")
}
}.apply { start() }
@@ -24,7 +24,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AnnouncementView : LinearLayout {
private val _root: ConstraintLayout;
private val _root: FrameLayout;
private val _textTitle: TextView;
private val _textCounter: TextView;
private val _textBody: TextView;
@@ -45,9 +45,6 @@ class AnnouncementView : LinearLayout {
_scope = findViewTreeLifecycleOwner()?.lifecycleScope ?: StateApp.instance.scopeOrNull; //TODO: Fetch correct scope
val dp10 = 10.dp(resources);
setPadding(dp10, dp10, dp10, dp10);
_root = findViewById(R.id.root);
_textTitle = findViewById(R.id.text_title);
_textCounter = findViewById(R.id.text_counter);
@@ -115,12 +112,12 @@ class AnnouncementView : LinearLayout {
_currentAnnouncement = announcement;
if (announcement == null) {
visibility = View.GONE
_root.visibility = View.GONE
onClose.emit()
return;
}
visibility = View.VISIBLE
_root.visibility = View.VISIBLE
_textTitle.text = announcement.title;
_textBody.text = announcement.msg;
@@ -16,73 +16,117 @@ import com.futo.platformplayer.timestampRegex
import kotlinx.coroutines.runBlocking
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, attrs: AttributeSet) : super(context, attrs) {}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}
override fun scrollTo(x: Int, y: Int) {
//do nothing
// do nothing
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
val action = event?.action
Logger.i(TAG, "onTouchEvent (action = $action)");
val action = event?.actionMasked
if (event == null) return super.onTouchEvent(event)
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
val x = event.x.toInt()
val y = event.y.toInt()
when (action) {
MotionEvent.ACTION_DOWN -> {
val x = event.x.toInt()
val y = event.y.toInt()
val layout: Layout? = this.layout
if (layout != null) {
val line = layout.getLineForVertical(y)
val offset = layout.getOffsetForHorizontal(line, x.toFloat())
val text = this.text
if (text is Spannable) {
val layout: Layout? = this.layout
if (layout != null && this.text is Spannable) {
val offset = layout.getOffsetForHorizontal(layout.getLineForVertical(y), x.toFloat())
val text = this.text as Spannable
val links = text.getSpans(offset, offset, URLSpan::class.java)
if (links.isNotEmpty()) {
runBlocking {
for (link in links) {
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;
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)));
}
}
}
parent?.requestDisallowInterceptTouchEvent(true)
_lastTouchedLinks = links
downX = event.x
downY = event.y
linkPressed = 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 && isTouchInside(event)) {
runBlocking {
for (link in _lastTouchedLinks!!) {
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." }
val c = context
if (c is MainActivity) {
if (c.handleUrl(link.url)) continue
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 false
}
private fun isTouchInside(event: MotionEvent): Boolean {
return event.x >= 0 && event.x <= width && event.y >= 0 && event.y <= height
}
companion object {
private const val TAG = "NonScrollingTextView"
}
}
}
@@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
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.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.structures.IAsyncPager
@@ -267,9 +268,13 @@ class CommentsList : ConstraintLayout {
}
fun replaceComment(c: PolycentricPlatformComment, newComment: PolycentricPlatformComment) {
val index = _comments.indexOf(c);
_comments[index] = newComment;
_adapterComments.notifyItemChanged(_adapterComments.childToParentPosition(index));
val index = _comments.indexOfFirst { it == c || (it is LazyComment && it.getUnderlyingComment() == c) };
if (index >= 0) {
_comments[index] = newComment;
_adapterComments.notifyItemChanged(_adapterComments.childToParentPosition(index));
} else {
Logger.w(TAG, "Parent comment not found")
}
}
companion object {
@@ -592,6 +592,11 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
@OptIn(UnstableApi::class)
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()
if (isFullScreen == fullScreen) {
@@ -808,17 +813,12 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
fun updateRotateLock() {
if(Settings.instance.playback.autoRotate == 0) {
_control_rotate_lock.visibility = View.GONE;
_control_rotate_lock_fullscreen.visibility = View.GONE;
}
else {
_control_rotate_lock.visibility = View.VISIBLE;
_control_rotate_lock_fullscreen.visibility = View.VISIBLE;
}
_control_rotate_lock.visibility = View.VISIBLE;
_control_rotate_lock_fullscreen.visibility = View.VISIBLE;
if(StatePlayer.instance.rotationLock) {
_control_rotate_lock_fullscreen.setImageResource(R.drawable.ic_screen_rotation);
_control_rotate_lock.setImageResource(R.drawable.ic_screen_rotation);
_control_rotate_lock_fullscreen.setImageResource(R.drawable.ic_screen_lock_rotation_active);
_control_rotate_lock.setImageResource(R.drawable.ic_screen_lock_rotation_active);
}
else {
_control_rotate_lock_fullscreen.setImageResource(R.drawable.ic_screen_lock_rotation);
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="@color/colorPrimary"
android:pathData="M35.25,23.3 L36.4,22.2 38.6,24.45Q39.5,25.3 39.5,26.55Q39.5,27.8 38.6,28.65L32.5,34.75Q31.6,35.6 30.375,35.6Q29.15,35.6 28.3,34.75L13.8,20.25Q12.95,19.4 12.95,18.15Q12.95,16.9 13.8,16.1L19.95,9.95Q20.8,9.1 22.05,9.1Q23.3,9.1 24.1,9.95L26.45,12.25L25.3,13.35L22.9,10.95Q22.55,10.6 22.025,10.6Q21.5,10.6 21.15,10.95L14.85,17.25Q14.5,17.6 14.5,18.15Q14.5,18.7 14.85,19.05L29.5,33.75Q29.85,34.1 30.4,34.1Q30.95,34.1 31.25,33.75L37.6,27.4Q37.95,27.05 37.95,26.525Q37.95,26 37.6,25.65ZM26.15,44.45Q21.6,44.45 17.6,42.725Q13.6,41 10.625,38.025Q7.65,35.05 5.925,31.025Q4.2,27 4.2,22.5H5.75Q5.75,26.65 7.325,30.35Q8.9,34.05 11.65,36.825Q14.4,39.6 18.125,41.2Q21.85,42.8 26,42.85L18.55,35.35L19.65,34.25L29.5,44.1Q28.6,44.25 27.8,44.35Q27,44.45 26.15,44.45ZM32.4,18Q31.7,18 31.1,17.4Q30.5,16.8 30.5,16.1V10.85Q30.5,10.15 31.1,9.525Q31.7,8.9 32.4,8.9H32.55V6.85Q32.55,5.4 33.575,4.4Q34.6,3.4 36.05,3.4Q37.55,3.4 38.55,4.4Q39.55,5.4 39.55,6.85V8.9H39.75Q40.45,8.9 41,9.525Q41.55,10.15 41.55,10.85V16.1Q41.55,16.8 40.975,17.4Q40.4,18 39.65,18ZM34.05,8.9H38.05V6.85Q38.05,6 37.475,5.425Q36.9,4.85 36.05,4.85Q35.2,4.85 34.625,5.425Q34.05,6 34.05,6.85ZM26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Q26.25,22.35 26.25,22.35Z"/>
</vector>
+8 -1
View File
@@ -35,6 +35,12 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<com.futo.platformplayer.views.announcements.AnnouncementView
android:id="@+id/announcement_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<LinearLayout
android:id="@+id/container_sort_by"
android:layout_width="match_parent"
@@ -110,7 +116,8 @@
android:visibility="gone"
android:id="@+id/empty_pager_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="wrap_content"
android:layout_marginTop="30dp" />
</FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
+87 -84
View File
@@ -1,119 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:id="@+id/root"
android:background="@drawable/background_16_round_4dp"
android:paddingLeft="10dp"
android:paddingTop="10dp"
android:paddingRight="10dp">
android:id="@+id/root">
<TextView android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="Do you know?"
android:fontFamily="@font/inter_semibold"
android:textSize="15sp"
android:textColor="@color/white"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@id/text_counter" />
<TextView android:id="@+id/text_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="1/4"
android:fontFamily="@font/inter_regular"
android:textSize="12dp"
android:textColor="#585656"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<TextView android:id="@+id/text_body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Our app now supports dark mode for a better viewing experience. Check it out in your settings. Enjoy the new look!"
android:fontFamily="@font/inter_light"
android:textSize="14sp"
android:textColor="#9D9D9D"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_title"/>
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/text_body"
app:layout_constraintRight_toRightOf="parent"
android:paddingTop="4dp"
android:paddingBottom="10dp">
android:background="@drawable/background_16_round_4dp"
android:paddingLeft="10dp"
android:paddingTop="10dp"
android:paddingRight="10dp"
android:layout_margin="10dp">
<TextView android:id="@+id/text_time"
<TextView android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="Do you know?"
android:fontFamily="@font/inter_semibold"
android:textSize="15sp"
android:textColor="@color/white"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@id/text_counter" />
<TextView android:id="@+id/text_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="2022-03-01"
tools:text="1/4"
android:fontFamily="@font/inter_regular"
android:textSize="12dp"
android:textColor="#585656"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<Space android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView android:id="@+id/text_never"
<TextView android:id="@+id/text_body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/never"
android:fontFamily="@font/inter_regular"
tools:text="Our app now supports dark mode for a better viewing experience. Check it out in your settings. Enjoy the new look!"
android:fontFamily="@font/inter_light"
android:textSize="14sp"
android:textColor="@color/colorPrimary"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
app:layout_constraintTop_toBottomOf="@id/text_body"
app:layout_constraintRight_toLeftOf="@id/text_close"/>
android:textColor="#9D9D9D"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_title"/>
<TextView android:id="@+id/text_close"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dismiss"
android:fontFamily="@font/inter_regular"
android:textSize="14sp"
android:textColor="@color/colorPrimary"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
app:layout_constraintTop_toBottomOf="@id/text_body"
app:layout_constraintRight_toLeftOf="@id/button_action"/>
app:layout_constraintRight_toRightOf="parent"
android:paddingTop="4dp"
android:paddingBottom="10dp">
<FrameLayout android:id="@+id/button_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/background_button_primary_round_4dp"
app:layout_constraintTop_toBottomOf="@id/text_body"
app:layout_constraintRight_toRightOf="parent">
<TextView android:id="@+id/text_action"
<TextView android:id="@+id/text_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="What's New"
tools:text="2022-03-01"
android:fontFamily="@font/inter_regular"
android:textSize="12dp"
android:textColor="#585656"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<Space android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView android:id="@+id/text_never"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/never"
android:fontFamily="@font/inter_regular"
android:textSize="14sp"
android:textColor="@color/white"
android:textColor="@color/colorPrimary"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
app:layout_constraintTop_toBottomOf="@id/text_body"
app:layout_constraintRight_toLeftOf="@id/text_close"/>
</FrameLayout>
</LinearLayout>
<TextView android:id="@+id/text_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dismiss"
android:fontFamily="@font/inter_regular"
android:textSize="14sp"
android:textColor="@color/colorPrimary"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
app:layout_constraintTop_toBottomOf="@id/text_body"
app:layout_constraintRight_toLeftOf="@id/button_action"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout android:id="@+id/button_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/background_button_primary_round_4dp"
app:layout_constraintTop_toBottomOf="@id/text_body"
app:layout_constraintRight_toRightOf="parent">
<TextView android:id="@+id/text_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="What's New"
android:fontFamily="@font/inter_regular"
android:textSize="14sp"
android:textColor="@color/white"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
app:layout_constraintTop_toBottomOf="@id/text_body"
app:layout_constraintRight_toLeftOf="@id/text_close"/>
</FrameLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
-8
View File
@@ -233,8 +233,6 @@
<string name="announcement">إعلان</string>
<string name="attempt_to_utilize_byte_ranges">محاولة استخدام مدى البايت</string>
<string name="auto_update">تحديث تلقائي</string>
<string name="auto_rotate">تدوير تلقائي</string>
<string name="auto_rotate_dead_zone">منطقة ميتة للتدوير التلقائي</string>
<string name="automatic_backup">نسخ احتياطي تلقائي</string>
<string name="background_behavior">سلوك الخلفية</string>
<string name="background_update">تحديث الخلفية</string>
@@ -539,7 +537,6 @@
<string name="not_yet_available_retrying_in_time_s">لم يصبح متوفراً بعد، إعادة المحاولة في {time}s</string>
<string name="failed_to_retry_for_live_stream">فشل في إعادة المحاولة للبث المباشر</string>
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">هذا التطبيق قيد التطوير. يرجى إرسال تقارير الأخطاء وفهم أن العديد من الميزات غير مكتملة.</string>
<string name="please_use_at_least_3_characters">يرجى استخدام 3 أحرف على الأقل</string>
<string name="are_you_sure_you_want_to_delete_this_video">هل أنت متأكد من أنك ترغب في حذف هذا الفيديو؟</string>
<string name="tap_to_open">انقر للفتح</string>
<string name="watching">يشاهد</string>
@@ -661,11 +658,6 @@
<item>عند التشغيل</item>
<item>أبداً</item>
</string-array>
<string-array name="system_enabled_disabled_array">
<item>معطل</item>
<item>مفعل</item>
<item>كما في النظام</item>
</string-array>
<string-array name="enabled_disabled_array">
<item>معطل</item>
<item>مفعل</item>
-8
View File
@@ -243,8 +243,6 @@
<string name="announcement">Ankündigung</string>
<string name="attempt_to_utilize_byte_ranges">Versuch, Byte-Bereiche zu nutzen</string>
<string name="auto_update">Automatische Aktualisierung</string>
<string name="auto_rotate">Automatische Drehung</string>
<string name="auto_rotate_dead_zone">Toter Winkel für automatische Drehung</string>
<string name="automatic_backup">Automatisches Backup</string>
<string name="background_behavior">Hintergrundverhalten</string>
<string name="background_update">Hintergrundaktualisierung</string>
@@ -542,7 +540,6 @@
<string name="not_yet_available_retrying_in_time_s">Noch nicht verfügbar, erneuter Versuch in {time}s</string>
<string name="failed_to_retry_for_live_stream">Fehler beim erneuten Versuch für den Live-Stream</string>
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">Diese App befindet sich in der Entwicklung. Bitte senden Sie Fehlerberichte und verstehen Sie, dass viele Funktionen unvollständig sind.</string>
<string name="please_use_at_least_3_characters">Bitte verwenden Sie mindestens 3 Zeichen</string>
<string name="are_you_sure_you_want_to_delete_this_video">Sind Sie sicher, dass Sie dieses Video löschen möchten?</string>
<string name="tap_to_open">Tippen Sie zum Öffnen</string>
<string name="watching">anschauen</string>
@@ -661,11 +658,6 @@
<item>Beim Start</item>
<item>Nie</item>
</string-array>
<string-array name="system_enabled_disabled_array">
<item>Deaktiviert</item>
<item>Aktiviert</item>
<item>Gleich wie System</item>
</string-array>
<string-array name="enabled_disabled_array">
<item>Deaktiviert</item>
<item>Aktiviert</item>
-14
View File
@@ -217,8 +217,6 @@
<string name="announcement">Anuncio</string>
<string name="attempt_to_utilize_byte_ranges">Intentar utilizar rangos de bytes</string>
<string name="auto_update">Actualización automática</string>
<string name="auto_rotate">Auto-rotar</string>
<string name="auto_rotate_dead_zone">Zona muerta de auto-rotación</string>
<string name="automatic_backup">Copia de seguridad automática</string>
<string name="background_behavior">Comportamiento en segundo plano</string>
<string name="background_update">Actualización en segundo plano</string>
@@ -523,7 +521,6 @@
<string name="not_yet_available_retrying_in_time_s">Todavía no está disponible, reintento en {time}s</string>
<string name="failed_to_retry_for_live_stream">Error al reintentar la transmisión en vivo</string>
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">Esta aplicación está en desarrollo. Por favor, envía informes de errores y comprende que muchas características están incompletas.</string>
<string name="please_use_at_least_3_characters">Por favor, usa al menos 3 caracteres</string>
<string name="are_you_sure_you_want_to_delete_this_video">¿Estás seguro de que deseas eliminar este video?</string>
<string name="tap_to_open">Toca para abrir</string>
<string name="watching">viendo</string>
@@ -671,17 +668,6 @@
<item>Al Iniciar</item>
<item>Nunca</item>
</string-array>
<string-array name="system_enabled_disabled_array">
<item>Desactivado</item>
<item>Activado</item>
<item>Mismo que el Sistema</item>
</string-array>
<string-array name="auto_rotate_dead_zone">
<item>0</item>
<item>5</item>
<item>10</item>
<item>20</item>
</string-array>
<string-array name="enabled_disabled_array">
<item>Desactivado</item>
<item>Activado</item>
-8
View File
@@ -256,8 +256,6 @@
<string name="announcement">Annonce</string>
<string name="attempt_to_utilize_byte_ranges">Tentative d\'utilisation de plages d\'octets</string>
<string name="auto_update">Mise à jour automatique</string>
<string name="auto_rotate">Rotation automatique</string>
<string name="auto_rotate_dead_zone">Zone morte de rotation automatique</string>
<string name="automatic_backup">Sauvegarde automatique</string>
<string name="background_behavior">Comportement en arrière-plan</string>
<string name="background_update">Mise à jour en arrière-plan</string>
@@ -562,7 +560,6 @@
<string name="not_yet_available_retrying_in_time_s">Pas encore disponible, réessai dans {time}s</string>
<string name="failed_to_retry_for_live_stream">Échec de la tentative de réessai pour la diffusion en direct</string>
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">Cette application est en développement. Veuillez soumettre des rapports de bug et comprendre que de nombreuses fonctionnalités ne sont pas encore complètes.</string>
<string name="please_use_at_least_3_characters">Veuillez utiliser au moins 3 caractères</string>
<string name="are_you_sure_you_want_to_delete_this_video">Êtes-vous sûr de vouloir supprimer cette vidéo ?</string>
<string name="tap_to_open">Appuyez pour ouvrir</string>
<string name="watching">en train de regarder</string>
@@ -661,11 +658,6 @@
<item>Au démarrage</item>
<item>Jamais</item>
</string-array>
<string-array name="system_enabled_disabled_array">
<item>Désactivé</item>
<item>Activé</item>
<item>Même que le système</item>
</string-array>
<string-array name="enabled_disabled_array">
<item>Désactivé</item>
<item>Activé</item>
-8
View File
@@ -220,8 +220,6 @@
<string name="announcement">お知らせ</string>
<string name="attempt_to_utilize_byte_ranges">バイト範囲を使用する試み</string>
<string name="auto_update">自動更新</string>
<string name="auto_rotate">自動回転</string>
<string name="auto_rotate_dead_zone">自動回転デッドゾーン</string>
<string name="automatic_backup">自動バックアップ</string>
<string name="background_behavior">バックグラウンドの動作</string>
<string name="background_update">バックグラウンド更新</string>
@@ -524,7 +522,6 @@
<string name="not_yet_available_retrying_in_time_s">{time}秒後に再試行、まだ利用できません</string>
<string name="failed_to_retry_for_live_stream">ライブストリームの再試行に失敗しました</string>
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">このアプリは開発中です。バグレポートを提出し、多くの機能が未完成であることを理解してください。</string>
<string name="please_use_at_least_3_characters">少なくとも3文字を使用してください</string>
<string name="are_you_sure_you_want_to_delete_this_video">このビデオを削除してもよろしいですか?</string>
<string name="tap_to_open">タップして開く</string>
<string name="watching">視聴中</string>
@@ -661,11 +658,6 @@
<item>起動時</item>
<item>なし</item>
</string-array>
<string-array name="system_enabled_disabled_array">
<item>無効</item>
<item>有効</item>
<item>システムと同じ</item>
</string-array>
<string-array name="enabled_disabled_array">
<item>無効</item>
<item>有効</item>
-8
View File
@@ -255,8 +255,6 @@
<string name="announcement">공고</string>
<string name="attempt_to_utilize_byte_ranges">바이트 범위 사용 시도</string>
<string name="auto_update">자동 업데이트</string>
<string name="auto_rotate">자동 회전</string>
<string name="auto_rotate_dead_zone">자동 회전 데드 존</string>
<string name="automatic_backup">자동 백업</string>
<string name="background_behavior">백그라운드 동작</string>
<string name="background_update">백그라운드 업데이트</string>
@@ -561,7 +559,6 @@
<string name="not_yet_available_retrying_in_time_s">아직 사용할 수 없습니다, {time}초 후에 다시 시도합니다</string>
<string name="failed_to_retry_for_live_stream">라이브 스트림을 다시 시도하지 못했습니다</string>
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">이 앱은 개발 중입니다. 버그 보고를 제출해 주시고, 많은 기능이 미완성임을 이해해 주세요.</string>
<string name="please_use_at_least_3_characters">최소 3자 이상 사용해 주세요</string>
<string name="are_you_sure_you_want_to_delete_this_video">이 비디오를 삭제하시겠습니까?</string>
<string name="tap_to_open">열려면 탭하세요</string>
<string name="watching">시청 중</string>
@@ -661,11 +658,6 @@
<item>시작할 때</item>
<item>안 함</item>
</string-array>
<string-array name="system_enabled_disabled_array">
<item>비활성화</item>
<item>활성화</item>
<item>시스템과 동일</item>
</string-array>
<string-array name="enabled_disabled_array">
<item>비활성화</item>
<item>활성화</item>
-8
View File
@@ -256,8 +256,6 @@
<string name="announcement">Anúncio</string>
<string name="attempt_to_utilize_byte_ranges">Tentar utilizar intervalos de bytes</string>
<string name="auto_update">Atualização Automática</string>
<string name="auto_rotate">Rotação Automática</string>
<string name="auto_rotate_dead_zone">Zona Morta de Rotação Automática</string>
<string name="automatic_backup">Backup Automático</string>
<string name="background_behavior">Comportamento em Segundo Plano</string>
<string name="background_update">Atualização em Segundo Plano</string>
@@ -557,7 +555,6 @@
<string name="not_yet_available_retrying_in_time_s">Ainda não disponível, tentando novamente em {time}s</string>
<string name="failed_to_retry_for_live_stream">Falha ao tentar novamente para transmissão ao vivo</string>
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">Este aplicativo está em desenvolvimento. Envie relatórios de erros e entenda que muitos recursos estão incompletos.</string>
<string name="please_use_at_least_3_characters">Use pelo menos 3 caracteres</string>
<string name="are_you_sure_you_want_to_delete_this_video">Tem certeza de que deseja excluir este vídeo?</string>
<string name="tap_to_open">Toque para abrir</string>
<string name="watching">Assistindo</string>
@@ -661,11 +658,6 @@
<item>Ao Iniciar</item>
<item>Nunca</item>
</string-array>
<string-array name="system_enabled_disabled_array">
<item>Desativado</item>
<item>Ativado</item>
<item>Como no Sistema</item>
</string-array>
<string-array name="enabled_disabled_array">
<item>Desativado</item>
<item>Ativado</item>
-8
View File
@@ -252,8 +252,6 @@
<string name="announcement">Объявление</string>
<string name="attempt_to_utilize_byte_ranges">Попытка использовать диапазоны байт</string>
<string name="auto_update">Автообновление</string>
<string name="auto_rotate">Автоповорот</string>
<string name="auto_rotate_dead_zone">Мертвая зона автоповорота</string>
<string name="automatic_backup">Автоматическое резервное копирование</string>
<string name="background_behavior">Поведение в фоновом режиме</string>
<string name="background_update">Фоновое обновление</string>
@@ -558,7 +556,6 @@
<string name="not_yet_available_retrying_in_time_s">Ещё недоступно, повторная попытка через {time}с</string>
<string name="failed_to_retry_for_live_stream">Не удалось повторить попытку для прямого эфира</string>
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">Это приложение находится в стадии разработки. Пожалуйста, отправляйте сообщения об ошибках и поймите, что многие функции незавершены.</string>
<string name="please_use_at_least_3_characters">Пожалуйста, используйте хотя бы 3 символа</string>
<string name="are_you_sure_you_want_to_delete_this_video">Вы уверены, что хотите удалить это видео?</string>
<string name="tap_to_open">Нажмите, чтобы открыть</string>
<string name="watching">Смотрят</string>
@@ -661,11 +658,6 @@
<item>При запуске</item>
<item>Никогда</item>
</string-array>
<string-array name="system_enabled_disabled_array">
<item>Отключено</item>
<item>Включено</item>
<item>Как в системе</item>
</string-array>
<string-array name="enabled_disabled_array">
<item>Отключено</item>
<item>Включено</item>
-8
View File
@@ -256,8 +256,6 @@
<string name="announcement">公告</string>
<string name="attempt_to_utilize_byte_ranges">尝试使用字节范围</string>
<string name="auto_update">自动更新</string>
<string name="auto_rotate">自动旋转</string>
<string name="auto_rotate_dead_zone">自动旋转死区</string>
<string name="automatic_backup">自动备份</string>
<string name="background_behavior">后台行为</string>
<string name="background_update">后台更新</string>
@@ -562,7 +560,6 @@
<string name="not_yet_available_retrying_in_time_s">尚未可用,将在{time}s后重试</string>
<string name="failed_to_retry_for_live_stream">无法重新尝试直播流</string>
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">此应用处于开发中。请提交错误报告,并理解许多功能尚未完成。</string>
<string name="please_use_at_least_3_characters">请至少使用3个字符</string>
<string name="are_you_sure_you_want_to_delete_this_video">您确定要删除此视频吗?</string>
<string name="tap_to_open">点击打开</string>
<string name="watching">正在观看</string>
@@ -661,11 +658,6 @@
<item>启动时</item>
<item>从不</item>
</string-array>
<string-array name="system_enabled_disabled_array">
<item>已禁用</item>
<item>已启用</item>
<item>与系统相同</item>
</string-array>
<string-array name="enabled_disabled_array">
<item>已禁用</item>
<item>已启用</item>
+1 -1
View File
@@ -2,5 +2,5 @@
<resources>
<dimen name="minimized_player_max_width">500dp</dimen>
<dimen name="app_bar_height">200dp</dimen>
<dimen name="landscape_threshold">300dp</dimen>
<integer name="column_width_dp">400</integer>
</resources>
+3 -13
View File
@@ -199,6 +199,7 @@
<string name="previous">Previous</string>
<string name="next">Next</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="my_playlist_name">My Playlist Name</string>
<string name="do_you_want_to_import_this_store">Do you want to import this store?</string>
@@ -286,10 +287,10 @@
<string name="planned_content_notifications_description">Schedules discovered planned content as notifications, resulting in more accurate notifications for this content.</string>
<string name="attempt_to_utilize_byte_ranges">Attempt to utilize byte ranges</string>
<string name="auto_update">Auto Update</string>
<string name="auto_rotate">Auto-Rotate</string>
<string name="force_enable_auto_rotate_in_full_screen">Force Enable Auto-Rotate In Full-Screen Mode</string>
<string name="force_enable_auto_rotate_in_full_screen_description">Force enable auto-rotation between the two landscape orientations in full-screen mode, even when you disable auto-rotate at the system level.</string>
<string name="simplify_sources">Simplify sources</string>
<string name="simplify_sources_description">Deduplicate sources by resolution so that only more relevant sources are visible.</string>
<string name="auto_rotate_dead_zone">Auto-Rotate Dead Zone</string>
<string name="automatic_backup">Automatic Backup</string>
<string name="background_behavior">Background Behavior</string>
<string name="background_update">Background Update</string>
@@ -927,17 +928,6 @@
<item>On Startup</item>
<item>Never</item>
</string-array>
<string-array name="system_enabled_disabled_array">
<item>Disabled</item>
<item>Enabled</item>
<item>Same as System</item>
</string-array>
<string-array name="auto_rotate_dead_zone" translatable="false">
<item>0</item>
<item>5</item>
<item>10</item>
<item>20</item>
</string-array>
<string-array name="enabled_disabled_array">
<item>Disabled</item>
<item>Enabled</item>
+1
View File
@@ -15,6 +15,7 @@ touch $DOCUMENT_ROOT/maintenance.file
# Swap over the 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
# Notify Cloudflare to wipe the CDN cache