Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay

This commit is contained in:
Koen 2023-12-20 10:56:28 +01:00
commit c1967556ac
14 changed files with 155 additions and 18 deletions

View file

@ -37,7 +37,8 @@ let Type = {
NORMAL: 0, NORMAL: 0,
SKIPPABLE: 5, SKIPPABLE: 5,
SKIP: 6 SKIP: 6,
SKIPONCE: 7
} }
}; };

View file

@ -277,7 +277,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9) @FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
var fetchOnTabOpen: Boolean = true; var fetchOnTabOpen: Boolean = true;
@FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 10) @FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 10, "background_update")
@DropdownFieldOptionsId(R.array.background_interval) @DropdownFieldOptionsId(R.array.background_interval)
var subscriptionsBackgroundUpdateInterval: Int = 0; var subscriptionsBackgroundUpdateInterval: Int = 0;

View file

@ -1,9 +1,14 @@
package com.futo.platformplayer package com.futo.platformplayer
import android.app.Activity
import android.app.NotificationManager
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
@ -131,8 +136,29 @@ class UISlideOverlays {
subscription.save(); subscription.save();
menu.hide(true); menu.hide(true);
if(subscription.doNotifications && !originalNotif && Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) { if(subscription.doNotifications && !originalNotif) {
val mainContext = StateApp.instance.contextOrNull;
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval == 0) {
UIDialogs.toast(container.context, "Enable 'Background Update' in settings for notifications to work"); UIDialogs.toast(container.context, "Enable 'Background Update' in settings for notifications to work");
if(mainContext is MainActivity) {
UIDialogs.showDialog(mainContext, R.drawable.ic_settings, "Background Updating Required",
"You need to set a Background Updating interval for notifications", null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Configure", {
val intent = Intent(mainContext, SettingsActivity::class.java);
intent.putExtra("query", mainContext.getString(R.string.background_update));
mainContext.startActivity(intent);
}, UIDialogs.ActionStyle.PRIMARY));
}
return@subscribe;
}
else if(!(mainContext?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled()) {
UIDialogs.toast(container.context, "Android notifications are disabled");
if(mainContext is MainActivity) {
mainContext.requestNotificationPermissions("Notifications are required for subscription updating and notifications to work");
}
}
} }
}; };
menu.onCancel.subscribe { menu.onCancel.subscribe {

View file

@ -1,10 +1,12 @@
package com.futo.platformplayer.activities package com.futo.platformplayer.activities
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationManager
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.ActivityInfo import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
@ -17,6 +19,8 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.motion.widget.MotionLayout import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
@ -1022,6 +1026,33 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
val notifPermission = "android.permission.POST_NOTIFICATIONS";
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted)
UIDialogs.toast(this, "Notification permission granted");
else
UIDialogs.toast(this, "Notification permission denied");
}
fun requestNotificationPermissions(reason: String) {
when {
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
}
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
reason, null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Enable", {
requestPermissionLauncher.launch(notifPermission);
}, UIDialogs.ActionStyle.PRIMARY));
}
else -> {
requestPermissionLauncher.launch(notifPermission);
}
}
}
//TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers. //TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers.
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>(); private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();

View file

@ -1,8 +1,10 @@
package com.futo.platformplayer.activities package com.futo.platformplayer.activities
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
@ -12,6 +14,8 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@ -33,6 +37,14 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
lateinit var overlay: FrameLayout; lateinit var overlay: FrameLayout;
val notifPermission = "android.permission.POST_NOTIFICATIONS";
val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted)
UIDialogs.toast(this, "Notification permission granted");
else
UIDialogs.toast(this, "Notification permission denied");
}
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext") Logger.i("SettingsActivity", "SettingsActivity.attachBaseContext")
super.attachBaseContext(StateApp.instance.getLocaleContext(newBase)) super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
@ -58,6 +70,33 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences"); Logger.i("SettingsActivity", "App language change detected, propogating to shared preferences");
StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString()); StateApp.instance.setLocaleSetting(this, Settings.instance.language.getAppLanguageLocaleString());
} }
if(field.descriptor?.id == "background_update") {
Logger.i("SettingsActivity", "Detected change in background work ${field.value}");
if(Settings.instance.subscriptions.subscriptionsBackgroundUpdateInterval > 0) {
val notifManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
if(!notifManager.areNotificationsEnabled()) {
UIDialogs.toast(this, "Notifications aren't enabled");
when {
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
}
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
"Notifications need to be enabled for background updating to function", null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Enable", {
requestPermissionLauncher.launch(notifPermission);
}, UIDialogs.ActionStyle.PRIMARY));
}
else -> {
requestPermissionLauncher.launch(notifPermission);
}
}
}
}
}
}; };
_buttonBack.setOnClickListener { _buttonBack.setOnClickListener {
finish(); finish();
@ -72,7 +111,10 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
reloadSettings(); reloadSettings();
} }
var isFirstLoad = true;
fun reloadSettings() { fun reloadSettings() {
val firstLoad = isFirstLoad;
isFirstLoad = false;
_form.setSearchVisible(false); _form.setSearchVisible(false);
_loaderView.start(); _loaderView.start();
_form.fromObject(lifecycleScope, Settings.instance) { _form.fromObject(lifecycleScope, Settings.instance) {
@ -90,6 +132,13 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode)); UIDialogs.toast(this, getString(R.string.you_are_now_in_developer_mode));
} }
}; };
if(firstLoad) {
val query = intent.getStringExtra("query");
if(!query.isNullOrEmpty()) {
_form.setSearchQuery(query);
}
}
}; };
} }
@ -135,6 +184,7 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
resultLauncher.launch(intent); resultLauncher.launch(intent);
} }
companion object { companion object {
//TODO: Temporary for solving Settings issues //TODO: Temporary for solving Settings issues
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")

View file

@ -14,7 +14,8 @@ enum class ChapterType(val value: Int) {
NORMAL(0), NORMAL(0),
SKIPPABLE(5), SKIPPABLE(5),
SKIP(6); SKIP(6),
SKIPONCE(7);

View file

@ -104,7 +104,8 @@ class SubscriptionsFeedFragment : MainFragment() {
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) { constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
Logger.i(TAG, "SubscriptionsFeedFragment constructor()"); Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
StateSubscriptions.instance.global.onUpdateProgress.subscribe(this) { progress, total -> StateSubscriptions.instance.onFeedProgress.subscribe(this) { id, progress, total ->
if(_subGroup?.id == id)
fragment.lifecycleScope.launch(Dispatchers.Main) { fragment.lifecycleScope.launch(Dispatchers.Main) {
try { try {
setProgress(progress, total); setProgress(progress, total);
@ -112,6 +113,8 @@ class SubscriptionsFeedFragment : MainFragment() {
Logger.e(TAG, "Failed to set progress", e); Logger.e(TAG, "Failed to set progress", e);
} }
} }
}
StateSubscriptions.instance.global.onUpdateProgress.subscribe(this) { progress, total ->
}; };
StateSubscriptions.instance.onSubscriptionsChanged.subscribe(this) { _, added -> StateSubscriptions.instance.onSubscriptionsChanged.subscribe(this) { _, added ->
@ -264,7 +267,10 @@ class SubscriptionsFeedFragment : MainFragment() {
UISlideOverlays.showCreateSubscriptionGroup(_overlayContainer); UISlideOverlays.showCreateSubscriptionGroup(_overlayContainer);
else { else {
_subGroup = g; _subGroup = g;
loadCache(); //TODO: Proper subset update setProgress(0, 0);
if(Settings.instance.subscriptions.fetchOnTabOpen)
loadResults(false);
else loadCache();
} }
}; };
_subscriptionBar?.onHoldGroup?.subscribe { g -> _subscriptionBar?.onHoldGroup?.subscribe { g ->

View file

@ -469,7 +469,7 @@ class VideoDetailView : ConstraintLayout {
if(!isScrub) { if(!isScrub) {
if(chapter?.type == ChapterType.SKIPPABLE) { if(chapter?.type == ChapterType.SKIPPABLE) {
_layoutSkip.visibility = VISIBLE; _layoutSkip.visibility = VISIBLE;
} else if(chapter?.type == ChapterType.SKIP) { } else if(chapter?.type == ChapterType.SKIP || chapter?.type == ChapterType.SKIPONCE) {
val ad = StateCasting.instance.activeDevice val ad = StateCasting.instance.activeDevice
if (ad != null) { if (ad != null) {
ad.seekVideo(chapter.timeEnd) ad.seekVideo(chapter.timeEnd)

View file

@ -9,6 +9,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.structures.* import com.futo.platformplayer.api.media.structures.*
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.constructs.Event3
import com.futo.platformplayer.functional.CentralizedFeed import com.futo.platformplayer.functional.CentralizedFeed
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
@ -50,7 +51,7 @@ class StateSubscriptions {
val global: CentralizedFeed = CentralizedFeed(); val global: CentralizedFeed = CentralizedFeed();
val feeds: HashMap<String, CentralizedFeed> = hashMapOf(); val feeds: HashMap<String, CentralizedFeed> = hashMapOf();
val onFeedProgress = Event3<String, Int, Int>();
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>(); val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
@ -70,6 +71,9 @@ class StateSubscriptions {
var f = feeds[id]; var f = feeds[id];
if(f == null && createIfNew) { if(f == null && createIfNew) {
f = CentralizedFeed(); f = CentralizedFeed();
f.onUpdateProgress.subscribe { progress, total ->
onFeedProgress.emit(id, progress, total)
};
feeds[id] = f; feeds[id] = f;
} }
return@synchronized f; return@synchronized f;

View file

@ -71,6 +71,10 @@ class FieldForm : LinearLayout {
} }
} }
fun setSearchQuery(query: String) {
_editSearch.setText(query);
updateSettingsVisibility();
}
fun setSearchVisible(visible: Boolean) { fun setSearchVisible(visible: Boolean) {
_containerSearch.visibility = if(visible) View.VISIBLE else View.GONE; _containerSearch.visibility = if(visible) View.VISIBLE else View.GONE;
_editSearch.setText(""); _editSearch.setText("");

View file

@ -27,6 +27,7 @@ import androidx.media3.ui.TimeBar
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.chapters.ChapterType
import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
@ -471,6 +472,10 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_control_chapter_fullscreen.text = ""; _control_chapter_fullscreen.text = "";
} }
onChapterChanged.emit(currentChapter, isScrub); onChapterChanged.emit(currentChapter, isScrub);
val chapt = _currentChapter;
if(chapt?.type == ChapterType.SKIPONCE)
ignoreChapter(chapt);
} }
return true; return true;
} }

View file

@ -72,6 +72,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
private val _referenceObject = Object(); private val _referenceObject = Object();
private var _connectivityLossTime_ms: Long? = null private var _connectivityLossTime_ms: Long? = null
private var _ignoredChapters: ArrayList<IChapter> = arrayListOf();
private var _chapters: List<IChapter>? = null; private var _chapters: List<IChapter>? = null;
var exoPlayer: PlayerManager? = null var exoPlayer: PlayerManager? = null
@ -273,13 +274,21 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
fun setChapters(chapters: List<IChapter>?) { fun setChapters(chapters: List<IChapter>?) {
_ignoredChapters = arrayListOf();
_chapters = chapters; _chapters = chapters;
} }
fun getChapters(): List<IChapter> { fun getChapters(): List<IChapter> {
return _chapters?.let { it.toList() } ?: listOf(); return _chapters?.let { it.toList() } ?: listOf();
} }
fun ignoreChapter(chapter: IChapter) {
synchronized(_ignoredChapters) {
if(!_ignoredChapters.contains(chapter))
_ignoredChapters.add(chapter);
}
}
fun getCurrentChapter(pos: Long): IChapter? { fun getCurrentChapter(pos: Long): IChapter? {
return _chapters?.let { chaps -> chaps.find { pos.toDouble() / 1000 > it.timeStart && pos.toDouble() / 1000 < it.timeEnd } }; val toIgnore = synchronized(_ignoredChapters){ _ignoredChapters.toList() };
return _chapters?.let { chaps -> chaps.find { pos.toDouble() / 1000 > it.timeStart && pos.toDouble() / 1000 < it.timeEnd && (toIgnore.isEmpty() || !toIgnore.contains(it)) } };
} }
fun setSource(videoSource: IVideoSource?, audioSource: IAudioSource? = null, play: Boolean = false, keepSubtitles: Boolean = false) { fun setSource(videoSource: IVideoSource?, audioSource: IAudioSource? = null, play: Boolean = false, keepSubtitles: Boolean = false) {

@ -1 +1 @@
Subproject commit 863d0be1322660c99e4d0cdae0b45d0a5918542d Subproject commit 01270edbb4b6b4fb004e22fc529bf787c7f5be81

@ -1 +1 @@
Subproject commit d41cc8e848891ef8e949e6d49384b754e7c305c7 Subproject commit 13551ab67fc8fb1899b5bcbfdec750855b154790