Compare commits

...

2 Commits

Author SHA1 Message Date
Kelvin 8536861e09 Update dialogs 2026-01-05 23:56:53 +01:00
Kelvin 71262da3c2 New notification ui 2026-01-02 20:38:43 +01:00
18 changed files with 794 additions and 28 deletions
@@ -7,6 +7,10 @@ import android.os.IBinder
import android.os.SystemClock
import com.futo.platformplayer.UIDialogs.ActionStyle
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.SessionAnnouncement
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateUpdate
import kotlinx.coroutines.*
@@ -14,6 +18,7 @@ import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.time.OffsetDateTime
class UpdateDownloadService : Service() {
@@ -85,13 +90,16 @@ class UpdateDownloadService : Service() {
job.cancel()
}
private fun throttledUpdateDownloadProgress(version: Int, progress: Int, indeterminate: Boolean) {
private fun throttledUpdateDownloadProgress(version: Int, progress: Int, indeterminate: Boolean, onProgress: ((Int) -> Unit)? = null) {
val now = SystemClock.elapsedRealtime()
val force = progress == 100 && !indeterminate
if (force || now - lastProgressUpdateElapsedMs >= MIN_PROGRESS_UPDATE_INTERVAL_MS) {
lastProgressUpdateElapsedMs = now
UpdateNotificationManager.updateDownloadProgress(this, version, progress, indeterminate)
UpdateNotificationManager.updateDownloadProgress(this, version, progress, indeterminate);
if(onProgress != null)
onProgress.invoke(progress);
}
}
@@ -99,6 +107,7 @@ class UpdateDownloadService : Service() {
val apkFile = StateUpdate.getApkFile(this, version)
val partialFile = StateUpdate.getPartialApkFile(this, version)
var announcement: SessionAnnouncement? = null;
try {
if (apkFile.exists() && apkFile.length() > 0L) {
Logger.i(TAG, "APK already downloaded at ${apkFile.absolutePath}")
@@ -106,6 +115,14 @@ class UpdateDownloadService : Service() {
return
}
try {
announcement = StateAnnouncement.instance.registerLoading("Downloading new version [${version}]", "New version is being downloaded..",
ImageVariable.fromResource(R.drawable.foreground));
}
catch(ex: Exception){
Logger.e(TAG, "Failed to set progress announcement", ex);
}
var backoffMs = INITIAL_BACKOFF_MS
for (attempt in 0 until MAX_RETRIES) {
@@ -115,7 +132,13 @@ class UpdateDownloadService : Service() {
}
try {
performDownload(StateUpdate.APK_URL, partialFile, version)
performDownload(StateUpdate.APK_URL, partialFile, version, {
try {
if (announcement != null)
announcement?.setProgress(it);
}
catch(ex: Throwable) {}
})
if (!cancelRequested) {
if (apkFile.exists()) {
@@ -145,6 +168,12 @@ class UpdateDownloadService : Service() {
}
}
} finally {
try {
if (announcement != null) {
StateAnnouncement.instance.closeAnnouncement(announcement.id);
}
}
catch(ex: Throwable){}
isDownloading = false
cancelRequested = false
stopForeground(Service.STOP_FOREGROUND_REMOVE)
@@ -152,7 +181,7 @@ class UpdateDownloadService : Service() {
}
}
private fun performDownload(url: String, partialFile: File, version: Int) {
private fun performDownload(url: String, partialFile: File, version: Int, onProgress: ((Int)->Unit)? = null) {
var startOffset = if (partialFile.exists()) partialFile.length() else 0L
Logger.i(TAG, "Starting download. url=$url, existingBytes=$startOffset")
@@ -204,7 +233,7 @@ class UpdateDownloadService : Service() {
progress > 100 -> 100
else -> progress
}
throttledUpdateDownloadProgress(version, safeProgress, indeterminate = false)
throttledUpdateDownloadProgress(version, safeProgress, indeterminate = false, onProgress)
}
} else {
throttledUpdateDownloadProgress(version, progress = 0, indeterminate = true)
@@ -250,6 +279,18 @@ class UpdateDownloadService : Service() {
UpdateNotificationManager.cancelAll(ctx)
UpdateInstaller.startInstall(ctx, version, apkFile)
}, ActionStyle.PRIMARY, true));
try {
StateAnnouncement.instance.registerAnnouncement("install-update-apk", "Grayjay v${version} is ready!", "You can now install the new Grayjay version.",
AnnouncementType.SESSION,
OffsetDateTime.now(), "update", "Install", {
UpdateNotificationManager.cancelAll(ctx)
UpdateInstaller.startInstall(ctx, version, apkFile)
});
}
catch(ex: Throwable) {
}
} catch (t: Throwable) {
Logger.w(TAG, "Failed to show in-app update downloaded dialog", t)
updateDownloadedDialog = null
@@ -110,6 +110,7 @@ import com.futo.platformplayer.stores.StringStorage
import com.futo.platformplayer.stores.SubscriptionStorage
import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.views.ToastView
import com.futo.platformplayer.views.notification.NotificationOverlayView
import com.futo.polycentric.core.ApiMethods
import com.google.gson.JsonParser
import com.google.zxing.integration.android.IntentIntegrator
@@ -201,6 +202,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragLibraryVideos: LibraryVideosFragment;
lateinit var _fragLibrarySearch: LibrarySearchFragment;
lateinit var _fragLibraryFiles: LibraryFilesFragment;
lateinit var _fragNotifications: NotificationOverlayView.Frag;
lateinit var _fragSettings: SettingsFragment;
lateinit var _fragDeveloper: DeveloperFragment;
lateinit var _fragLogin: LoginFragment;
@@ -389,6 +391,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragLibraryVideos = LibraryVideosFragment.newInstance();
_fragLibraryFiles = LibraryFilesFragment.newInstance();
_fragLibrarySearch = LibrarySearchFragment.newInstance();
_fragNotifications = NotificationOverlayView.Frag();
_fragSettings = SettingsFragment.newInstance();
_fragDeveloper = DeveloperFragment.newInstance();
_fragLogin = LoginFragment.newInstance();
@@ -538,6 +541,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragLibrarySearch.topBar = _fragTopBarSearch;
_fragSettings.topBar = _fragTopBarNavigation;
_fragDeveloper.topBar = _fragTopBarNavigation;
_fragNotifications.topBar = _fragTopBarGeneral;
_fragBrowser.topBar = _fragTopBarNavigation;
@@ -1368,6 +1372,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
LibraryVideosFragment::class -> _fragLibraryVideos as T;
LibraryFilesFragment::class -> _fragLibraryFiles as T;
LibrarySearchFragment::class -> _fragLibrarySearch as T;
NotificationOverlayView.Frag::class -> _fragNotifications as T;
SettingsFragment:: class -> _fragSettings as T;
DeveloperFragment::class -> _fragDeveloper as T;
LoginFragment::class -> _fragLogin as T;
@@ -47,7 +47,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
private val _progressBar: ProgressBar;
private val _spinnerSortBy: Spinner;
private val _containerSortBy: LinearLayout;
private val _announcementView: AnnouncementView;
//private val _announcementView: AnnouncementView;
private val _tagsView: TagsView;
private val _textCentered: TextView;
private val _emptyPagerContainer: FrameLayout;
@@ -87,7 +87,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
_textCentered = findViewById(R.id.text_centered);
_emptyPagerContainer = findViewById(R.id.empty_pager_container);
_progressBar = findViewById(R.id.progress_bar);
_announcementView = findViewById(R.id.announcement_view)
//_announcementView = findViewById(R.id.announcement_view)
_progressBar.inactiveColor = Color.TRANSPARENT;
_swipeRefresh = findViewById(R.id.swipe_refresh);
@@ -192,7 +192,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
}
protected fun showAnnouncementView() {
_announcementView.visibility = View.VISIBLE
//_announcementView.visibility = View.VISIBLE
}
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
@@ -7,6 +7,9 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.fragment.mainactivity.main.CreatorsFragment
@@ -17,18 +20,54 @@ import com.futo.platformplayer.fragment.mainactivity.main.PlaylistsFragment
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragmentData
import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.views.casting.CastButton
import com.futo.platformplayer.views.notification.NotificationOverlayView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class GeneralTopBarFragment : TopFragment() {
private var _buttonSearch: ImageButton? = null;
private var _buttonCast: CastButton? = null;
private var _buttonNotifs: ConstraintLayout? = null;
private var _buttonNotifIcon: ImageView? = null;
private var _buttonNotifCount: TextView? = null;
init {
StateAnnouncement.instance.onAnnouncementChanged.subscribe {
lifecycleScope?.launch(Dispatchers.Main) {
updateNotifCount();
}
}
}
fun updateNotifCount() {
val currentAnnouncements = StateAnnouncement.instance.getVisibleAnnouncements();
if(currentAnnouncements.any())
_buttonNotifCount?.let {
it.text = currentAnnouncements.size.toString();
it.visibility = View.VISIBLE;
}
else
_buttonNotifCount?.let {
it.text = currentAnnouncements.size.toString();
it.visibility = View.GONE;
}
}
override fun onShown(parameter: Any?) {
if(currentMain is CreatorsFragment) {
_buttonSearch?.setImageResource(R.drawable.ic_person_search_300w);
} else {
_buttonSearch?.setImageResource(R.drawable.ic_search_300w);
}
if(currentMain is NotificationOverlayView.Frag) {
_buttonNotifIcon?.setImageResource(R.drawable.ic_notifications_filled)
}
else {
_buttonNotifIcon?.setImageResource(R.drawable.ic_notifications)
}
}
override fun onHide() {
@@ -44,6 +83,16 @@ class GeneralTopBarFragment : TopFragment() {
val buttonSearch: ImageButton = view.findViewById(R.id.button_search);
_buttonCast = view.findViewById(R.id.button_cast);
_buttonNotifs = view.findViewById(R.id.button_notifs);
_buttonNotifIcon = view.findViewById(R.id.button_notifs_icon);
_buttonNotifCount = view.findViewById(R.id.button_notifs_count);
updateNotifCount();
_buttonNotifs?.setOnClickListener {
navigate<NotificationOverlayView.Frag>();
}
buttonSearch.setOnClickListener {
if(currentMain is CreatorsFragment) {
navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.CREATOR));
@@ -1,13 +1,22 @@
package com.futo.platformplayer.states
import android.view.View
import android.view.WindowManager
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.dialogs.PluginUpdateDialog
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringHashSetStorage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@@ -110,6 +119,48 @@ class StateAnnouncement {
onAnnouncementChanged.emit();
}
//Special Announcements
fun registerPluginUpdate(oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig): SessionAnnouncement {
val announcement = SessionAnnouncement(
"update-plugin-" + UUID.randomUUID().toString(),
"${newConfig.name} update v${newConfig.version} available!",
"An update is available to upgrade from ${oldConfig.version} to ${newConfig.version}.",
AnnouncementType.SESSION,
null, "updates", "Update", StateAnnouncement.ACTION_UPDATE_PLUGIN,
null, null,oldConfig.id,
newConfig?.absoluteIconUrl?.let { ImageVariable.fromUrl(it) }
).withExtraAction("Changelog", StateAnnouncement.ACTION_CHANGELOG, oldConfig.id);
registerAnnouncementSession(announcement);
return announcement;
}
fun registerPluginUpdated(newConfig: SourcePluginConfig) {
registerAnnouncementSession(SessionAnnouncement(
"updated-plugin-" + UUID.randomUUID().toString(),
"${newConfig.name} updated to v${newConfig.version}!",
"You have succesfully been updater to v${newConfig.version}.",
AnnouncementType.SESSION,
null, "updates", null, null,
null, null,null,
newConfig?.absoluteIconUrl?.let { ImageVariable.fromUrl(it) }
).withExtraAction("Changelog", StateAnnouncement.ACTION_CHANGELOG, newConfig.id));
}
fun registerLoading(title: String, description: String, icon: ImageVariable? = null, customId: String? = null): SessionAnnouncement {
val id = "loading-" + UUID.randomUUID().toString();
val announcement = SessionAnnouncement(
customId ?: id,
title,
description,
AnnouncementType.ONGOING,
null, "loading", null, null,
null, null,null, icon
);
registerAnnouncementSession(announcement);
return announcement;
}
fun getVisibleAnnouncements(category: String? = null): List<Announcement> {
synchronized(_lock) {
if (category != null) {
@@ -122,7 +173,9 @@ class StateAnnouncement {
}
}
fun closeAnnouncement(id: String) {
fun closeAnnouncement(id: String?) {
if(id == null)
return;
val item: Announcement?;
synchronized(_lock) {
item = _announcementsStore.findItem { it.id == id };
@@ -164,6 +217,7 @@ class StateAnnouncement {
cancelAction?.invoke(item);
}
}
onAnnouncementChanged?.emit();
}
fun deleteAllAnnouncements() {
@@ -194,7 +248,9 @@ class StateAnnouncement {
onAnnouncementChanged.emit();
}
fun neverAnnouncement(id: String) {
fun neverAnnouncement(id: String?) {
if(id == null)
return;
synchronized(_lock) {
val item = _announcementsStore.findItem { it.id == id };
if (item != null && !_announcementsNever.contains(id))
@@ -208,19 +264,26 @@ class StateAnnouncement {
_announcementsNever.save();
onAnnouncementChanged.emit();
}
fun actionAnnouncement(id: String) {
fun actionAnnouncement(id: String?, extra: Boolean = false) {
if(id == null)
return;
val item = _announcementsStore.findItem { it.id == id } ?: _sessionAnnouncements[id];
if(item != null)
actionAnnouncement(item);
actionAnnouncement(item, extra);
}
fun actionAnnouncement(item: Announcement) {
fun actionAnnouncement(item: Announcement, extra: Boolean = false) {
val actionId = if(!extra) item.actionId else if(item is SessionAnnouncement) item.extraActionId else null;
val actionData = if(!extra) item.actionData else if(item is SessionAnnouncement) item.extraActionData else null;
val action = _sessionActions[item.id];
if (action != null) {
action(item);
} else {
when (item.actionId) {
when (actionId) {
ACTION_NEVER -> neverAnnouncement(item.id);
ACTION_SOMETHING -> actionSomething();
ACTION_CHANGELOG -> actionChangelog(actionData);
ACTION_UPDATE_PLUGIN -> actionUpdatePlugin(item.id, actionData);
}
}
}
@@ -251,6 +314,84 @@ class StateAnnouncement {
}
private fun actionChangelog(id: String?) {
if(id == null)
return;
StateApp.instance.contextOrNull?.let { context ->
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
val plugin = StatePlugins.instance.getPlugin(id);
if (plugin == null)
return@launch
val update = StatePlugins.instance.checkForUpdates(plugin.config);
if(update == null)
return@launch;
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
UIDialogs.showChangelogDialog(context, update.version, update.changelog!!.filterKeys { it.toIntOrNull() != null }
.mapKeys { it.key.toInt() }
.mapValues { update.getChangelogString(it.key.toString()) ?: "" });
}
}
}
}
private fun actionUpdatePlugin(notifId: String?, id: String?) {
if(id == null)
return;
val plugin = StatePlugins.instance.getPlugin(id);
if (plugin == null)
return
closeAnnouncement(notifId);
val loadingAnnouncement = registerLoading("Updating ${plugin.config.name}..", "An update is in progress for ${plugin.config.name}.",
if(plugin.config.absoluteIconUrl != null) ImageVariable.fromUrl(plugin.config.absoluteIconUrl!!) else null);
val loadingId = loadingAnnouncement.id;
StateApp.instance.contextOrNull?.let { context ->
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try {
val update = StatePlugins.instance.checkForUpdates(plugin.config);
if (update == null)
return@launch;
val client = ManagedHttpClient();
client.setTimeout(10000);
val script = StatePlugins.instance.getScript(plugin.config.id) ?: "";
val newScript = client.get(update.absoluteScriptUrl)?.body?.string();
if(newScript.isNullOrEmpty())
throw IllegalStateException("No script found");
if(true || plugin.config.isLowRiskUpdate(script, update, newScript)) {
StatePlugins.instance.installPluginBackground(context, StateApp.instance.scope, update, newScript,
{ text: String, progress: Double -> },
{ ex ->
if(ex == null) {
registerPluginUpdated(update);
}
else {
UIDialogs.appToast("Update for ${update.name} failed\n" + ex.message);
}
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
closeAnnouncement(loadingId);
}
});
}
else {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
closeAnnouncement(loadingId);
UIDialogs.showPluginUpdateDialog(context, plugin.config, update);
}
}
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to trigger update from announcement", ex);
}
}
}
}
fun registerDefaultHandlerAnnouncement() {
registerAnnouncement(
"default-url-handler",
@@ -279,6 +420,8 @@ class StateAnnouncement {
const val ACTION_SOMETHING = "SOMETHING";
const val ACTION_CHANGELOG = "CHANGELOG";
const val ACTION_UPDATE_PLUGIN = "UPDATE_PLUGIN";
const val ACTION_NEVER = "NEVER";
private const val TAG = "StateAnnouncement";
}
@@ -294,7 +437,8 @@ open class Announcement(
val time: OffsetDateTime? = null,
val category: String? = null,
val actionName: String? = null,
val actionId: String? = null
val actionId: String? = null,
val actionData: String? = null
);
class SessionAnnouncement(
id: String,
@@ -306,7 +450,9 @@ class SessionAnnouncement(
actionName: String? = null,
actionId: String? = null,
val cancelName: String? = null,
val cancelActionId: String? = null
val cancelActionId: String? = null,
actionData: String? = null,
val icon: ImageVariable? = null
): Announcement(
id= id,
title = title,
@@ -315,13 +461,40 @@ class SessionAnnouncement(
time = time,
category = category,
actionName = actionName,
actionId = actionId
);
actionId = actionId,
actionData = actionData
) {
var extraActionName: String? = null;
var extraActionId: String? = null;
var extraActionData: String? = null;
var extraObj: Any? = null;
var progress: Double? = null;
val onProgressChanged = Event1<SessionAnnouncement>();
fun withExtraAction(name: String, id: String, data: String? = null): SessionAnnouncement {
extraActionName = name;
extraActionId = id;
extraActionData = data;
return this;
}
fun setProgress(progress: Double) {
this.progress = progress;
onProgressChanged?.emit(this);
}
fun setProgress(progress: Int) {
this.progress = progress.toDouble().div(100);
onProgressChanged?.emit(this);
}
}
enum class AnnouncementType(val value : Int) {
DELETABLE(0), //Close button deletes announcement (generally for actions)
RECURRING(1), //Shows up till never is pressed (generally for patchnotes etc)
PERMANENT(2), //Shows up until deleted through other means (action)
SESSION(3), //Not persistent, only during this session
SESSION_RECURRING(4); //Not persistent, only during this session, recurring id
SESSION_RECURRING(4), //Not persistent, only during this session, recurring id
ONGOING(5);
}
@@ -43,6 +43,7 @@ import com.futo.platformplayer.logging.AndroidLogConsumer
import com.futo.platformplayer.logging.FileLogConsumer
import com.futo.platformplayer.logging.LogLevel
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.receivers.AudioNoisyReceiver
import com.futo.platformplayer.services.DownloadService
import com.futo.platformplayer.stores.FragmentedStorage
@@ -732,8 +733,10 @@ class StateApp {
));
for(update in updateAvailable)
if(StatePlatform.instance.isClientEnabled(update.first.id))
UIDialogs.showPluginUpdateDialog(context, update.first, update.second);
if(StatePlatform.instance.isClientEnabled(update.first.id)) {
//UIDialogs.showPluginUpdateDialog(context, update.first, update.second);
StateAnnouncement.instance.registerPluginUpdate(update.first, update.second);
}
}
}
}
@@ -116,7 +116,7 @@ class StatePlugins {
_updatesAvailableMap = updatesAvailableFor
return@withContext configs;
}
private suspend fun checkForUpdates(c: SourcePluginConfig): SourcePluginConfig? = withContext(Dispatchers.IO) {
suspend fun checkForUpdates(c: SourcePluginConfig): SourcePluginConfig? = withContext(Dispatchers.IO) {
val sourceUrl = c.sourceUrl ?: return@withContext null;
Logger.i(TAG, "Check for source updates '${c.name}'.");
@@ -113,7 +113,10 @@ class StateUpdate {
if (!dir.exists()) {
dir.mkdirs();
}
return File(dir, "app-${DESIRED_ABI}-${version}.apk");
val result = File(dir, "app-${DESIRED_ABI}-${version}.apk");
//if(result.exists())
// result.delete();
return result;
}
fun getPartialApkFile(context: Context, version: Int): File {
@@ -121,7 +124,10 @@ class StateUpdate {
if (!dir.exists()) {
dir.mkdirs();
}
return File(dir, "app-${DESIRED_ABI}-${version}.apk.part");
val result = File(dir, "app-${DESIRED_ABI}-${version}.apk.part");
//if(result.exists())
// result.delete();
return result;
}
fun finish() {
@@ -6,12 +6,10 @@ import android.view.View
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.Announcement
import com.futo.platformplayer.states.AnnouncementType
@@ -162,6 +160,10 @@ class AnnouncementView : LinearLayout {
_textClose.visibility = View.VISIBLE;
_textNever.visibility = View.VISIBLE;
}
AnnouncementType.ONGOING -> {
_textClose.visibility = View.GONE;
_textNever.visibility = View.GONE;
}
}
if (announcement.time != null) {
@@ -0,0 +1,238 @@
package com.futo.platformplayer.views.notification
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.Announcement
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.SessionAnnouncement
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.LoaderView
import com.futo.platformplayer.views.adapters.AnyAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class NotificationOverlayView: ConstraintLayout {
lateinit var recycler: RecyclerView;
var adapterNotifications: AnyAdapterView<Announcement, ViewHolder>;
constructor(context: Context) : super(context) {
inflate(context, R.layout.overlay_notifications, this)
recycler = findViewById<RecyclerView>(R.id.container_notifications);
adapterNotifications = recycler.asAny<Announcement, ViewHolder>(RecyclerView.VERTICAL, false, {
});
}
fun onShown(parameter: Any?) {
val announcements = StateAnnouncement.instance.getVisibleAnnouncements();
adapterNotifications.adapter.setData(announcements);
StateAnnouncement.instance.onAnnouncementChanged.subscribe(this) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
Logger.i("NotificationOverlayView", "Announcements Changed");
val adapter = adapterNotifications;
val announcements = StateAnnouncement.instance.getVisibleAnnouncements();
adapter.adapter.setData(announcements);
}
}
}
fun onResume() {
}
fun onPause() {
StateAnnouncement.instance.onAnnouncementChanged.remove(this);
}
class ViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<Announcement>(
LayoutInflater.from(_viewGroup.context).inflate(
R.layout.list_announcement,
_viewGroup, false)) {
protected var _announcement: Announcement? = null;
protected val _textName: TextView
protected val _textMetadata: TextView;
protected val _icon: ImageView;
protected val _buttonIgnore: ImageView
protected val _buttonNever: LinearLayout
protected val _buttonAction: LinearLayout
protected val _buttonActionText: TextView
protected val _buttonExtra: LinearLayout
protected val _buttonExtraText: TextView
protected val _loader: LoaderView;
protected val _progress: ProgressBar;
init {
_textName = _view.findViewById(R.id.text_name);
_textMetadata = _view.findViewById(R.id.text_metadata);
_buttonIgnore = _view.findViewById(R.id.button_ignore);
_buttonNever = _view.findViewById(R.id.button_never);
_buttonAction = _view.findViewById(R.id.button_action);
_buttonActionText = _view.findViewById(R.id.button_action_text);
_buttonExtra = _view.findViewById(R.id.button_extra);
_buttonExtraText = _view.findViewById(R.id.button_extra_text);
_icon = _view.findViewById(R.id.icon);
_loader = _view.findViewById(R.id.loader);
_progress = _view.findViewById(R.id.progress);
_buttonIgnore.setOnClickListener {
_announcement.let {
StateAnnouncement.instance.closeAnnouncement(it?.id);
}
}
_buttonNever.setOnClickListener {
_announcement.let {
StateAnnouncement.instance.neverAnnouncement(it?.id);
}
}
_buttonExtra.setOnClickListener {
_announcement.let {
StateAnnouncement.instance.actionAnnouncement(it?.id, true)
}
}
_buttonAction.setOnClickListener {
_announcement.let {
StateAnnouncement.instance.actionAnnouncement(it?.id);
}
}
}
override fun bind(value: Announcement) {
val oldAnnouncement = _announcement;
_announcement = value;
if(oldAnnouncement is SessionAnnouncement)
oldAnnouncement.onProgressChanged.clear();
_textName.text = value.title;
_textMetadata.text = value.msg;
if(value is SessionAnnouncement) {
if(value.icon != null) {
value.icon.setImageView(_icon);
_icon.visibility = View.VISIBLE;
}
else
_icon.visibility = View.GONE;
if(value.extraActionName != null && value.extraActionId != null) {
_buttonExtraText.text = value.extraActionName;
_buttonExtra.visibility = View.VISIBLE;
}
else
_buttonExtra.visibility = View.GONE;
if(value.announceType == AnnouncementType.ONGOING) {
_buttonIgnore.visibility = View.GONE;
}
else {
_buttonIgnore.visibility = View.VISIBLE;
}
if(value.progress != null && value.announceType == AnnouncementType.ONGOING) {
_progress.isVisible = true;
_progress.min = 0;
_progress.max = 100;
value.onProgressChanged.subscribe {
val prog = it.progress;
if(prog == 0.toDouble() || prog == 100.toDouble()) {
_progress.isIndeterminate = true;
}
else {
_progress.isIndeterminate = false;
_progress.setProgress(it.progress?.times(100)?.toInt() ?: 0, false);
}
}
}
else
_progress.isVisible = false;
}
else {
_buttonExtra.visibility = View.GONE;
_icon.visibility = View.GONE;
_buttonIgnore.visibility = View.VISIBLE;
}
if(value.announceType == AnnouncementType.ONGOING) {
_loader.visibility = View.VISIBLE;
_loader.start();
}
else {
_loader.visibility = View.GONE;
_loader.stop();
}
_buttonNever.visibility =
if (value.announceType == AnnouncementType.RECURRING || value.announceType == AnnouncementType.SESSION_RECURRING)
View.VISIBLE
else
View.GONE;
_buttonAction.visibility =
if(value.actionId != null && value.actionName != null)
View.VISIBLE;
else View.GONE;
if(value.actionId != null && value.actionName != null) {
_buttonActionText.text = value.actionName;
}
}
}
class Frag : MainFragment() {
override val isMainView : Boolean = true;
override val isTab: Boolean = true;
override val hasBottomBar: Boolean get() = true;
private var _view: NotificationOverlayView? = null;
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
_view?.onShown(parameter);
}
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = NotificationOverlayView(requireContext());
_view = view;
return view;
}
override fun onDestroyMainView() {
super.onDestroyMainView();
_view = null;
}
override fun onResume() {
super.onResume()
_view?.onResume();
}
override fun onPause() {
super.onPause()
_view?.onPause();
}
}
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#2D63ED" />
<corners android:radius="20dp" />
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M160,760L160,680L240,680L240,400Q240,317 290,252.5Q340,188 420,168L420,140Q420,115 437.5,97.5Q455,80 480,80Q505,80 522.5,97.5Q540,115 540,140L540,168Q620,188 670,252.5Q720,317 720,400L720,680L800,680L800,760L160,760ZM480,880Q447,880 423.5,856.5Q400,833 400,800L560,800Q560,833 536.5,856.5Q513,880 480,880Z"/>
</vector>
+2 -1
View File
@@ -38,11 +38,12 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<!--
<com.futo.platformplayer.views.announcements.AnnouncementView
android:id="@+id/announcement_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
android:visibility="gone" /> -->
<LinearLayout
android:id="@+id/container_sort_by"
@@ -46,6 +46,42 @@
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_cast_white_25dp" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/button_notifs">
<ImageButton
android:id="@+id/button_notifs_icon"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:contentDescription="@string/cd_button_notifs"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="11dp"
android:paddingBottom="10dp"
android:scaleType="fitCenter"
android:clickable="false"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:srcCompat="@drawable/ic_notifications" />
<TextView
android:id="@+id/button_notifs_count"
android:layout_width="18dp"
android:layout_height="18dp"
android:text="5"
android:textSize="12dp"
android:textAlignment="center"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:background="@drawable/background_primary_round_20dp"
android:layout_marginTop="3dp"
android:layout_marginRight="5dp"
android:clickable="false"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
<!--Back Button-->
<ImageButton
android:id="@+id/button_search"
@@ -30,11 +30,12 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<!--
<com.futo.platformplayer.views.announcements.AnnouncementView
android:id="@+id/announcement_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
android:visibility="gone" /> -->
<com.futo.platformplayer.views.others.RadioGroupView
android:id="@+id/radio_group"
@@ -0,0 +1,167 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="0dp"
android:layout_marginBottom="0dp"
android:id="@+id/root"
android:clickable="true"
android:paddingTop="10dp"
android:paddingLeft="10dp"
android:paddingRight="10dp">
<ImageView
android:id="@+id/icon"
android:layout_width="30dp"
android:layout_height="30dp"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="@id/text_metadata" />
<TextView
android:id="@+id/text_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textSize="13dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular"
tools:text="Example Artist"
android:maxLines="1"
app:layout_constraintLeft_toRightOf="@id/icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@id/button_ignore"
android:layout_marginRight="20dp"
android:layout_marginTop="10dp"
app:layout_constraintBottom_toTopOf="@id/text_metadata"
android:layout_marginStart="10dp" />
<TextView
android:id="@+id/text_metadata"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textSize="12dp"
android:textColor="#888888"
android:fontFamily="@font/inter_regular"
tools:text="3 videos"
android:maxLines="2"
app:layout_constraintTop_toBottomOf="@id/text_name"
app:layout_constraintLeft_toRightOf="@id/icon"
app:layout_constraintRight_toLeftOf="@id/button_ignore"
android:layout_marginRight="20dp"
android:paddingBottom="10dp"
android:layout_marginStart="10dp" />
<ImageView
android:id="@+id/button_ignore"
android:layout_width="30dp"
android:layout_height="30dp"
android:src="@drawable/ic_close"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="@id/text_metadata" />
<com.futo.platformplayer.views.LoaderView
android:id="@+id/loader"
android:layout_width="30dp"
android:layout_height="30dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="@id/text_metadata" />
<LinearLayout
android:id="@+id/container_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_metadata"
app:layout_constraintBottom_toTopOf="@id/separator"
android:gravity="center"
android:paddingBottom="10dp"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/button_never"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/background_button_accent"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingStart="28dp"
android:paddingEnd="28dp"
android:text="Never" />
</LinearLayout>
<LinearLayout
android:id="@+id/button_extra"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/background_button_accent"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp">
<TextView
android:id="@+id/button_extra_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingStart="28dp"
android:paddingEnd="28dp"
android:text="Extra" />
</LinearLayout>
<LinearLayout
android:id="@+id/button_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:background="@drawable/background_button_primary"
android:clickable="true">
<TextView
android:id="@+id/button_action_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Action"
android:textSize="14dp"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingStart="28dp"
android:paddingEnd="28dp" />
</LinearLayout>
</LinearLayout>
<ProgressBar
android:id="@+id/progress"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginBottom="1dp"
android:progressTint="@color/primary"
/>
<View
android:id="@+id/separator"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/container_buttons"
android:layout_width="match_parent"
android:layout_height="1px"
android:background="#181818" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/overlay_slide_up_menu_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#C9000000" />
<View
android:id="@+id/separator"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="match_parent"
android:layout_height="1px"
android:background="#181818" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/container_notifications"
app:layout_constraintTop_toBottomOf="@id/separator"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="match_parent"
android:layout_height="0dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
+1
View File
@@ -896,6 +896,7 @@
<string name="cd_creator_thumbnail">Creator thumbnail</string>
<string name="cd_button_clear_search">Clear search</string>
<string name="cd_button_search">Search</string>
<string name="cd_button_notifs">Notifications</string>
<string name="cd_search_icon">Search icon</string>
<string name="cd_button_back">Back button</string>
<string name="cd_app_icon">App icon</string>