mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a8ac0bfaa | |||
| 772bff6bc0 | |||
| b6b04054b9 | |||
| 1ea794459c | |||
| c27f5e4096 | |||
| 8469f17b4c | |||
| 067abc415b | |||
| d692533f20 | |||
| 31a6ea0f39 | |||
| 5ba2f2be75 | |||
| 8536861e09 | |||
| 71262da3c2 |
@@ -1047,8 +1047,12 @@ class Settings : FragmentedStorageFileJson() {
|
||||
|
||||
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7)
|
||||
var polycentricLocalCache: Boolean = true;
|
||||
|
||||
var showPrivacyModeDialog: Boolean = true;
|
||||
|
||||
|
||||
fun shouldClearWebviewCookies(): Boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
|
||||
|
||||
@@ -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;
|
||||
|
||||
+13
-1
@@ -61,6 +61,11 @@ class SourcePluginConfig(
|
||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, 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? {
|
||||
if(url == null)
|
||||
return null;
|
||||
@@ -165,6 +170,12 @@ class SourcePluginConfig(
|
||||
"Unrestricted Http Header access",
|
||||
"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;
|
||||
}
|
||||
@@ -224,7 +235,8 @@ class SourcePluginConfig(
|
||||
val variable: String? = null,
|
||||
val dependency: String? = null,
|
||||
val warningDialog: String? = null,
|
||||
val options: List<String>? = null
|
||||
val options: List<String>? = null,
|
||||
val isAdvanced: Boolean? = null
|
||||
) {
|
||||
val variableOrName: String get() = variable ?: name;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ import com.caoccao.javet.values.primitive.V8ValueString
|
||||
import com.caoccao.javet.values.reference.IV8ValuePromise
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.caoccao.javet.values.reference.V8ValuePromise
|
||||
import com.futo.platformplayer.BuildConfig
|
||||
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.constructs.Event1
|
||||
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.internal.V8Converter
|
||||
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.PackageHttp
|
||||
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.logging.Logger
|
||||
import com.futo.platformplayer.states.StateAssets
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import com.futo.platformplayer.toList
|
||||
import com.futo.platformplayer.toV8ValueBlocking
|
||||
import com.futo.platformplayer.toV8ValueAsync
|
||||
@@ -218,6 +222,9 @@ class V8Plugin {
|
||||
if(pack is PackageHttp) {
|
||||
pack.cleanup();
|
||||
}
|
||||
else if(pack is PackageBrowser) {
|
||||
pack.deinitialize();
|
||||
}
|
||||
}
|
||||
|
||||
_runtime?.let {
|
||||
@@ -387,6 +394,18 @@ class V8Plugin {
|
||||
"HttpImp" -> PackageHttpImp(this, config)
|
||||
"Utilities" -> PackageUtilities(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}");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -105,6 +105,11 @@ class PackageBridge : V8Package {
|
||||
)
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun hasPackage(str: String): Boolean {
|
||||
return _plugin.getPackages().any { it.name == str };
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun dispose(value: V8Value) {
|
||||
Logger.e(TAG, "Manual dispose: " + value.javaClass.name);
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
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.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.collection.emptyLongSet
|
||||
import com.caoccao.javet.annotations.V8Function
|
||||
import com.caoccao.javet.utils.JavetResourceUtils
|
||||
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
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class PackageBrowser: V8Package {
|
||||
override val name: String get() = "Browser";
|
||||
override val variableName: String = "browser";
|
||||
|
||||
private val _json = Json { };
|
||||
|
||||
@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");
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_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() {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
_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) {
|
||||
try {
|
||||
browser.loadUrl(url);
|
||||
} catch(ex: Throwable) {}
|
||||
}
|
||||
}
|
||||
|
||||
@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, {
|
||||
_plugin.busy {
|
||||
funcClone?.callVoid(null, arrayOf(it));
|
||||
}
|
||||
if (!_plugin.isStopped)
|
||||
JavetResourceUtils.safeClose(funcClone);
|
||||
});
|
||||
}
|
||||
}
|
||||
StateApp.instance.scope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
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", "Browser running failed: " + ex.message, ex);
|
||||
}
|
||||
}
|
||||
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 ?: ""));
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
Logger.i("PackageBrowser", "Invoking V8 with result (${funcClone != null})");
|
||||
try {
|
||||
_plugin.busy {
|
||||
if (value != null) {
|
||||
val json = _json.decodeFromString<String>(value);
|
||||
funcClone?.callVoid(null, arrayOf(json));
|
||||
} else
|
||||
funcClone?.callVoid(null, arrayOf((null as String?)));
|
||||
}
|
||||
if (!_plugin.isStopped)
|
||||
JavetResourceUtils.safeClose(funcClone);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e("PackageBrowser", "Browser Failed to callback: " + ex.message, ex);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e("PackageBrowser", "Browser 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) {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
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.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Spinner
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -21,6 +22,7 @@ class BrowserFragment : MainFragment() {
|
||||
override val isTab: Boolean = false;
|
||||
override val hasBottomBar: Boolean get() = true;
|
||||
|
||||
private var _root: LinearLayout? = null;
|
||||
private var _webview: WebView? = null;
|
||||
private val _webviewWithoutHandling = object: WebViewClient() {
|
||||
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 {
|
||||
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 {
|
||||
this.webViewClient = _webviewWithoutHandling;
|
||||
this.settings.javaScriptEnabled = true;
|
||||
@@ -43,7 +46,12 @@ class BrowserFragment : MainFragment() {
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
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?.loadUrl(parameter);
|
||||
}
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
+2
-2
@@ -266,7 +266,7 @@ class LibraryFragment : MainFragment() {
|
||||
});
|
||||
|
||||
if(this.allowMusic) {
|
||||
val artists = StateLibrary.instance.getArtists(ArtistOrdering.TrackCount);
|
||||
val artists = StateLibrary.instance.getArtists(ArtistOrdering.TrackCount)
|
||||
adapterArtists.setData(artists);
|
||||
if (artists.size == 0)
|
||||
sectionArtists.setEmpty(
|
||||
@@ -289,7 +289,7 @@ class LibraryFragment : MainFragment() {
|
||||
}
|
||||
|
||||
if(this.allowMusic) {
|
||||
val albums = StateLibrary.instance.getAlbums();
|
||||
val albums = StateLibrary.instance.getAlbums()
|
||||
adapterAlbums.setData(albums);
|
||||
if (albums.size == 0)
|
||||
sectionAlbums.setEmpty("No albums", "No albums were found on your device", -1);
|
||||
|
||||
+13
-1
@@ -309,13 +309,14 @@ class SourceDetailFragment : MainFragment() {
|
||||
BigButton(c, context.getString(R.string.logout), context.getString(R.string.sign_out_of_the_platform), R.drawable.ic_logout) {
|
||||
logoutSource();
|
||||
},
|
||||
if(!Settings.instance.other.shouldClearWebviewCookies())
|
||||
BigButton(c, "Logout without Clear", "Logout but keep the browser cookies.\nThis allows for quick re-logging.", R.drawable.ic_logout) {
|
||||
logoutSource(false);
|
||||
}.apply {
|
||||
this.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
|
||||
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||
};
|
||||
}
|
||||
} else null
|
||||
)
|
||||
);
|
||||
|
||||
@@ -518,6 +519,17 @@ class SourceDetailFragment : MainFragment() {
|
||||
}
|
||||
Logger.e(TAG, "Failed to set plugin authentication (loginSource, loginWarning)", e)
|
||||
}
|
||||
finally {
|
||||
if(Settings.instance.other.shouldClearWebviewCookies()) {
|
||||
try {
|
||||
val cookieManager: CookieManager =
|
||||
CookieManager.getInstance();
|
||||
cookieManager.removeAllCookies(null);
|
||||
} catch (ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to clear cookies", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}, UIDialogs.ActionStyle.PRIMARY))
|
||||
}
|
||||
|
||||
+49
@@ -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);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.webkit.CookieManager
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -43,6 +44,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
|
||||
@@ -447,6 +449,16 @@ class StateApp {
|
||||
_cacheDirectory?.let { ApiMethods.initCache(it) };
|
||||
}
|
||||
|
||||
if(Settings.instance.other.shouldClearWebviewCookies()) {
|
||||
try {
|
||||
val cookieManager: CookieManager =
|
||||
CookieManager.getInstance();
|
||||
cookieManager.removeAllCookies(null);
|
||||
} catch (ex: Throwable) {
|
||||
Logger.e(SourceDetailFragment.Companion.TAG, "Failed to clear cookies", ex);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.i(TAG, "MainApp Starting: Initializing [ModerationsManager]");
|
||||
ModerationsManager.initialize(context);
|
||||
|
||||
@@ -732,8 +744,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) {
|
||||
|
||||
@@ -50,7 +50,7 @@ class FieldForm : LinearLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSettingsVisibility(group: GroupField? = null) {
|
||||
fun updateSettingsVisibility(group: GroupField? = null, allowEmptyGroups: Boolean = false) {
|
||||
val settings = group?.getFields() ?: _fields;
|
||||
val query = _editSearch.text.toString().lowercase();
|
||||
|
||||
@@ -58,7 +58,8 @@ class FieldForm : LinearLayout {
|
||||
val isGroupMatch = query.isEmpty() || group?.searchContent?.lowercase()?.contains(query) == true;
|
||||
for(field in settings) {
|
||||
if(field is GroupField) {
|
||||
updateSettingsVisibility(field);
|
||||
if(!allowEmptyGroups)
|
||||
updateSettingsVisibility(field);
|
||||
} else if(field is View && field.descriptor != null) {
|
||||
if(field.isAdvanced && !_showAdvancedSettings)
|
||||
{
|
||||
@@ -73,15 +74,21 @@ class FieldForm : LinearLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(field is View) {
|
||||
if(field.isAdvanced && !_showAdvancedSettings)
|
||||
field.visibility = View.GONE;
|
||||
else
|
||||
field.visibility = VISIBLE;
|
||||
}
|
||||
}
|
||||
if(group != null) {
|
||||
group.visibility = if (groupVisible) View.VISIBLE else View.GONE;
|
||||
}
|
||||
}
|
||||
|
||||
fun setShowAdvancedSettings(show: Boolean) {
|
||||
fun setShowAdvancedSettings(show: Boolean, allowEmptyGroups: Boolean = false) {
|
||||
_showAdvancedSettings = show;
|
||||
updateSettingsVisibility();
|
||||
updateSettingsVisibility(null, allowEmptyGroups);
|
||||
}
|
||||
fun setSearchQuery(query: String) {
|
||||
_editSearch.setText(query);
|
||||
@@ -141,7 +148,9 @@ class FieldForm : LinearLayout {
|
||||
}
|
||||
fun fromPluginSettings(settings: List<SourcePluginConfig.Setting>, values: HashMap<String, String?>, groupTitle: String? = null, groupDescription: String? = null) {
|
||||
_fieldsContainer.removeAllViews();
|
||||
val newFields = getFieldsFromPluginSettings(context, settings, values);
|
||||
val newFields = getFieldsFromPluginSettings(context, settings, values, {
|
||||
setShowAdvancedSettings(it, true);
|
||||
});
|
||||
if (newFields.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -157,6 +166,7 @@ class FieldForm : LinearLayout {
|
||||
_fieldsContainer.addView(v);
|
||||
}
|
||||
_fields = newFields.map { it.second };
|
||||
updateSettingsVisibility(null, true);
|
||||
} else {
|
||||
for(field in newFields) {
|
||||
finalizePluginSettingField(field.first, field.second, newFields);
|
||||
@@ -164,6 +174,8 @@ class FieldForm : LinearLayout {
|
||||
val group = GroupField(context, groupTitle, groupDescription)
|
||||
.withFields(newFields.map { it.second });
|
||||
_fieldsContainer.addView(group as View);
|
||||
_fields = newFields.map { it.second };
|
||||
updateSettingsVisibility(null, true);
|
||||
}
|
||||
}
|
||||
private fun finalizePluginSettingField(setting: SourcePluginConfig.Setting, field: IField, others: List<Pair<SourcePluginConfig.Setting, IField>>) {
|
||||
@@ -234,7 +246,7 @@ class FieldForm : LinearLayout {
|
||||
private val _json = Json;
|
||||
|
||||
|
||||
fun getFieldsFromPluginSettings(context: Context, settings: List<SourcePluginConfig.Setting>, values: HashMap<String, String?>): List<Pair<SourcePluginConfig.Setting, IField>> {
|
||||
fun getFieldsFromPluginSettings(context: Context, settings: List<SourcePluginConfig.Setting>, values: HashMap<String, String?>, onAdvancedChanged: ((newVal: Boolean)->Unit)? = null): List<Pair<SourcePluginConfig.Setting, IField>> {
|
||||
val fields = mutableListOf<Pair<SourcePluginConfig.Setting, IField>>()
|
||||
|
||||
for(setting in settings) {
|
||||
@@ -243,6 +255,7 @@ class FieldForm : LinearLayout {
|
||||
val field = when(setting.type.lowercase()) {
|
||||
"header" -> {
|
||||
val groupField = GroupField(context, setting.name, setting.description);
|
||||
groupField.isAdvanced = (setting.isAdvanced ?: false);
|
||||
groupField;
|
||||
}
|
||||
"boolean" -> {
|
||||
@@ -252,6 +265,7 @@ class FieldForm : LinearLayout {
|
||||
field.onChanged.subscribe { _, v, _ ->
|
||||
values[setting.variableOrName] = _json.encodeToString (v == 1 || v == true);
|
||||
}
|
||||
field.isAdvanced = (setting.isAdvanced ?: false);
|
||||
field;
|
||||
}
|
||||
"dropdown" -> {
|
||||
@@ -261,6 +275,7 @@ class FieldForm : LinearLayout {
|
||||
field.onChanged.subscribe { _, v, _ ->
|
||||
values[setting.variableOrName] = v.toString();
|
||||
}
|
||||
field.isAdvanced = (setting.isAdvanced ?: false);
|
||||
field;
|
||||
}
|
||||
else null;
|
||||
@@ -272,6 +287,17 @@ class FieldForm : LinearLayout {
|
||||
fields.add(Pair(setting, field));
|
||||
}
|
||||
}
|
||||
|
||||
if(onAdvancedChanged != null && settings.any { it.isAdvanced == true }) {
|
||||
val setting = SourcePluginConfig.Setting("Show Advanced", "See advanced settings, which may be counter productive to change", "boolean", "false");
|
||||
val field = ToggleField(context).withValue(setting.name, setting.description, false);
|
||||
|
||||
field.onChanged.subscribe { field, new, old ->
|
||||
onAdvancedChanged?.invoke(new as Boolean);
|
||||
}
|
||||
fields.add(Pair(setting, field));
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
|
||||
+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"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
<LinearLayout
|
||||
android:id="@+id/root"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
@@ -11,4 +12,4 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</LinearLayout>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Submodule app/src/stable/assets/sources/bilibili updated: b153339c93...9186672f0f
Submodule app/src/stable/assets/sources/dailymotion updated: d95df7dca2...70f625a3bd
Submodule app/src/stable/assets/sources/kick updated: 96503584d9...5cae761620
Submodule app/src/stable/assets/sources/rumble updated: 3b51471010...34e0a15016
Submodule app/src/stable/assets/sources/soundcloud updated: 49db9e3e15...e785c5d8c9
Submodule app/src/stable/assets/sources/youtube updated: 079dc6e3dc...b9aae557fd
Submodule app/src/unstable/assets/sources/bilibili updated: b153339c93...9186672f0f
Submodule app/src/unstable/assets/sources/dailymotion updated: d95df7dca2...70f625a3bd
Submodule app/src/unstable/assets/sources/kick updated: 96503584d9...5cae761620
Submodule app/src/unstable/assets/sources/rumble updated: 3b51471010...34e0a15016
Submodule app/src/unstable/assets/sources/soundcloud updated: 49db9e3e15...e785c5d8c9
Submodule app/src/unstable/assets/sources/youtube updated: 079dc6e3dc...b9aae557fd
Reference in New Issue
Block a user