mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Merge branch 'package-browser' into 'master'
New Notification UI & PackageBrowser support See merge request videostreaming/grayjay!163
This commit is contained in:
@@ -7,6 +7,10 @@ import android.os.IBinder
|
|||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import com.futo.platformplayer.UIDialogs.ActionStyle
|
import com.futo.platformplayer.UIDialogs.ActionStyle
|
||||||
import com.futo.platformplayer.logging.Logger
|
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.StateApp
|
||||||
import com.futo.platformplayer.states.StateUpdate
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
@@ -14,6 +18,7 @@ import java.io.File
|
|||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
class UpdateDownloadService : Service() {
|
class UpdateDownloadService : Service() {
|
||||||
|
|
||||||
@@ -85,13 +90,16 @@ class UpdateDownloadService : Service() {
|
|||||||
job.cancel()
|
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 now = SystemClock.elapsedRealtime()
|
||||||
val force = progress == 100 && !indeterminate
|
val force = progress == 100 && !indeterminate
|
||||||
|
|
||||||
if (force || now - lastProgressUpdateElapsedMs >= MIN_PROGRESS_UPDATE_INTERVAL_MS) {
|
if (force || now - lastProgressUpdateElapsedMs >= MIN_PROGRESS_UPDATE_INTERVAL_MS) {
|
||||||
lastProgressUpdateElapsedMs = now
|
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 apkFile = StateUpdate.getApkFile(this, version)
|
||||||
val partialFile = StateUpdate.getPartialApkFile(this, version)
|
val partialFile = StateUpdate.getPartialApkFile(this, version)
|
||||||
|
|
||||||
|
var announcement: SessionAnnouncement? = null;
|
||||||
try {
|
try {
|
||||||
if (apkFile.exists() && apkFile.length() > 0L) {
|
if (apkFile.exists() && apkFile.length() > 0L) {
|
||||||
Logger.i(TAG, "APK already downloaded at ${apkFile.absolutePath}")
|
Logger.i(TAG, "APK already downloaded at ${apkFile.absolutePath}")
|
||||||
@@ -106,6 +115,14 @@ class UpdateDownloadService : Service() {
|
|||||||
return
|
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
|
var backoffMs = INITIAL_BACKOFF_MS
|
||||||
|
|
||||||
for (attempt in 0 until MAX_RETRIES) {
|
for (attempt in 0 until MAX_RETRIES) {
|
||||||
@@ -115,7 +132,13 @@ class UpdateDownloadService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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 (!cancelRequested) {
|
||||||
if (apkFile.exists()) {
|
if (apkFile.exists()) {
|
||||||
@@ -145,6 +168,12 @@ class UpdateDownloadService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
try {
|
||||||
|
if (announcement != null) {
|
||||||
|
StateAnnouncement.instance.closeAnnouncement(announcement.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable){}
|
||||||
isDownloading = false
|
isDownloading = false
|
||||||
cancelRequested = false
|
cancelRequested = false
|
||||||
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
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
|
var startOffset = if (partialFile.exists()) partialFile.length() else 0L
|
||||||
Logger.i(TAG, "Starting download. url=$url, existingBytes=$startOffset")
|
Logger.i(TAG, "Starting download. url=$url, existingBytes=$startOffset")
|
||||||
|
|
||||||
@@ -204,7 +233,7 @@ class UpdateDownloadService : Service() {
|
|||||||
progress > 100 -> 100
|
progress > 100 -> 100
|
||||||
else -> progress
|
else -> progress
|
||||||
}
|
}
|
||||||
throttledUpdateDownloadProgress(version, safeProgress, indeterminate = false)
|
throttledUpdateDownloadProgress(version, safeProgress, indeterminate = false, onProgress)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throttledUpdateDownloadProgress(version, progress = 0, indeterminate = true)
|
throttledUpdateDownloadProgress(version, progress = 0, indeterminate = true)
|
||||||
@@ -250,6 +279,18 @@ class UpdateDownloadService : Service() {
|
|||||||
UpdateNotificationManager.cancelAll(ctx)
|
UpdateNotificationManager.cancelAll(ctx)
|
||||||
UpdateInstaller.startInstall(ctx, version, apkFile)
|
UpdateInstaller.startInstall(ctx, version, apkFile)
|
||||||
}, ActionStyle.PRIMARY, true));
|
}, 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) {
|
} catch (t: Throwable) {
|
||||||
Logger.w(TAG, "Failed to show in-app update downloaded dialog", t)
|
Logger.w(TAG, "Failed to show in-app update downloaded dialog", t)
|
||||||
updateDownloadedDialog = null
|
updateDownloadedDialog = null
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ import com.futo.platformplayer.stores.StringStorage
|
|||||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||||
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.platformplayer.views.notification.NotificationOverlayView
|
||||||
import com.futo.polycentric.core.ApiMethods
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
@@ -201,6 +202,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
lateinit var _fragLibraryVideos: LibraryVideosFragment;
|
lateinit var _fragLibraryVideos: LibraryVideosFragment;
|
||||||
lateinit var _fragLibrarySearch: LibrarySearchFragment;
|
lateinit var _fragLibrarySearch: LibrarySearchFragment;
|
||||||
lateinit var _fragLibraryFiles: LibraryFilesFragment;
|
lateinit var _fragLibraryFiles: LibraryFilesFragment;
|
||||||
|
lateinit var _fragNotifications: NotificationOverlayView.Frag;
|
||||||
lateinit var _fragSettings: SettingsFragment;
|
lateinit var _fragSettings: SettingsFragment;
|
||||||
lateinit var _fragDeveloper: DeveloperFragment;
|
lateinit var _fragDeveloper: DeveloperFragment;
|
||||||
lateinit var _fragLogin: LoginFragment;
|
lateinit var _fragLogin: LoginFragment;
|
||||||
@@ -389,6 +391,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragLibraryVideos = LibraryVideosFragment.newInstance();
|
_fragLibraryVideos = LibraryVideosFragment.newInstance();
|
||||||
_fragLibraryFiles = LibraryFilesFragment.newInstance();
|
_fragLibraryFiles = LibraryFilesFragment.newInstance();
|
||||||
_fragLibrarySearch = LibrarySearchFragment.newInstance();
|
_fragLibrarySearch = LibrarySearchFragment.newInstance();
|
||||||
|
_fragNotifications = NotificationOverlayView.Frag();
|
||||||
_fragSettings = SettingsFragment.newInstance();
|
_fragSettings = SettingsFragment.newInstance();
|
||||||
_fragDeveloper = DeveloperFragment.newInstance();
|
_fragDeveloper = DeveloperFragment.newInstance();
|
||||||
_fragLogin = LoginFragment.newInstance();
|
_fragLogin = LoginFragment.newInstance();
|
||||||
@@ -538,6 +541,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_fragLibrarySearch.topBar = _fragTopBarSearch;
|
_fragLibrarySearch.topBar = _fragTopBarSearch;
|
||||||
_fragSettings.topBar = _fragTopBarNavigation;
|
_fragSettings.topBar = _fragTopBarNavigation;
|
||||||
_fragDeveloper.topBar = _fragTopBarNavigation;
|
_fragDeveloper.topBar = _fragTopBarNavigation;
|
||||||
|
_fragNotifications.topBar = _fragTopBarGeneral;
|
||||||
|
|
||||||
_fragBrowser.topBar = _fragTopBarNavigation;
|
_fragBrowser.topBar = _fragTopBarNavigation;
|
||||||
|
|
||||||
@@ -1368,6 +1372,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
LibraryVideosFragment::class -> _fragLibraryVideos as T;
|
LibraryVideosFragment::class -> _fragLibraryVideos as T;
|
||||||
LibraryFilesFragment::class -> _fragLibraryFiles as T;
|
LibraryFilesFragment::class -> _fragLibraryFiles as T;
|
||||||
LibrarySearchFragment::class -> _fragLibrarySearch as T;
|
LibrarySearchFragment::class -> _fragLibrarySearch as T;
|
||||||
|
NotificationOverlayView.Frag::class -> _fragNotifications as T;
|
||||||
SettingsFragment:: class -> _fragSettings as T;
|
SettingsFragment:: class -> _fragSettings as T;
|
||||||
DeveloperFragment::class -> _fragDeveloper as T;
|
DeveloperFragment::class -> _fragDeveloper as T;
|
||||||
LoginFragment::class -> _fragLogin as T;
|
LoginFragment::class -> _fragLogin as T;
|
||||||
|
|||||||
+11
@@ -61,6 +61,11 @@ class SourcePluginConfig(
|
|||||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||||
val absoluteScriptUrl: String get() = resolveAbsoluteUrl(scriptUrl, sourceUrl)!!;
|
val absoluteScriptUrl: String get() = resolveAbsoluteUrl(scriptUrl, sourceUrl)!!;
|
||||||
|
|
||||||
|
fun isOfficialAuthor(): Boolean {
|
||||||
|
return scriptSignature != null &&
|
||||||
|
scriptPublicKey == "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsoFJU4AReDyUnSQI9A99UjLCwkY8OH+1o8cdtf2EjSb+fO2qmP8MGMTAvfvgmq5d2QBJE2XHRkRO3JKcTlcc1j0WlOlU8P9W272DYCeX6oYaavpKNqGKoGEuodp9wtiyNwyH46++JfpU/uIUacZbZKkHv9gIGchmNvpKYZQjFd/8pUqXGpcXZP54tGSC9PLcY+5TozZThK7Oy1+3YEf1bZ44UinRYYATbLk/wNuAfsupvlt6nxZOcJhABhdo9V+gY0FE6Ayg5+1cd1noWhnRtLF+sPdEr3z8Nt15JEK5a/524t25FMhwz8yKxlGW5qW3QLJHSUgLQncL6a1zlZ1s8QIDAQAB"
|
||||||
|
}
|
||||||
|
|
||||||
private fun resolveAbsoluteUrl(url: String?, sourceUrl: String?): String? {
|
private fun resolveAbsoluteUrl(url: String?, sourceUrl: String?): String? {
|
||||||
if(url == null)
|
if(url == null)
|
||||||
return null;
|
return null;
|
||||||
@@ -165,6 +170,12 @@ class SourcePluginConfig(
|
|||||||
"Unrestricted Http Header access",
|
"Unrestricted Http Header access",
|
||||||
"Allows this plugin to access all headers (including cookies and authorization headers) for unauthenticated requests."
|
"Allows this plugin to access all headers (including cookies and authorization headers) for unauthenticated requests."
|
||||||
))
|
))
|
||||||
|
if(packagesOptional.contains("Browser") || packages.contains("Browser")) {
|
||||||
|
list.add(Pair(
|
||||||
|
"Browser Interop",
|
||||||
|
"This plugin requires webbrowser interop. May access urls outside of the restricted urls. This will only work for official plugins and during development builds."
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import com.caoccao.javet.values.primitive.V8ValueString
|
|||||||
import com.caoccao.javet.values.reference.IV8ValuePromise
|
import com.caoccao.javet.values.reference.IV8ValuePromise
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.caoccao.javet.values.reference.V8ValuePromise
|
import com.caoccao.javet.values.reference.V8ValuePromise
|
||||||
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.engine.exceptions.NoInternetException
|
import com.futo.platformplayer.engine.exceptions.NoInternetException
|
||||||
@@ -34,6 +36,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptTimeoutException
|
|||||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||||
import com.futo.platformplayer.engine.internal.V8Converter
|
import com.futo.platformplayer.engine.internal.V8Converter
|
||||||
import com.futo.platformplayer.engine.packages.PackageBridge
|
import com.futo.platformplayer.engine.packages.PackageBridge
|
||||||
|
import com.futo.platformplayer.engine.packages.PackageBrowser
|
||||||
import com.futo.platformplayer.engine.packages.PackageDOMParser
|
import com.futo.platformplayer.engine.packages.PackageDOMParser
|
||||||
import com.futo.platformplayer.engine.packages.PackageHttp
|
import com.futo.platformplayer.engine.packages.PackageHttp
|
||||||
import com.futo.platformplayer.engine.packages.PackageHttpImp
|
import com.futo.platformplayer.engine.packages.PackageHttpImp
|
||||||
@@ -44,6 +47,7 @@ import com.futo.platformplayer.getOrDefault
|
|||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateAssets
|
import com.futo.platformplayer.states.StateAssets
|
||||||
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import com.futo.platformplayer.toList
|
import com.futo.platformplayer.toList
|
||||||
import com.futo.platformplayer.toV8ValueBlocking
|
import com.futo.platformplayer.toV8ValueBlocking
|
||||||
import com.futo.platformplayer.toV8ValueAsync
|
import com.futo.platformplayer.toV8ValueAsync
|
||||||
@@ -218,6 +222,9 @@ class V8Plugin {
|
|||||||
if(pack is PackageHttp) {
|
if(pack is PackageHttp) {
|
||||||
pack.cleanup();
|
pack.cleanup();
|
||||||
}
|
}
|
||||||
|
else if(pack is PackageBrowser) {
|
||||||
|
pack.deinitialize();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_runtime?.let {
|
_runtime?.let {
|
||||||
@@ -387,6 +394,18 @@ class V8Plugin {
|
|||||||
"HttpImp" -> PackageHttpImp(this, config)
|
"HttpImp" -> PackageHttpImp(this, config)
|
||||||
"Utilities" -> PackageUtilities(this, config)
|
"Utilities" -> PackageUtilities(this, config)
|
||||||
"JSDOM" -> PackageJSDOM(this, config)
|
"JSDOM" -> PackageJSDOM(this, config)
|
||||||
|
"Browser" -> {
|
||||||
|
val isOfficial = (config is SourcePluginConfig && config.isOfficialAuthor());
|
||||||
|
|
||||||
|
if(BuildConfig.DEBUG)
|
||||||
|
PackageBrowser(this)
|
||||||
|
else if(isOfficial)
|
||||||
|
PackageBrowser(this)
|
||||||
|
else if(config is SourcePluginConfig && config.id == StateDeveloper.DEV_ID)
|
||||||
|
PackageBrowser(this)
|
||||||
|
else
|
||||||
|
throw IllegalArgumentException("Browser is only allowed for debug and official plugins due to security");
|
||||||
|
};
|
||||||
else -> if(allowNull) null else throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
else -> if(allowNull) null else throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,11 @@ class PackageBridge : V8Package {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun hasPackage(str: String): Boolean {
|
||||||
|
return _plugin.getPackages().any { it.name == str };
|
||||||
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun dispose(value: V8Value) {
|
fun dispose(value: V8Value) {
|
||||||
Logger.e(TAG, "Manual dispose: " + value.javaClass.name);
|
Logger.e(TAG, "Manual dispose: " + value.javaClass.name);
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package com.futo.platformplayer.engine.packages
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.webkit.ConsoleMessage
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
|
import android.webkit.ValueCallback
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import androidx.collection.emptyLongSet
|
||||||
|
import com.caoccao.javet.annotations.V8Function
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueFunction
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
|
||||||
|
class PackageBrowser: V8Package {
|
||||||
|
override val name: String get() = "Browser";
|
||||||
|
override val variableName: String = "browser";
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
private var _readySemaphore: Semaphore? = null;
|
||||||
|
@Transient
|
||||||
|
private val _callbacks = mutableMapOf<String, (String?)->Unit>();
|
||||||
|
@Transient
|
||||||
|
private val _interop = JSInterop(this);
|
||||||
|
@Transient
|
||||||
|
private var _browser: WebView? = null;
|
||||||
|
private val browser: WebView get() {
|
||||||
|
if(_browser == null)
|
||||||
|
throw IllegalStateException("Browser not initialized");
|
||||||
|
return _browser!!;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(v8Plugin: V8Plugin): super(v8Plugin) {
|
||||||
|
|
||||||
|
}
|
||||||
|
@V8Function
|
||||||
|
fun initialize() {
|
||||||
|
if(_browser == null){
|
||||||
|
StateApp.instance.scope.launch(Dispatchers.Main) {
|
||||||
|
_browser = WebView(StateApp.instance.contextOrNull ?: return@launch);
|
||||||
|
_browser?.settings?.javaScriptEnabled = true;
|
||||||
|
_browser?.settings?.blockNetworkImage = false;
|
||||||
|
_browser?.settings?.blockNetworkLoads = false;
|
||||||
|
_browser?.settings?.allowContentAccess = false;
|
||||||
|
_browser?.settings?.allowFileAccess = false;
|
||||||
|
//_browser?.settings?.useWideViewPort = true;
|
||||||
|
//_browser?.settings?.loadWithOverviewMode = true;
|
||||||
|
_browser?.webViewClient = object : WebViewClient() {
|
||||||
|
override fun onPageCommitVisible(view: WebView?, url: String?) {
|
||||||
|
super.onPageCommitVisible(view, url)
|
||||||
|
_readySemaphore?.release();
|
||||||
|
_readySemaphore = null;
|
||||||
|
Logger.i("PackageBrowser", "Browser loaded");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_browser?.webChromeClient = object : WebChromeClient() {
|
||||||
|
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||||
|
if(consoleMessage?.messageLevel() == ConsoleMessage.MessageLevel.ERROR) {
|
||||||
|
val msg = "Browser Error:${consoleMessage?.message()} [${consoleMessage?.lineNumber()}]" ?: ""
|
||||||
|
Logger.e("PackageBrowser", msg);
|
||||||
|
if(_plugin.config is SourcePluginConfig && _plugin.config.id == StateDeveloper.DEV_ID)
|
||||||
|
StateDeveloper.instance.logDevException(StateDeveloper.instance.currentDevID ?: "", msg)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val msg = "Browser Log:" + consoleMessage?.message() ?: "";
|
||||||
|
Logger.e("PackageBrowser", msg);
|
||||||
|
if(_plugin.config is SourcePluginConfig && _plugin.config.id == StateDeveloper.DEV_ID)
|
||||||
|
StateDeveloper.instance.logDevInfo(StateDeveloper.instance.currentDevID ?: "", msg);
|
||||||
|
}
|
||||||
|
return super.onConsoleMessage(consoleMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_browser?.addJavascriptInterface(_interop, "__GJ");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@V8Function
|
||||||
|
fun deinitialize() {
|
||||||
|
_browser?.destroy();
|
||||||
|
_browser = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun getCurrentUrl(): String? {
|
||||||
|
return browser.url;
|
||||||
|
}
|
||||||
|
@V8Function
|
||||||
|
fun waitTillLoaded(timeout: Int = 1000): Boolean {
|
||||||
|
val acquired = _readySemaphore?.let {
|
||||||
|
if(!it.tryAcquire()) {
|
||||||
|
Logger.i("PackageBrowser", "Waiting for browser to be ready");
|
||||||
|
if(!runBlocking {
|
||||||
|
try {
|
||||||
|
return@runBlocking withTimeout(timeout.toLong(), {
|
||||||
|
it.acquire()
|
||||||
|
return@withTimeout true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(ex: TimeoutCancellationException) {
|
||||||
|
return@runBlocking false;
|
||||||
|
}
|
||||||
|
}) return@let false;
|
||||||
|
}
|
||||||
|
it.release();
|
||||||
|
return@let true;
|
||||||
|
} ?: true;
|
||||||
|
if(acquired)
|
||||||
|
Logger.i("PackageBrowser", "Browser is ready");
|
||||||
|
else
|
||||||
|
Logger.i("PackageBrowser", "Browser failed wait ready");
|
||||||
|
return acquired;
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun load(url: String) {
|
||||||
|
Logger.i("PackageBrowser", "Browser loading url [${url}]");
|
||||||
|
_readySemaphore = Semaphore(1, 1);
|
||||||
|
StateApp.instance.scope.launch(Dispatchers.Main) {
|
||||||
|
browser.loadUrl(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun run(js: String, callbackId: String? = null, callback: V8ValueFunction? = null) {
|
||||||
|
waitTillLoaded();
|
||||||
|
val funcClone = callback?.toClone<V8ValueFunction>()
|
||||||
|
if(callbackId != null && callback != null) {
|
||||||
|
synchronized(_callbacks) {
|
||||||
|
_callbacks.put(callbackId, {
|
||||||
|
funcClone?.callVoid(null, arrayOf(it));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StateApp.instance.scope.launch(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
Logger.i("PackageBrowser", "Browser running JS with callback [${callbackId}]\n${(if(js.length > 200) (js.substring(0, 200) + "...") else js)})");
|
||||||
|
browser.evaluateJavascript(js, object : ValueCallback<String> {
|
||||||
|
override fun onReceiveValue(value: String?) {
|
||||||
|
Logger.i("PackageBrowser", "Browser run finished");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("PackageBrowser", "Failed to invoke browser", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@V8Function
|
||||||
|
fun runWithReturn(js: String, callback: V8ValueFunction? = null) {
|
||||||
|
waitTillLoaded();
|
||||||
|
val funcClone = callback?.toClone<V8ValueFunction>()
|
||||||
|
StateApp.instance.scope.launch(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
Logger.i("PackageBrowser", "Browser running JS with callback [sync]\n${(if(js.length > 200) (js.substring(0, 200) + "...") else js)})");
|
||||||
|
browser.evaluateJavascript(js, object : ValueCallback<String> {
|
||||||
|
override fun onReceiveValue(value: String?) {
|
||||||
|
Logger.i("PackageBrowser", "Browser run returned: " + (value ?: ""));
|
||||||
|
funcClone?.callVoid(null, arrayOf(value));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("PackageBrowser", "Failed to invoke browser", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JSInterop(private val pack: PackageBrowser) {
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun callback(id: String, result: String) {
|
||||||
|
Logger.i("PackageBrowser", "Browser Callback [${id}]: ${result}");
|
||||||
|
val callback = synchronized(pack._callbacks) { pack._callbacks.remove(id); };
|
||||||
|
if(callback != null)
|
||||||
|
callback.invoke(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun log(msg: String) {
|
||||||
|
Logger.i("PackageBrowser", "Log: " + msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-1
@@ -10,6 +10,7 @@ import android.webkit.WebView
|
|||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.LinearLayout
|
||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -21,6 +22,7 @@ class BrowserFragment : MainFragment() {
|
|||||||
override val isTab: Boolean = false;
|
override val isTab: Boolean = false;
|
||||||
override val hasBottomBar: Boolean get() = true;
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
|
private var _root: LinearLayout? = null;
|
||||||
private var _webview: WebView? = null;
|
private var _webview: WebView? = null;
|
||||||
private val _webviewWithoutHandling = object: WebViewClient() {
|
private val _webviewWithoutHandling = object: WebViewClient() {
|
||||||
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
||||||
@@ -31,6 +33,7 @@ class BrowserFragment : MainFragment() {
|
|||||||
|
|
||||||
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_browser, container, false);
|
val view = inflater.inflate(R.layout.fragment_browser, container, false);
|
||||||
|
_root = view.findViewById<LinearLayout>(R.id.root);
|
||||||
_webview = view.findViewById<WebView?>(R.id.webview).apply {
|
_webview = view.findViewById<WebView?>(R.id.webview).apply {
|
||||||
this.webViewClient = _webviewWithoutHandling;
|
this.webViewClient = _webviewWithoutHandling;
|
||||||
this.settings.javaScriptEnabled = true;
|
this.settings.javaScriptEnabled = true;
|
||||||
@@ -43,7 +46,12 @@ class BrowserFragment : MainFragment() {
|
|||||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
super.onShownWithView(parameter, isBack)
|
super.onShownWithView(parameter, isBack)
|
||||||
|
|
||||||
if(parameter is String) {
|
if(parameter is WebView) {
|
||||||
|
_root?.removeView(_webview);
|
||||||
|
_root?.addView(parameter);
|
||||||
|
_webview = parameter;
|
||||||
|
}
|
||||||
|
else if(parameter is String) {
|
||||||
_webview?.webViewClient = _webviewWithoutHandling;
|
_webview?.webViewClient = _webviewWithoutHandling;
|
||||||
_webview?.loadUrl(parameter);
|
_webview?.loadUrl(parameter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,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 _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;
|
||||||
@@ -87,7 +87,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)
|
//_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);
|
||||||
@@ -192,7 +192,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected fun showAnnouncementView() {
|
protected fun showAnnouncementView() {
|
||||||
_announcementView.visibility = View.VISIBLE
|
//_announcementView.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
|
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
|
||||||
|
|||||||
+2
-2
@@ -266,7 +266,7 @@ class LibraryFragment : MainFragment() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if(this.allowMusic) {
|
if(this.allowMusic) {
|
||||||
val artists = StateLibrary.instance.getArtists(ArtistOrdering.TrackCount);
|
val artists = StateLibrary.instance.getArtists(ArtistOrdering.TrackCount)
|
||||||
adapterArtists.setData(artists);
|
adapterArtists.setData(artists);
|
||||||
if (artists.size == 0)
|
if (artists.size == 0)
|
||||||
sectionArtists.setEmpty(
|
sectionArtists.setEmpty(
|
||||||
@@ -289,7 +289,7 @@ class LibraryFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(this.allowMusic) {
|
if(this.allowMusic) {
|
||||||
val albums = StateLibrary.instance.getAlbums();
|
val albums = StateLibrary.instance.getAlbums()
|
||||||
adapterAlbums.setData(albums);
|
adapterAlbums.setData(albums);
|
||||||
if (albums.size == 0)
|
if (albums.size == 0)
|
||||||
sectionAlbums.setEmpty("No albums", "No albums were found on your device", -1);
|
sectionAlbums.setEmpty("No albums", "No albums were found on your device", -1);
|
||||||
|
|||||||
+49
@@ -7,6 +7,9 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
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.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.CreatorsFragment
|
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.SuggestionsFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragmentData
|
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragmentData
|
||||||
import com.futo.platformplayer.models.SearchType
|
import com.futo.platformplayer.models.SearchType
|
||||||
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.views.casting.CastButton
|
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() {
|
class GeneralTopBarFragment : TopFragment() {
|
||||||
private var _buttonSearch: ImageButton? = null;
|
private var _buttonSearch: ImageButton? = null;
|
||||||
private var _buttonCast: CastButton? = 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?) {
|
override fun onShown(parameter: Any?) {
|
||||||
if(currentMain is CreatorsFragment) {
|
if(currentMain is CreatorsFragment) {
|
||||||
_buttonSearch?.setImageResource(R.drawable.ic_person_search_300w);
|
_buttonSearch?.setImageResource(R.drawable.ic_person_search_300w);
|
||||||
} else {
|
} else {
|
||||||
_buttonSearch?.setImageResource(R.drawable.ic_search_300w);
|
_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() {
|
override fun onHide() {
|
||||||
|
|
||||||
@@ -44,6 +83,16 @@ class GeneralTopBarFragment : TopFragment() {
|
|||||||
val buttonSearch: ImageButton = view.findViewById(R.id.button_search);
|
val buttonSearch: ImageButton = view.findViewById(R.id.button_search);
|
||||||
_buttonCast = view.findViewById(R.id.button_cast);
|
_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 {
|
buttonSearch.setOnClickListener {
|
||||||
if(currentMain is CreatorsFragment) {
|
if(currentMain is CreatorsFragment) {
|
||||||
navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.CREATOR));
|
navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.CREATOR));
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
package com.futo.platformplayer.states
|
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.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
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.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.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringHashSetStorage
|
import com.futo.platformplayer.stores.StringHashSetStorage
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -110,6 +119,48 @@ class StateAnnouncement {
|
|||||||
onAnnouncementChanged.emit();
|
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> {
|
fun getVisibleAnnouncements(category: String? = null): List<Announcement> {
|
||||||
synchronized(_lock) {
|
synchronized(_lock) {
|
||||||
if (category != null) {
|
if (category != null) {
|
||||||
@@ -122,7 +173,9 @@ class StateAnnouncement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun closeAnnouncement(id: String) {
|
fun closeAnnouncement(id: String?) {
|
||||||
|
if(id == null)
|
||||||
|
return;
|
||||||
val item: Announcement?;
|
val item: Announcement?;
|
||||||
synchronized(_lock) {
|
synchronized(_lock) {
|
||||||
item = _announcementsStore.findItem { it.id == id };
|
item = _announcementsStore.findItem { it.id == id };
|
||||||
@@ -164,6 +217,7 @@ class StateAnnouncement {
|
|||||||
cancelAction?.invoke(item);
|
cancelAction?.invoke(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onAnnouncementChanged?.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAllAnnouncements() {
|
fun deleteAllAnnouncements() {
|
||||||
@@ -194,7 +248,9 @@ class StateAnnouncement {
|
|||||||
|
|
||||||
onAnnouncementChanged.emit();
|
onAnnouncementChanged.emit();
|
||||||
}
|
}
|
||||||
fun neverAnnouncement(id: String) {
|
fun neverAnnouncement(id: String?) {
|
||||||
|
if(id == null)
|
||||||
|
return;
|
||||||
synchronized(_lock) {
|
synchronized(_lock) {
|
||||||
val item = _announcementsStore.findItem { it.id == id };
|
val item = _announcementsStore.findItem { it.id == id };
|
||||||
if (item != null && !_announcementsNever.contains(id))
|
if (item != null && !_announcementsNever.contains(id))
|
||||||
@@ -208,19 +264,26 @@ class StateAnnouncement {
|
|||||||
_announcementsNever.save();
|
_announcementsNever.save();
|
||||||
onAnnouncementChanged.emit();
|
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];
|
val item = _announcementsStore.findItem { it.id == id } ?: _sessionAnnouncements[id];
|
||||||
if(item != null)
|
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];
|
val action = _sessionActions[item.id];
|
||||||
if (action != null) {
|
if (action != null) {
|
||||||
action(item);
|
action(item);
|
||||||
} else {
|
} else {
|
||||||
when (item.actionId) {
|
when (actionId) {
|
||||||
ACTION_NEVER -> neverAnnouncement(item.id);
|
ACTION_NEVER -> neverAnnouncement(item.id);
|
||||||
ACTION_SOMETHING -> actionSomething();
|
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() {
|
fun registerDefaultHandlerAnnouncement() {
|
||||||
registerAnnouncement(
|
registerAnnouncement(
|
||||||
"default-url-handler",
|
"default-url-handler",
|
||||||
@@ -279,6 +420,8 @@ class StateAnnouncement {
|
|||||||
|
|
||||||
|
|
||||||
const val ACTION_SOMETHING = "SOMETHING";
|
const val ACTION_SOMETHING = "SOMETHING";
|
||||||
|
const val ACTION_CHANGELOG = "CHANGELOG";
|
||||||
|
const val ACTION_UPDATE_PLUGIN = "UPDATE_PLUGIN";
|
||||||
const val ACTION_NEVER = "NEVER";
|
const val ACTION_NEVER = "NEVER";
|
||||||
private const val TAG = "StateAnnouncement";
|
private const val TAG = "StateAnnouncement";
|
||||||
}
|
}
|
||||||
@@ -294,7 +437,8 @@ open class Announcement(
|
|||||||
val time: OffsetDateTime? = null,
|
val time: OffsetDateTime? = null,
|
||||||
val category: String? = null,
|
val category: String? = null,
|
||||||
val actionName: String? = null,
|
val actionName: String? = null,
|
||||||
val actionId: String? = null
|
val actionId: String? = null,
|
||||||
|
val actionData: String? = null
|
||||||
);
|
);
|
||||||
class SessionAnnouncement(
|
class SessionAnnouncement(
|
||||||
id: String,
|
id: String,
|
||||||
@@ -306,7 +450,9 @@ class SessionAnnouncement(
|
|||||||
actionName: String? = null,
|
actionName: String? = null,
|
||||||
actionId: String? = null,
|
actionId: String? = null,
|
||||||
val cancelName: String? = null,
|
val cancelName: String? = null,
|
||||||
val cancelActionId: String? = null
|
val cancelActionId: String? = null,
|
||||||
|
actionData: String? = null,
|
||||||
|
val icon: ImageVariable? = null
|
||||||
): Announcement(
|
): Announcement(
|
||||||
id= id,
|
id= id,
|
||||||
title = title,
|
title = title,
|
||||||
@@ -315,13 +461,40 @@ class SessionAnnouncement(
|
|||||||
time = time,
|
time = time,
|
||||||
category = category,
|
category = category,
|
||||||
actionName = actionName,
|
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) {
|
enum class AnnouncementType(val value : Int) {
|
||||||
DELETABLE(0), //Close button deletes announcement (generally for actions)
|
DELETABLE(0), //Close button deletes announcement (generally for actions)
|
||||||
RECURRING(1), //Shows up till never is pressed (generally for patchnotes etc)
|
RECURRING(1), //Shows up till never is pressed (generally for patchnotes etc)
|
||||||
PERMANENT(2), //Shows up until deleted through other means (action)
|
PERMANENT(2), //Shows up until deleted through other means (action)
|
||||||
SESSION(3), //Not persistent, only during this session
|
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.FileLogConsumer
|
||||||
import com.futo.platformplayer.logging.LogLevel
|
import com.futo.platformplayer.logging.LogLevel
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImageVariable
|
||||||
import com.futo.platformplayer.receivers.AudioNoisyReceiver
|
import com.futo.platformplayer.receivers.AudioNoisyReceiver
|
||||||
import com.futo.platformplayer.services.DownloadService
|
import com.futo.platformplayer.services.DownloadService
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
@@ -732,8 +733,10 @@ class StateApp {
|
|||||||
));
|
));
|
||||||
|
|
||||||
for(update in updateAvailable)
|
for(update in updateAvailable)
|
||||||
if(StatePlatform.instance.isClientEnabled(update.first.id))
|
if(StatePlatform.instance.isClientEnabled(update.first.id)) {
|
||||||
UIDialogs.showPluginUpdateDialog(context, update.first, update.second);
|
//UIDialogs.showPluginUpdateDialog(context, update.first, update.second);
|
||||||
|
StateAnnouncement.instance.registerPluginUpdate(update.first, update.second);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class StatePlugins {
|
|||||||
_updatesAvailableMap = updatesAvailableFor
|
_updatesAvailableMap = updatesAvailableFor
|
||||||
return@withContext configs;
|
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;
|
val sourceUrl = c.sourceUrl ?: return@withContext null;
|
||||||
|
|
||||||
Logger.i(TAG, "Check for source updates '${c.name}'.");
|
Logger.i(TAG, "Check for source updates '${c.name}'.");
|
||||||
|
|||||||
@@ -113,7 +113,10 @@ class StateUpdate {
|
|||||||
if (!dir.exists()) {
|
if (!dir.exists()) {
|
||||||
dir.mkdirs();
|
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 {
|
fun getPartialApkFile(context: Context, version: Int): File {
|
||||||
@@ -121,7 +124,10 @@ class StateUpdate {
|
|||||||
if (!dir.exists()) {
|
if (!dir.exists()) {
|
||||||
dir.mkdirs();
|
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() {
|
fun finish() {
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ import android.view.View
|
|||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.dp
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.Announcement
|
import com.futo.platformplayer.states.Announcement
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
@@ -162,6 +160,10 @@ class AnnouncementView : LinearLayout {
|
|||||||
_textClose.visibility = View.VISIBLE;
|
_textClose.visibility = View.VISIBLE;
|
||||||
_textNever.visibility = View.VISIBLE;
|
_textNever.visibility = View.VISIBLE;
|
||||||
}
|
}
|
||||||
|
AnnouncementType.ONGOING -> {
|
||||||
|
_textClose.visibility = View.GONE;
|
||||||
|
_textNever.visibility = View.GONE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (announcement.time != null) {
|
if (announcement.time != null) {
|
||||||
|
|||||||
+238
@@ -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>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/root"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -11,4 +12,4 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</LinearLayout>
|
||||||
@@ -38,11 +38,12 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!--
|
||||||
<com.futo.platformplayer.views.announcements.AnnouncementView
|
<com.futo.platformplayer.views.announcements.AnnouncementView
|
||||||
android:id="@+id/announcement_view"
|
android:id="@+id/announcement_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" /> -->
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/container_sort_by"
|
android:id="@+id/container_sort_by"
|
||||||
|
|||||||
@@ -46,6 +46,42 @@
|
|||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:srcCompat="@drawable/ic_cast_white_25dp" />
|
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-->
|
<!--Back Button-->
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/button_search"
|
android:id="@+id/button_search"
|
||||||
|
|||||||
@@ -30,11 +30,12 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!--
|
||||||
<com.futo.platformplayer.views.announcements.AnnouncementView
|
<com.futo.platformplayer.views.announcements.AnnouncementView
|
||||||
android:id="@+id/announcement_view"
|
android:id="@+id/announcement_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" /> -->
|
||||||
|
|
||||||
<com.futo.platformplayer.views.others.RadioGroupView
|
<com.futo.platformplayer.views.others.RadioGroupView
|
||||||
android:id="@+id/radio_group"
|
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>
|
||||||
@@ -896,6 +896,7 @@
|
|||||||
<string name="cd_creator_thumbnail">Creator thumbnail</string>
|
<string name="cd_creator_thumbnail">Creator thumbnail</string>
|
||||||
<string name="cd_button_clear_search">Clear search</string>
|
<string name="cd_button_clear_search">Clear search</string>
|
||||||
<string name="cd_button_search">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_search_icon">Search icon</string>
|
||||||
<string name="cd_button_back">Back button</string>
|
<string name="cd_button_back">Back button</string>
|
||||||
<string name="cd_app_icon">App icon</string>
|
<string name="cd_app_icon">App icon</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user