Compare commits

...

42 Commits

Author SHA1 Message Date
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
Kelvin 6166392515 Polycentric disk caching 2024-12-04 18:20:40 +01:00
Kelvin 49d0dead7d Fix playlist wrong id for local conversion, refs 2024-12-04 01:31:53 +01:00
Kelvin 6f004830ff Fix directly opening playlists urls in wrong ui, causing wrong thread 2024-12-02 16:23:30 +01:00
50 changed files with 610 additions and 392 deletions
+7 -1
View File
@@ -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">
@@ -412,15 +412,13 @@ class Settings : FragmentedStorageFileJson() {
var preferredPreviewQuality: Int = 5; var preferredPreviewQuality: Int = 5;
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality); fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4) @FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
var simplifySources: Boolean = true; var simplifySources: Boolean = true;
@FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5) @FormField(R.string.force_allow_full_screen_rotation, FieldForm.TOGGLE, R.string.force_allow_full_screen_rotation_description, 5)
@DropdownFieldOptionsId(R.array.system_enabled_disabled_array) var forceAllowFullScreenRotation: Boolean = false
var autoRotate: Int = 2;
@FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7) @FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 6)
@DropdownFieldOptionsId(R.array.player_background_behavior) @DropdownFieldOptionsId(R.array.player_background_behavior)
var backgroundPlay: Int = 2; var backgroundPlay: Int = 2;
@@ -866,6 +864,9 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 3) @FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 3)
var polycentricEnabled: Boolean = true; var polycentricEnabled: Boolean = true;
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 4)
var polycentricLocalCache: Boolean = true;
} }
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19) @FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
@@ -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
@@ -834,7 +838,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) { } else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found playlist client"); Logger.i(TAG, "handleUrl(url=$url) found playlist client");
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
navigate(_fragMainPlaylist, url); navigate(_fragMainRemotePlaylist, url);
delay(100); delay(100);
_fragVideoDetail.minimizeVideoDetail(); _fragVideoDetail.minimizeVideoDetail();
}; };
@@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.ReusablePager import com.futo.platformplayer.api.media.structures.ReusablePager
import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Playlist
import java.util.UUID
class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails { class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
override val contents: IPager<IPlatformVideo>; override val contents: IPager<IPlatformVideo>;
@@ -37,6 +38,6 @@ class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
onProgress?.invoke(videos.size); onProgress?.invoke(videos.size);
} }
return Playlist(id.toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)}); return Playlist(UUID.randomUUID().toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)});
} }
} }
@@ -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);
@@ -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<*>) {
@@ -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>
) { ) {
@@ -90,7 +90,7 @@ class BuyFragment : MainFragment() {
try { try {
val currencies = StatePayment.instance.getAvailableCurrencies("grayjay"); val currencies = StatePayment.instance.getAvailableCurrencies("grayjay");
val prices = StatePayment.instance.getAvailableCurrencyPrices("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)) } }; val currency = country?.let { c -> PaymentConfigurations.CURRENCIES.find { it.id == c.defaultCurrencyId && (currencies.contains(it.id)) } };
if(currency != null && prices.containsKey(currency.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.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
} }
@@ -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);
@@ -25,10 +25,12 @@ import com.futo.platformplayer.views.others.ProgressBar
import com.futo.platformplayer.views.others.TagsView import com.futo.platformplayer.views.others.TagsView
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder import com.futo.platformplayer.views.adapters.InsertedViewHolder
import com.futo.platformplayer.views.announcements.AnnouncementView
import kotlinx.coroutines.CancellationException 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;
@@ -37,6 +39,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
private val _progressBar: ProgressBar; private val _progressBar: ProgressBar;
private val _spinnerSortBy: Spinner; private val _spinnerSortBy: Spinner;
private val _containerSortBy: LinearLayout; private val _containerSortBy: LinearLayout;
private val _announcementView: AnnouncementView;
private val _tagsView: TagsView; private val _tagsView: TagsView;
private val _textCentered: TextView; private val _textCentered: TextView;
private val _emptyPagerContainer: FrameLayout; private val _emptyPagerContainer: FrameLayout;
@@ -73,6 +76,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
_textCentered = findViewById(R.id.text_centered); _textCentered = findViewById(R.id.text_centered);
_emptyPagerContainer = findViewById(R.id.empty_pager_container); _emptyPagerContainer = findViewById(R.id.empty_pager_container);
_progressBar = findViewById(R.id.progress_bar); _progressBar = findViewById(R.id.progress_bar);
_announcementView = findViewById(R.id.announcement_view)
_progressBar.inactiveColor = Color.TRANSPARENT; _progressBar.inactiveColor = Color.TRANSPARENT;
_swipeRefresh = findViewById(R.id.swipe_refresh); _swipeRefresh = findViewById(R.id.swipe_refresh);
@@ -172,6 +176,10 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
_recyclerResults.addOnScrollListener(_scrollListener); _recyclerResults.addOnScrollListener(_scrollListener);
} }
protected fun showAnnouncementView() {
_announcementView.visibility = View.VISIBLE
}
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) { private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
val canScroll = if (recyclerData.results.isEmpty()) false else { val canScroll = if (recyclerData.results.isEmpty()) false else {
val layoutManager = recyclerData.layoutManager val layoutManager = recyclerData.layoutManager
@@ -227,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?) {
@@ -94,20 +94,10 @@ class HomeFragment : MainFragment() {
class HomeView : ContentFeedView<HomeFragment> { class HomeView : ContentFeedView<HomeFragment> {
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle(); 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>>; private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar 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) { 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 }, { _taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({ fragment.lifecycleScope }, {
StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope) StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope)
}) })
@@ -138,6 +128,7 @@ class HomeFragment : MainFragment() {
}; };
setPreviewsEnabled(Settings.instance.home.previewFeedItems); setPreviewsEnabled(Settings.instance.home.previewFeedItems);
showAnnouncementView()
} }
fun onShown() { fun onShown() {
@@ -70,7 +70,7 @@ class PlaylistFragment : MainFragment() {
private var _editPlaylistOverlay: SlideUpMenuOverlay? = null; private var _editPlaylistOverlay: SlideUpMenuOverlay? = null;
private var _url: String? = null; private var _url: String? = null;
private val _taskLoadPlaylist: TaskHandler<String, IPlatformPlaylistDetails>; private val _taskLoadPlaylist: TaskHandler<String, Playlist>;
constructor(fragment: PlaylistFragment, inflater: LayoutInflater) : super(inflater) { constructor(fragment: PlaylistFragment, inflater: LayoutInflater) : super(inflater) {
_fragment = fragment; _fragment = fragment;
@@ -137,16 +137,16 @@ class PlaylistFragment : MainFragment() {
); );
}; };
_taskLoadPlaylist = TaskHandler<String, IPlatformPlaylistDetails>( _taskLoadPlaylist = TaskHandler<String, Playlist>(
StateApp.instance.scopeGetter, StateApp.instance.scopeGetter,
{ {
return@TaskHandler StatePlatform.instance.getPlaylist(it); return@TaskHandler StatePlatform.instance.getPlaylist(it).toPlaylist();
}) })
.success { .success {
setName(it.name); setName(it.name);
//TODO: Implement support for pagination //TODO: Implement support for pagination
setVideos(it.toPlaylist().videos, false); setVideos(it.videos, false);
setVideoCount(it.videoCount); setVideoCount(it.videos.size);
setLoading(false); setLoading(false);
} }
.exception<Throwable> { .exception<Throwable> {
@@ -35,7 +35,6 @@ import com.futo.platformplayer.views.ToastView
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder 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.buttons.BigButton
import com.futo.platformplayer.views.subscriptions.SubscriptionBar import com.futo.platformplayer.views.subscriptions.SubscriptionBar
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@@ -125,6 +124,9 @@ class SubscriptionsFeedFragment : MainFragment() {
initializeToolbarContent(); initializeToolbarContent();
setPreviewsEnabled(Settings.instance.subscriptions.previewFeedItems); setPreviewsEnabled(Settings.instance.subscriptions.previewFeedItems);
if (Settings.instance.tabs.find { it.id == 0 }?.enabled != true) {
showAnnouncementView()
}
} }
fun onShown() { 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) { if (!StateSubscriptions.instance.global.isGlobalUpdating) {
finishRefreshLayoutLoader(); finishRefreshLayoutLoader();
} }
@@ -192,8 +174,6 @@ class SubscriptionsFeedFragment : MainFragment() {
private var _subscriptionBar: SubscriptionBar? = null; private var _subscriptionBar: SubscriptionBar? = null;
private var _announcementsView: AnnouncementView? = null;
@Serializable @Serializable
class FeedFilterSettings: FragmentedStorageFileJson() { class FeedFilterSettings: FragmentedStorageFileJson() {
val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST); val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST);
@@ -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) {
@@ -143,7 +143,6 @@ class VideoDetailFragment : MainFragment {
} }
} }
@SuppressLint("SourceLockedOrientationActivity")
fun updateOrientation() { fun updateOrientation() {
val a = activity ?: return val a = activity ?: return
val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait
@@ -156,7 +155,11 @@ class VideoDetailFragment : MainFragment {
// For small windows if the device isn't landscape right now and full screen portrait isn't allowed then we should force landscape // 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) { if (isSmallWindow && isFullscreen && !isFullScreenPortraitAllowed && resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT && !rotationLock && isLandscapeVideo) {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE if(Settings.instance.playback.forceAllowFullScreenRotation){
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
}else{
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}
} }
// For small windows if the device isn't in a portrait orientation and we're in the maximized state then we should force portrait // 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 && resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
@@ -164,26 +167,10 @@ class VideoDetailFragment : MainFragment {
} else if (rotationLock) { } else if (rotationLock) {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
} else { } else {
when (Settings.instance.playback.autoRotate) { a.requestedOrientation = if (isReversePortraitAllowed) {
0 -> { ActivityInfo.SCREEN_ORIENTATION_FULL_USER
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED } else {
} ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
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
}
}
} }
} }
} }
@@ -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
} }
} }
@@ -55,21 +55,25 @@ class ServiceRecordAggregator {
if (_cts != null) throw Exception("Already started.") if (_cts != null) throw Exception("Already started.")
_cts = CoroutineScope(Dispatchers.Default).launch { _cts = CoroutineScope(Dispatchers.Default).launch {
while (isActive) { try {
val now = Date() while (isActive) {
synchronized(_currentServices) { val now = Date()
_cachedAddressRecords.forEach { it.value.removeAll { record -> now.after(record.expirationTime) } } synchronized(_currentServices) {
_cachedTxtRecords.entries.removeIf { now.after(it.value.expirationTime) } _cachedAddressRecords.forEach { it.value.removeAll { record -> now.after(record.expirationTime) } }
_cachedSrvRecords.entries.removeIf { now.after(it.value.expirationTime) } _cachedTxtRecords.entries.removeIf { now.after(it.value.expirationTime) }
_cachedPtrRecords.forEach { it.value.removeAll { record -> now.after(record.expirationTime) } } _cachedSrvRecords.entries.removeIf { now.after(it.value.expirationTime) }
_cachedPtrRecords.forEach { it.value.removeAll { record -> now.after(record.expirationTime) } }
val newServices = getCurrentServices() val newServices = getCurrentServices()
_currentServices.clear() _currentServices.clear()
_currentServices.addAll(newServices) _currentServices.addAll(newServices)
}
onServicesUpdated?.invoke(_currentServices.toList())
delay(5000)
} }
} catch (e: Throwable) {
onServicesUpdated?.invoke(_currentServices.toList()) Logger.e(TAG, "Unexpected failure in MDNS loop", e)
delay(5000)
} }
} }
} }
@@ -83,6 +87,7 @@ class ServiceRecordAggregator {
} }
fun add(packet: DnsPacket) { fun add(packet: DnsPacket) {
val currentServices: List<DnsService>
val dnsResourceRecords = packet.answers + packet.additionals + packet.authorities 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 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() } 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}") } 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")*/ 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) { 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() currentServices = getCurrentServices()
this._currentServices.clear() this._currentServices.clear()
this._currentServices.addAll(currentServices) this._currentServices.addAll(currentServices)
@@ -12,70 +12,113 @@ 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 && isTouchInside(widget, event)) {
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 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 { 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() {
@@ -47,6 +47,7 @@ import com.futo.platformplayer.services.DownloadService
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.views.ToastView import com.futo.platformplayer.views.ToastView
import com.futo.polycentric.core.ApiMethods
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.util.* import java.util.*
@@ -156,12 +157,9 @@ class StateApp {
//Files //Files
private var _tempDirectory: File? = null; private var _tempDirectory: File? = null;
private var _cacheDirectory: File? = null;
private var _persistentDirectory: File? = null; private var _persistentDirectory: File? = null;
//AutoRotate
var systemAutoRotate: Boolean = false;
//Network //Network
private var _lastMeteredState: Boolean = false; private var _lastMeteredState: Boolean = false;
private var _connectivityManager: ConnectivityManager? = null; private var _connectivityManager: ConnectivityManager? = null;
@@ -199,17 +197,6 @@ class StateApp {
return File(_persistentDirectory, name); 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 { fun isCurrentMetered(): Boolean {
ensureConnectivityManager(); ensureConnectivityManager();
return _connectivityManager?.isActiveNetworkMetered ?: throw IllegalStateException("Connectivity manager not available"); return _connectivityManager?.isActiveNetworkMetered ?: throw IllegalStateException("Connectivity manager not available");
@@ -310,9 +297,6 @@ class StateApp {
fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null) { fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null) {
_context = context; _context = context;
_scope = coroutineScope _scope = coroutineScope
//System checks
systemAutoRotate = getCurrentSystemAutoRotate();
} }
fun initializeFiles(force: Boolean = false) { fun initializeFiles(force: Boolean = false) {
@@ -324,6 +308,9 @@ class StateApp {
_tempDirectory?.deleteRecursively(); _tempDirectory?.deleteRecursively();
} }
_tempDirectory?.mkdirs(); _tempDirectory?.mkdirs();
_cacheDirectory = File(context.filesDir, "cache");
if(_cacheDirectory?.exists() == false)
_cacheDirectory?.mkdirs();
_persistentDirectory = File(context.filesDir, "persist"); _persistentDirectory = File(context.filesDir, "persist");
if(_persistentDirectory?.exists() == false) { if(_persistentDirectory?.exists() == false) {
_persistentDirectory?.mkdirs(); _persistentDirectory?.mkdirs();
@@ -383,6 +370,11 @@ class StateApp {
Logger.i(TAG, "MainApp Starting"); Logger.i(TAG, "MainApp Starting");
initializeFiles(true); initializeFiles(true);
if(Settings.instance.other.polycentricLocalCache) {
Logger.i(TAG, "Initialize Polycentric Disk Cache")
_cacheDirectory?.let { ApiMethods.initCache(it) };
}
val logFile = File(context.filesDir, "log.txt"); val logFile = File(context.filesDir, "log.txt");
if (Settings.instance.logging.logLevel > LogLevel.NONE.value) { if (Settings.instance.logging.logLevel > LogLevel.NONE.value) {
val fileLogConsumer = FileLogConsumer(logFile, LogLevel.fromInt(Settings.instance.logging.logLevel), false); val fileLogConsumer = FileLogConsumer(logFile, LogLevel.fromInt(Settings.instance.logging.logLevel), false);
@@ -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() }
@@ -24,7 +24,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class AnnouncementView : LinearLayout { class AnnouncementView : LinearLayout {
private val _root: ConstraintLayout; private val _root: FrameLayout;
private val _textTitle: TextView; private val _textTitle: TextView;
private val _textCounter: TextView; private val _textCounter: TextView;
private val _textBody: TextView; private val _textBody: TextView;
@@ -45,9 +45,6 @@ class AnnouncementView : LinearLayout {
_scope = findViewTreeLifecycleOwner()?.lifecycleScope ?: StateApp.instance.scopeOrNull; //TODO: Fetch correct scope _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); _root = findViewById(R.id.root);
_textTitle = findViewById(R.id.text_title); _textTitle = findViewById(R.id.text_title);
_textCounter = findViewById(R.id.text_counter); _textCounter = findViewById(R.id.text_counter);
@@ -115,12 +112,12 @@ class AnnouncementView : LinearLayout {
_currentAnnouncement = announcement; _currentAnnouncement = announcement;
if (announcement == null) { if (announcement == null) {
visibility = View.GONE _root.visibility = View.GONE
onClose.emit() onClose.emit()
return; return;
} }
visibility = View.VISIBLE _root.visibility = View.VISIBLE
_textTitle.text = announcement.title; _textTitle.text = announcement.title;
_textBody.text = announcement.msg; _textBody.text = announcement.msg;
@@ -16,73 +16,117 @@ 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 && 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 return false
} }
private fun isTouchInside(event: MotionEvent): Boolean {
return event.x >= 0 && event.x <= width && event.y >= 0 && event.y <= height
}
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) {
@@ -808,17 +813,12 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
} }
fun updateRotateLock() { fun updateRotateLock() {
if(Settings.instance.playback.autoRotate == 0) { _control_rotate_lock.visibility = View.VISIBLE;
_control_rotate_lock.visibility = View.GONE; _control_rotate_lock_fullscreen.visibility = View.VISIBLE;
_control_rotate_lock_fullscreen.visibility = View.GONE;
}
else {
_control_rotate_lock.visibility = View.VISIBLE;
_control_rotate_lock_fullscreen.visibility = View.VISIBLE;
}
if(StatePlayer.instance.rotationLock) { if(StatePlayer.instance.rotationLock) {
_control_rotate_lock_fullscreen.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_rotation); _control_rotate_lock.setImageResource(R.drawable.ic_screen_lock_rotation_active);
} }
else { else {
_control_rotate_lock_fullscreen.setImageResource(R.drawable.ic_screen_lock_rotation); _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:layout_height="wrap_content"
android:orientation="vertical"> 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 <LinearLayout
android:id="@+id/container_sort_by" android:id="@+id/container_sort_by"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -110,7 +116,8 @@
android:visibility="gone" android:visibility="gone"
android:id="@+id/empty_pager_container" android:id="@+id/empty_pager_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="wrap_content"
android:layout_marginTop="30dp" />
</FrameLayout> </FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
+87 -84
View File
@@ -1,119 +1,122 @@
<?xml version="1.0" encoding="utf-8"?> <?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:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/root">
android:orientation="vertical"
android:id="@+id/root"
android:background="@drawable/background_16_round_4dp"
android:paddingLeft="10dp"
android:paddingTop="10dp"
android:paddingRight="10dp">
<TextView android:id="@+id/text_title" <androidx.constraintlayout.widget.ConstraintLayout
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
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/text_body" android:background="@drawable/background_16_round_4dp"
app:layout_constraintRight_toRightOf="parent" android:paddingLeft="10dp"
android:paddingTop="4dp" android:paddingTop="10dp"
android:paddingBottom="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_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="2022-03-01" tools:text="1/4"
android:fontFamily="@font/inter_regular" android:fontFamily="@font/inter_regular"
android:textSize="12dp" android:textSize="12dp"
android:textColor="#585656" android:textColor="#585656"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent" /> app:layout_constraintRight_toRightOf="parent" />
<Space android:layout_width="0dp" <TextView android:id="@+id/text_body"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView android:id="@+id/text_never"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/never" 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_regular" android:fontFamily="@font/inter_light"
android:textSize="14sp" android:textSize="14sp"
android:textColor="@color/colorPrimary" android:textColor="#9D9D9D"
android:paddingTop="10dp" app:layout_constraintLeft_toLeftOf="parent"
android:paddingBottom="10dp" app:layout_constraintTop_toBottomOf="@id/text_title"/>
android:paddingLeft="20dp"
android:paddingRight="20dp"
app:layout_constraintTop_toBottomOf="@id/text_body"
app:layout_constraintRight_toLeftOf="@id/text_close"/>
<TextView android:id="@+id/text_close" <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="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_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" <TextView android:id="@+id/text_time"
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_width="wrap_content"
android:layout_height="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:fontFamily="@font/inter_regular"
android:textSize="14sp" android:textSize="14sp"
android:textColor="@color/white" android:textColor="@color/colorPrimary"
android:paddingTop="10dp" android:paddingTop="10dp"
android:paddingBottom="10dp" android:paddingBottom="10dp"
android:paddingLeft="20dp" android:paddingLeft="20dp"
android:paddingRight="20dp" android:paddingRight="20dp"
app:layout_constraintTop_toBottomOf="@id/text_body" app:layout_constraintTop_toBottomOf="@id/text_body"
app:layout_constraintRight_toLeftOf="@id/text_close"/> 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>
+1 -1
View File
@@ -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>
+5 -13
View File
@@ -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>
@@ -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="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="attempt_to_utilize_byte_ranges">Attempt to utilize byte ranges</string>
<string name="auto_update">Auto Update</string> <string name="auto_update">Auto Update</string>
<string name="auto_rotate">Auto-Rotate</string> <string name="force_allow_full_screen_rotation">Force Allow Full Screen Rotation</string>
<string name="force_allow_full_screen_rotation_description">Allow auto-rotation between the two landscape orientations even when you disable auto-rotate at the system level.</string>
<string name="simplify_sources">Simplify sources</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="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="automatic_backup">Automatic Backup</string>
<string name="background_behavior">Background Behavior</string> <string name="background_behavior">Background Behavior</string>
<string name="background_update">Background Update</string> <string name="background_update">Background Update</string>
@@ -424,6 +425,8 @@
<string name="playlist_delete_confirmation">Playlist Delete Confirmation</string> <string name="playlist_delete_confirmation">Playlist Delete Confirmation</string>
<string name="playlist_delete_confirmation_description">Show confirmation dialog when deleting media from a playlist</string> <string name="playlist_delete_confirmation_description">Show confirmation dialog when deleting media from a playlist</string>
<string name="enable_polycentric">Enable Polycentric</string> <string name="enable_polycentric">Enable Polycentric</string>
<string name="polycentric_local_cache">Enable Polycentric Local Caching</string>
<string name="polycentric_local_cache_description">Caches polycentric results on-device to reduce load times, changing requires app reboot</string>
<string name="can_be_disabled_when_you_are_experiencing_issues">Can be disabled when you are experiencing issues</string> <string name="can_be_disabled_when_you_are_experiencing_issues">Can be disabled when you are experiencing issues</string>
<string name="bypass_rotation_prevention_description">Allows for rotation on non-video views.\nWARNING: Not designed for it</string> <string name="bypass_rotation_prevention_description">Allows for rotation on non-video views.\nWARNING: Not designed for it</string>
<string name="bypass_rotation_prevention_warning">This may cause unexpected behavior, and is mostly untested.</string> <string name="bypass_rotation_prevention_warning">This may cause unexpected behavior, and is mostly untested.</string>
@@ -925,17 +928,6 @@
<item>On Startup</item> <item>On Startup</item>
<item>Never</item> <item>Never</item>
</string-array> </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"> <string-array name="enabled_disabled_array">
<item>Disabled</item> <item>Disabled</item>
<item>Enabled</item> <item>Enabled</item>
+1
View File
@@ -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