mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-18 05:42:40 +02:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f1c0209a8 | |||
| 819e81b7a6 | |||
| 8193234c2f | |||
| 6263a31f41 | |||
| 481a0cda99 | |||
| b39b89e908 | |||
| ce0f98055f | |||
| 3dddf68766 | |||
| 88d687f26e | |||
| d44df42727 | |||
| 88c8dbcb7c | |||
| b4fddbe26a | |||
| ab6d7669d7 | |||
| 3f22c7f717 | |||
| f36e9588cb | |||
| 8f99f399ee | |||
| 56166a7948 | |||
| 4edd8ee1ea | |||
| a830c918ab | |||
| 53f74c4b6e | |||
| 959c192762 | |||
| 8be7b1272b | |||
| 6b57878275 | |||
| 66c7741c38 | |||
| b370af9d91 | |||
| 40b86cb5de | |||
| 84622e22aa | |||
| 092b20041e | |||
| f6cc00f471 |
@@ -58,3 +58,9 @@
|
||||
[submodule "dep/futopay"]
|
||||
path = dep/futopay
|
||||
url = ../futopayclientlibraries.git
|
||||
[submodule "app/src/unstable/assets/sources/bilibili"]
|
||||
path = app/src/unstable/assets/sources/bilibili
|
||||
url = ../plugins/bilibili.git
|
||||
[submodule "app/src/stable/assets/sources/bilibili"]
|
||||
path = app/src/stable/assets/sources/bilibili
|
||||
url = ../plugins/bilibili.git
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_287_2206)">
|
||||
<path d="M22.0557 38.25L43.1117 6H1L22.0557 38.25Z" fill="url(#paint0_linear_287_2206)"/>
|
||||
<path d="M6 28.2444C6.85811 27.3291 8.98625 25.2353 10.6338 24.1827C12.2814 23.13 14.257 20.1209 15.0388 18.7479C17.4224 15.2392 22.7618 7.91286 25.0501 6.67716C25.462 6.35678 26.0608 5.85718 26.3087 5.64745C27.1668 3.7405 30.0844 0.498738 34.8898 2.78706C35.3017 2.64974 36.32 2.61542 36.7777 2.61542C36.4153 2.86334 35.6564 3.58795 35.5191 4.50328C35.153 7.02039 33.7647 8.48874 33.1164 8.90825C32.6587 11.8259 32.0294 14.4002 30.6564 15.3155L31.915 17.5466C33.8029 19.5489 37.7159 23.8737 38.2649 25.1552C36.4344 24.5603 35.2521 23.992 34.8898 23.7822L38.2649 28.416C36.2818 28.2635 31.8235 26.9744 29.8556 23.0385C30.6336 25.1438 31.4001 27.7677 31.6862 28.8165C30.6183 27.9393 28.3224 25.3955 27.6816 22.2376C27.8647 25.304 27.8342 27.4816 27.7961 28.1872C27.2812 27.7105 26.0913 26.2307 25.4505 24.1255V27.6723C24.6821 26.604 23.1363 24.0104 22.9967 22.0533C23.1255 24.2716 23.047 25.3115 22.9906 25.5556L20.0731 22.8097C19.2912 23.2292 17.1898 24.1827 15.0388 24.6403C13.5743 25.876 11.797 28.969 11.0915 30.3611V28.5877L9.14643 30.5327L9.83291 28.4733L8.57433 29.5602C8.28828 29.7318 7.62468 30.0751 7.25857 30.0751C7.39585 29.7547 7.65904 29.4076 7.77345 29.2741L6.11441 29.9034C6.3051 29.3504 6.90388 28.13 7.77345 27.6723C6.58351 28.13 6.09536 28.2444 6 28.2444Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_287_2206" x1="22.0557" y1="38.25" x2="22.0557" y2="-4.75" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#01D6E6"/>
|
||||
<stop offset="1" stop-color="#0182E7"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_287_2206">
|
||||
<rect width="44" height="44" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -262,6 +262,17 @@ function getDevLogs(lastIndex, cb) {
|
||||
.then(x=>x.json())
|
||||
.then(y=> cb && cb(y));
|
||||
}
|
||||
function getDevHttpExchanges(cb) {
|
||||
fetch("/plugin/getDevHttpExchanges", {
|
||||
timeout: 1000
|
||||
})
|
||||
.then(x=>x.json())
|
||||
.then(y=> cb && cb(y));
|
||||
}
|
||||
function setDevHttpProxy(url, port) {
|
||||
return fetch("/dev/setDevProxy?url=" + encodeURIComponent(url) + "&port=" + port)
|
||||
.then(x=>x.json());
|
||||
}
|
||||
function sendFakeDevLog(devId, msg) {
|
||||
return syncGET("/plugin/fakeDevLog?devId=" + devId + "&msg=" + msg, {});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
<!--<link href="./dependencies/vuetify.min.css" rel="stylesheet">-->
|
||||
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.7.1/dist/vuetify.min.css" rel="stylesheet">
|
||||
|
||||
<title>DevPortal</title>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.svg">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
|
||||
|
||||
<style>
|
||||
@@ -150,7 +153,7 @@
|
||||
.pastPluginUrl {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 500px;
|
||||
width: 700px;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
@@ -160,13 +163,122 @@
|
||||
box-shadow: 0px 1px 2px #131313;
|
||||
font-weight: lighter;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.pastPluginUrl .deleteButton {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
height: 100%;
|
||||
width: 30px;
|
||||
top: 0px;
|
||||
padding-top: 2px;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
transform: scaleX(1.5);
|
||||
}
|
||||
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
#cloakLoader {
|
||||
display: block;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: black;
|
||||
color: white;
|
||||
padding-top: 50px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.httpContainer {
|
||||
position: relative;
|
||||
}
|
||||
.httpLine {
|
||||
}
|
||||
.httpLine .request {
|
||||
height: 50px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.httpLine .request .status {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
width: 40px;
|
||||
top: 10px;
|
||||
padding: 5px;
|
||||
background-color: #333;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.httpLine .request .status.error {
|
||||
background-color: #880000;
|
||||
}
|
||||
.httpLine .request .status.success {
|
||||
background-color: #008800;
|
||||
}
|
||||
.httpLine .request .status.warn {
|
||||
background-color: #803500;
|
||||
}
|
||||
.httpLine .request .method {
|
||||
position: absolute;
|
||||
left: 55px;
|
||||
top: 10px;
|
||||
padding: 5px;
|
||||
background-color: #333;
|
||||
border-radius: 5px;
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
.httpLine .request .url {
|
||||
position: absolute;
|
||||
left: 110px;
|
||||
top: 10px;
|
||||
padding: 5px;
|
||||
background-color: #333;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.httpLine .response {
|
||||
background-color: #111;
|
||||
margin-left: 55px;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
}
|
||||
.httpLine .response .body{
|
||||
white-space: pre-wrap;
|
||||
font-family: monospace;
|
||||
background-color: black;
|
||||
padding: 10px;
|
||||
}
|
||||
.httpLine .response .headers {
|
||||
margin: 10px;
|
||||
}
|
||||
.httpLine .response .headers .key {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
color: #FFF;
|
||||
}
|
||||
.httpLine .response .headers .value {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
color: #AAA;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<v-app>
|
||||
<v-main>
|
||||
<div v-cloak id="cloakLoader" v-if="!page">
|
||||
<h2>Loading..</h2>
|
||||
First load may take longer
|
||||
</div>
|
||||
<v-main v-cloak>
|
||||
<div id="topMenu">
|
||||
<div style="height: 100%; display: inline-block; padding-left: 10px; padding-right: 20px;">
|
||||
<img src="./dependencies/FutoMainLogo.svg"
|
||||
@@ -250,10 +362,13 @@
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="pastPluginUrls" style="margin-top: 60px;">
|
||||
<div v-if="pastPluginUrls" style="margin-top: 60px; margin-left: 25px;">
|
||||
<h2 style="font-weight: lighter; text-align: center;">Past Plugins</h2>
|
||||
<div class="pastPluginUrl" v-for="pastPluginUrl in pastPluginUrls" @click="this.Plugin.newPluginUrl = pastPluginUrl; loadPlugin(pastPluginUrl)">
|
||||
{{pastPluginUrl}}
|
||||
<div class="deleteButton" @click="(ev)=>{ev.stopPropagation(); deletePastPlugin(pastPluginUrl)}">
|
||||
X
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -402,6 +517,11 @@
|
||||
<div class="code">
|
||||
{{req.code}}
|
||||
</div>
|
||||
<div class="documentation" v-if="req.docUrl" style="position: absolute; right: 15px; top: 15px;">
|
||||
<a :href="req.docUrl" target="_blank">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<div class="parameter" v-for="parameter in req.parameters">
|
||||
<div class="name">
|
||||
@@ -500,7 +620,62 @@
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn>Clear</v-btn>
|
||||
<v-btn @click="Integration.logs = []">Clear</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-card style="margin: 20px;" v-if="Plugin.currentPlugin && Integration.httpExchanges">
|
||||
<v-card-title>
|
||||
Http Logs
|
||||
</v-card-title>
|
||||
</v-card-header>
|
||||
<v-card-text>
|
||||
<div style="position: absolute; top: 0px; right: 15px;">
|
||||
<v-checkbox v-model="Integration.showHttpRequests" label="Show Http Requests"></v-checkbox>
|
||||
</div>
|
||||
<div class="httpContainer" v-if="Integration.showHttpRequests">
|
||||
<div class="httpLine" v-for="exchange of Integration.httpExchanges">
|
||||
<div class="request" @click="toggleHttpExchange(exchange)">
|
||||
<div :class="[{ success: exchange.response.status < 300, warn: exchange.response.status >= 300 && exchange.response.status < 400, error: exchange.response.status >= 400 }, 'status']">
|
||||
{{exchange.response.status}}
|
||||
</div>
|
||||
<div class="method">
|
||||
{{exchange.request.method}}
|
||||
</div>
|
||||
<div class="url">
|
||||
{{exchange.request.url}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="response" v-if="exchange.response.show">
|
||||
<h2>Request Headers</h2>
|
||||
<div class="headers">
|
||||
<div class="header" v-for="(headerValue, header) in exchange.request.headers">
|
||||
<div class="key">
|
||||
{{header}}
|
||||
</div>
|
||||
<div class="value">
|
||||
{{headerValue}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Response</h2>
|
||||
<div class="headers">
|
||||
<div class="header" v-for="(headerValue, header) in exchange.response.headers">
|
||||
<div class="key">
|
||||
{{header}}
|
||||
</div>
|
||||
<div class="value">
|
||||
{{headerValue}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">{{exchange.response.body}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="Integration.showHttpRequests" @click="Integration.httpExchanges = []">Clear</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
@@ -538,6 +713,7 @@
|
||||
<!--<script src="./dependencies/vue.js"></script>-->
|
||||
<!--<script src="./dependencies/vuetify.js"></script>-->
|
||||
<script src="./source_docs.js"></script>
|
||||
<script src="./source_doc_urls.js"></script>
|
||||
<script src="./source.js"></script>
|
||||
<script src="./dev_bridge.js"></script>
|
||||
<script>
|
||||
@@ -556,7 +732,9 @@
|
||||
lastLogIndex: -1,
|
||||
lastLogDevID: "",
|
||||
logs: [],
|
||||
lastInjectTime: ""
|
||||
httpExchanges: [],
|
||||
lastInjectTime: "",
|
||||
showHttpRequests: false
|
||||
},
|
||||
Plugin: {
|
||||
loadUsingTag: false,
|
||||
@@ -574,6 +752,9 @@
|
||||
Testing: {
|
||||
requests: sourceDocs.map(x=>{
|
||||
x.parameters.forEach(y=>y.value = null);
|
||||
|
||||
if(sourceDocUrls[x.title])
|
||||
x.docUrl = sourceDocUrls[x.title];
|
||||
return x;
|
||||
}),
|
||||
lastResult: "",
|
||||
@@ -637,6 +818,16 @@
|
||||
});
|
||||
}
|
||||
});
|
||||
if(this.Integration.showHttpRequests) {
|
||||
getDevHttpExchanges((exchanges)=>{
|
||||
Vue.nextTick(()=>{
|
||||
for(i = 0; i < exchanges.length; i++) {
|
||||
exchanges[i].response.show = false;
|
||||
this.Integration.httpExchanges.unshift(exchanges[i]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
catch(ex) {
|
||||
console.error("Failed update", ex);
|
||||
@@ -678,6 +869,12 @@
|
||||
this.reloadPlugin();
|
||||
});
|
||||
},
|
||||
deletePastPlugin(url) {
|
||||
let currentPastPlugins = this.pastPluginUrls;
|
||||
currentPastPlugins = currentPastPlugins.filter(x=>x.toLowerCase() != url.toLowerCase());
|
||||
this.pastPluginUrls = currentPastPlugins;
|
||||
localStorage.setItem("pastPlugins", JSON.stringify(currentPastPlugins));
|
||||
},
|
||||
loginTestPlugin() {
|
||||
pluginLoginTestPlugin();
|
||||
setTimeout(()=>{
|
||||
@@ -913,6 +1110,9 @@
|
||||
},
|
||||
showTestResults(results) {
|
||||
|
||||
},
|
||||
toggleHttpExchange(exchange) {
|
||||
exchange.response.show = !exchange.response.show;
|
||||
},
|
||||
copyClipboard(cpy) {
|
||||
if(navigator.clipboard)
|
||||
|
||||
@@ -357,6 +357,15 @@ class AudioUrlSource {
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
class AudioUrlWidevineSource extends AudioUrlSource {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
this.plugin_type = "AudioUrlWidevineSource";
|
||||
|
||||
this.bearerToken = obj.bearerToken;
|
||||
this.licenseUri = obj.licenseUri;
|
||||
}
|
||||
}
|
||||
class AudioUrlRangeSource extends AudioUrlSource {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
|
||||
@@ -50,12 +50,9 @@ fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
||||
|
||||
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
|
||||
if (!systemState.servers.contains(PolycentricCache.STAGING_SERVER)) {
|
||||
removeServer(PolycentricCache.STAGING_SERVER)
|
||||
}
|
||||
|
||||
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
|
||||
removeServer(PolycentricCache.SERVER)
|
||||
Logger.w("Backfill", "Polycentric prod server not added, adding it.")
|
||||
addServer(PolycentricCache.SERVER)
|
||||
}
|
||||
|
||||
val exceptions = fullyBackfillServers()
|
||||
|
||||
@@ -311,7 +311,10 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
|
||||
var alwaysReloadFromCache: Boolean = false;
|
||||
|
||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 15)
|
||||
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
|
||||
var peekChannelContents: Boolean = false;
|
||||
|
||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
|
||||
fun clearChannelCache() {
|
||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
||||
StateCache.instance.clear();
|
||||
@@ -546,6 +549,8 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@DropdownFieldOptionsId(R.array.log_levels)
|
||||
var logLevel: Int = 0;
|
||||
|
||||
fun isVerbose() = logLevel >= 4;
|
||||
|
||||
@FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1)
|
||||
fun submitLogs() {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
|
||||
@@ -31,12 +32,17 @@ import com.futo.platformplayer.dialogs.ConnectedCastingDialog
|
||||
import com.futo.platformplayer.dialogs.ImportDialog
|
||||
import com.futo.platformplayer.dialogs.ImportOptionsDialog
|
||||
import com.futo.platformplayer.dialogs.MigrateDialog
|
||||
import com.futo.platformplayer.dialogs.PluginUpdateDialog
|
||||
import com.futo.platformplayer.dialogs.ProgressDialog
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import com.futo.platformplayer.views.ToastView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -184,6 +190,14 @@ class UIDialogs {
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
fun showPluginUpdateDialog(context: Context, oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig) {
|
||||
val dialog = PluginUpdateDialog(context, oldConfig, newConfig);
|
||||
registerDialogOpened(dialog);
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
|
||||
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
|
||||
val builder = AlertDialog.Builder(context);
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
||||
@@ -269,22 +283,48 @@ class UIDialogs {
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
}
|
||||
fun showGeneralRetryErrorDialog(context: Context, msg: String, ex: Throwable? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null) {
|
||||
fun showGeneralRetryErrorDialog(context: Context, msg: String, ex: Throwable? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null, mainFragment: MainFragment? = null) {
|
||||
val pluginConfig = if(ex is PluginException) ex.config else null;
|
||||
val pluginInfo = if(ex is PluginException)
|
||||
"\nPlugin [${ex.config.name}]" else "";
|
||||
showDialog(context,
|
||||
R.drawable.ic_error_pred,
|
||||
"${msg}${pluginInfo}",
|
||||
(if(ex != null ) "${ex.message}" else ""),
|
||||
if(ex is PluginException) ex.code else null,
|
||||
0,
|
||||
UIDialogs.Action(context.getString(R.string.retry), {
|
||||
retryAction?.invoke();
|
||||
}, UIDialogs.ActionStyle.PRIMARY),
|
||||
UIDialogs.Action(context.getString(R.string.close), {
|
||||
closeAction?.invoke()
|
||||
}, UIDialogs.ActionStyle.NONE)
|
||||
);
|
||||
|
||||
var exMsg = if(ex != null ) "${ex.message}" else "";
|
||||
if(pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
|
||||
exMsg += "\n\nAn update is available"
|
||||
|
||||
if(mainFragment != null && pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
|
||||
showDialog(context,
|
||||
R.drawable.ic_error_pred,
|
||||
"${msg}${pluginInfo}",
|
||||
exMsg,
|
||||
if(ex is PluginException) ex.code else null,
|
||||
1,
|
||||
UIDialogs.Action(context.getString(R.string.update), {
|
||||
mainFragment.navigate<SourceDetailFragment>(SourceDetailFragment.UpdatePluginAction(pluginConfig));
|
||||
if(mainFragment is VideoDetailFragment)
|
||||
mainFragment.minimizeVideoDetail();
|
||||
}, UIDialogs.ActionStyle.ACCENT),
|
||||
UIDialogs.Action(context.getString(R.string.close), {
|
||||
closeAction?.invoke()
|
||||
}, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action(context.getString(R.string.retry), {
|
||||
retryAction?.invoke();
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
else
|
||||
showDialog(context,
|
||||
R.drawable.ic_error_pred,
|
||||
"${msg}${pluginInfo}",
|
||||
exMsg,
|
||||
if(ex is PluginException) ex.code else null,
|
||||
0,
|
||||
UIDialogs.Action(context.getString(R.string.close), {
|
||||
closeAction?.invoke()
|
||||
}, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action(context.getString(R.string.retry), {
|
||||
retryAction?.invoke();
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
}
|
||||
|
||||
fun showSingleButtonDialog(context: Context, icon: Int, text: String, buttonText: String, action: (() -> Unit)) {
|
||||
|
||||
@@ -510,10 +510,15 @@ class UISlideOverlays {
|
||||
}
|
||||
}
|
||||
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
|
||||
showUnknownVideoDownload(container.context.getString(R.string.video), container) { px, bitrate ->
|
||||
showUnknownVideoDownload(container.context.getString(R.string.playlist), container) { px, bitrate ->
|
||||
StateDownloads.instance.download(playlist, px, bitrate);
|
||||
};
|
||||
}
|
||||
fun showDownloadWatchlaterOverlay(container: ViewGroup) {
|
||||
showUnknownVideoDownload(container.context.getString(R.string.watch_later), container, { px, bitrate ->
|
||||
StateDownloads.instance.downloadWatchLater(px, bitrate);
|
||||
})
|
||||
}
|
||||
private fun showUnknownVideoDownload(toDownload: String, container: ViewGroup, cb: (Long?, Long?)->Unit) {
|
||||
val items = arrayListOf<View>();
|
||||
var menu: SlideUpMenuOverlay? = null;
|
||||
@@ -783,8 +788,8 @@ class UISlideOverlays {
|
||||
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
|
||||
}
|
||||
|
||||
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters {
|
||||
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues);
|
||||
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
|
||||
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
|
||||
overlay.show();
|
||||
return overlay;
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ class AddSourceActivity : AppCompatActivity() {
|
||||
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
|
||||
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
||||
if(it) {
|
||||
StatePlatform.instance.clearUpdateAvailable(config)
|
||||
StatePlugins.instance.clearUpdateAvailable(config)
|
||||
if(isNew)
|
||||
lifecycleScope.launch {
|
||||
StatePlatform.instance.enableClient(listOf(config.id));
|
||||
|
||||
@@ -48,6 +48,7 @@ import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import com.futo.platformplayer.views.ToastView
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.google.gson.JsonParser
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import kotlinx.coroutines.*
|
||||
@@ -154,6 +155,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}
|
||||
|
||||
constructor() : super() {
|
||||
ApiMethods.UserAgent = "Grayjay Android (${BuildConfig.VERSION_CODE})";
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
|
||||
val writer = StringWriter();
|
||||
|
||||
@@ -192,6 +195,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Logger.i(TAG, "MainActivity Starting");
|
||||
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
||||
StateApp.instance.mainAppStarting(this);
|
||||
|
||||
|
||||
+2
-1
@@ -12,6 +12,7 @@ import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
@@ -77,7 +78,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||
}
|
||||
|
||||
processHandle.addServer("https://srv1-stg.polycentric.io");
|
||||
processHandle.addServer(PolycentricCache.SERVER);
|
||||
processHandle.setUsername(username);
|
||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||
} catch (e: Throwable) {
|
||||
|
||||
@@ -60,6 +60,9 @@ class CachedPlatformClient : IPlatformClient {
|
||||
filters: Map<String, List<String>>?
|
||||
): IPager<IPlatformContent> = _client.getChannelContents(channelUrl);
|
||||
|
||||
override fun getPeekChannelTypes(): List<String> = _client.getPeekChannelTypes();
|
||||
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = _client.peekChannelContents(channelUrl, type);
|
||||
|
||||
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String? = _client.getChannelUrlByClaim(claimType, claimValues)
|
||||
|
||||
override fun searchSuggestions(query: String): Array<String> = _client.searchSuggestions(query);
|
||||
|
||||
@@ -84,6 +84,15 @@ interface IPlatformClient {
|
||||
*/
|
||||
fun getChannelContents(channelUrl: String, type: String? = null, order: String? = null, filters: Map<String, List<String>>? = null): IPager<IPlatformContent>;
|
||||
|
||||
/**
|
||||
* Describes what the plugin is capable on peek channel results
|
||||
*/
|
||||
fun getPeekChannelTypes(): List<String>;
|
||||
/**
|
||||
* Peeks contents of a channel, upload time descending
|
||||
*/
|
||||
fun peekChannelContents(channelUrl: String, type: String? = null): List<IPlatformContent>
|
||||
|
||||
/**
|
||||
* Gets the channel url associated with a claimType
|
||||
*/
|
||||
|
||||
@@ -13,10 +13,12 @@ data class PlatformClientCapabilities(
|
||||
val hasGetChannelUrlByClaim: Boolean = false,
|
||||
val hasGetChannelTemplateByClaimMap: Boolean = false,
|
||||
val hasGetSearchCapabilities: Boolean = false,
|
||||
val hasGetSearchChannelContentsCapabilities: Boolean = false,
|
||||
val hasGetChannelCapabilities: Boolean = false,
|
||||
val hasGetLiveEvents: Boolean = false,
|
||||
val hasGetLiveChatWindow: Boolean = false,
|
||||
val hasGetContentChapters: Boolean = false
|
||||
val hasGetContentChapters: Boolean = false,
|
||||
val hasPeekChannelContents: Boolean = false
|
||||
) {
|
||||
|
||||
}
|
||||
@@ -14,7 +14,7 @@ open class PlatformAuthorLink {
|
||||
val id: PlatformID;
|
||||
val name: String;
|
||||
val url: String;
|
||||
val thumbnail: String?;
|
||||
var thumbnail: String?;
|
||||
var subscribers: Long? = null; //Optional
|
||||
|
||||
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null)
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.futo.platformplayer.api.media.models
|
||||
import com.caoccao.javet.values.reference.V8ValueArray
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8PluginConfig
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
@@ -31,7 +33,7 @@ class Thumbnails {
|
||||
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
|
||||
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
|
||||
.toArray()
|
||||
.map { Thumbnail.fromV8(it as V8ValueObject) }
|
||||
.map { Thumbnail.fromV8(config, it as V8ValueObject) }
|
||||
.toTypedArray());
|
||||
}
|
||||
}
|
||||
@@ -40,10 +42,10 @@ class Thumbnails {
|
||||
data class Thumbnail(val url : String?, val quality : Int = 0) {
|
||||
|
||||
companion object {
|
||||
fun fromV8(value: V8ValueObject): Thumbnail {
|
||||
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnail {
|
||||
return Thumbnail(
|
||||
value.getString("url"),
|
||||
value.getInteger("quality"));
|
||||
value.getOrDefault<String>(config,"url", "Thumbnail", null),
|
||||
value.getOrDefault(config, "quality", "Thumbnail", 0) ?: 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
interface IAudioUrlWidevineSource : IAudioUrlSource {
|
||||
val bearerToken: String
|
||||
val licenseUri: String
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSOptional
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
|
||||
@@ -45,6 +46,7 @@ import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -56,8 +58,10 @@ import com.futo.platformplayer.states.StatePlugins
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.Exception
|
||||
import kotlin.reflect.full.findAnnotations
|
||||
import kotlin.reflect.jvm.kotlinFunction
|
||||
import kotlin.streams.asSequence
|
||||
|
||||
open class JSClient : IPlatformClient {
|
||||
val config: SourcePluginConfig;
|
||||
@@ -73,6 +77,7 @@ open class JSClient : IPlatformClient {
|
||||
private var _searchCapabilities: ResultCapabilities? = null;
|
||||
private var _searchChannelContentsCapabilities: ResultCapabilities? = null;
|
||||
private var _channelCapabilities: ResultCapabilities? = null;
|
||||
private var _peekChannelTypes: List<String>? = null;
|
||||
|
||||
protected val _script: String;
|
||||
|
||||
@@ -91,7 +96,11 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
private val _busyLock = Object();
|
||||
private var _busyCounter = 0;
|
||||
private var _busyAction = "";
|
||||
val isBusy: Boolean get() = _busyCounter > 0;
|
||||
val isBusyAction: String get() {
|
||||
return _busyAction;
|
||||
}
|
||||
|
||||
val settings: HashMap<String, String?> get() = descriptor.settings;
|
||||
|
||||
@@ -150,6 +159,8 @@ open class JSClient : IPlatformClient {
|
||||
if(it is ScriptCaptchaRequiredException)
|
||||
onCaptchaException.emit(this, it);
|
||||
};
|
||||
|
||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||
}
|
||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
||||
this._context = context;
|
||||
@@ -173,6 +184,8 @@ open class JSClient : IPlatformClient {
|
||||
if(it is ScriptCaptchaRequiredException)
|
||||
onCaptchaException.emit(this, it);
|
||||
};
|
||||
|
||||
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||
}
|
||||
|
||||
open fun getCopy(): JSClient {
|
||||
@@ -214,9 +227,11 @@ open class JSClient : IPlatformClient {
|
||||
hasGetChannelTemplateByClaimMap = plugin.executeBoolean("!!source.getChannelTemplateByClaimMap") ?: false,
|
||||
hasGetSearchCapabilities = plugin.executeBoolean("!!source.getSearchCapabilities") ?: false,
|
||||
hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false,
|
||||
hasGetSearchChannelContentsCapabilities = plugin.executeBoolean("!!source.getSearchChannelContentsCapabilities") ?: false,
|
||||
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
|
||||
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
|
||||
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
||||
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -260,7 +275,7 @@ open class JSClient : IPlatformClient {
|
||||
}
|
||||
|
||||
@JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform")
|
||||
override fun getHome(): IPager<IPlatformContent> = isBusyWith {
|
||||
override fun getHome(): IPager<IPlatformContent> = isBusyWith("getHome") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSContentPager(config, this,
|
||||
plugin.executeTyped("source.getHome()"));
|
||||
@@ -268,7 +283,7 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
|
||||
@JSDocsParameter("query", "Query to complete suggestions for")
|
||||
override fun searchSuggestions(query: String): Array<String> = isBusyWith {
|
||||
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith plugin.executeTyped<V8ValueArray>("source.searchSuggestions(${Json.encodeToString(query)})")
|
||||
.toArray()
|
||||
@@ -298,7 +313,7 @@ open class JSClient : IPlatformClient {
|
||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
||||
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
||||
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("search") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSContentPager(config, this,
|
||||
plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||
@@ -306,6 +321,9 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
@JSDocs(4, "source.getSearchChannelContentsCapabilities()", "Gets capabilities this plugin has for search videos")
|
||||
override fun getSearchChannelContentsCapabilities(): ResultCapabilities {
|
||||
if(!capabilities.hasGetSearchChannelContentsCapabilities)
|
||||
return ResultCapabilities(listOf(ResultCapabilities.TYPE_MIXED));
|
||||
|
||||
ensureEnabled();
|
||||
if (_searchChannelContentsCapabilities != null)
|
||||
return _searchChannelContentsCapabilities!!;
|
||||
@@ -319,7 +337,7 @@ open class JSClient : IPlatformClient {
|
||||
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
|
||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
||||
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchChannelContents") {
|
||||
ensureEnabled();
|
||||
if(!capabilities.hasSearchChannelContents)
|
||||
throw IllegalStateException("This plugin does not support channel search");
|
||||
@@ -331,7 +349,7 @@ open class JSClient : IPlatformClient {
|
||||
@JSOptional
|
||||
@JSDocs(5, "source.searchChannels(query)", "Searches for channels on the platform")
|
||||
@JSDocsParameter("query", "Query that channels should match")
|
||||
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith {
|
||||
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith("searchChannels") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSChannelPager(config, this,
|
||||
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
||||
@@ -351,7 +369,7 @@ open class JSClient : IPlatformClient {
|
||||
}
|
||||
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
|
||||
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
||||
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith {
|
||||
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith("getChannel") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSChannel(config,
|
||||
plugin.executeTyped("source.getChannel(${Json.encodeToString(channelUrl)})"));
|
||||
@@ -378,12 +396,46 @@ open class JSClient : IPlatformClient {
|
||||
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
||||
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("getChannelContents") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSContentPager(config, this,
|
||||
plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||
}
|
||||
|
||||
@JSDocs(10, "source.getPeekChannelTypes()", "Gets types this plugin has for peek channel contents")
|
||||
override fun getPeekChannelTypes(): List<String> {
|
||||
if(!capabilities.hasPeekChannelContents)
|
||||
return listOf();
|
||||
try {
|
||||
if (_peekChannelTypes != null) {
|
||||
return _peekChannelTypes!!;
|
||||
}
|
||||
val arr: V8ValueArray = plugin.executeTyped("source.getPeekChannelTypes()");
|
||||
|
||||
_peekChannelTypes = arr.keys.mapNotNull {
|
||||
val str = arr.get<V8ValueString>(it);
|
||||
return@mapNotNull str.value;
|
||||
};
|
||||
return _peekChannelTypes ?: listOf();
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
announcePluginUnhandledException("getPeekChannelTypes", ex);
|
||||
return listOf();
|
||||
}
|
||||
}
|
||||
@JSDocs(10, "source.peekChannelContents(url, type)", "Peek contents of a channel (reverse chronological order)")
|
||||
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
||||
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
||||
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = isBusyWith("peekChannelContents") {
|
||||
ensureEnabled();
|
||||
|
||||
val items: V8ValueArray = plugin.executeTyped("source.peekChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)})");
|
||||
return@isBusyWith items.keys.mapNotNull {
|
||||
val obj = items.get<V8ValueObject>(it);
|
||||
return@mapNotNull IJSContent.fromV8(this, obj);
|
||||
};
|
||||
}
|
||||
|
||||
@JSOptional
|
||||
@JSDocs(11, "source.getChannelUrlByClaim(claimType, claimValues)", "Gets the channel url that should be used to fetch a given polycentric claim")
|
||||
@JSDocsParameter("claimType", "Polycentric claimtype id")
|
||||
@@ -444,7 +496,7 @@ open class JSClient : IPlatformClient {
|
||||
}
|
||||
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
|
||||
@JSDocsParameter("url", "A content url (this platform)")
|
||||
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith {
|
||||
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith("getContentDetails") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith IJSContentDetails.fromV8(this,
|
||||
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
|
||||
@@ -453,7 +505,7 @@ open class JSClient : IPlatformClient {
|
||||
@JSOptional //getContentChapters = function(url, initialData)
|
||||
@JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details")
|
||||
@JSDocsParameter("url", "A content url (this platform)")
|
||||
override fun getContentChapters(url: String): List<IChapter> = isBusyWith {
|
||||
override fun getContentChapters(url: String): List<IChapter> = isBusyWith("getContentChapters") {
|
||||
if(!capabilities.hasGetContentChapters)
|
||||
return@isBusyWith listOf();
|
||||
ensureEnabled();
|
||||
@@ -464,7 +516,7 @@ open class JSClient : IPlatformClient {
|
||||
@JSOptional
|
||||
@JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url")
|
||||
@JSDocsParameter("url", "A content url (this platform)")
|
||||
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith {
|
||||
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith("getPlaybackTracker") {
|
||||
if(!capabilities.hasGetPlaybackTracker)
|
||||
return@isBusyWith null;
|
||||
ensureEnabled();
|
||||
@@ -478,7 +530,7 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
@JSDocs(16, "source.getComments(url)", "Gets comments for a content by its url")
|
||||
@JSDocsParameter("url", "A content url (this platform)")
|
||||
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith {
|
||||
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith("getComments") {
|
||||
ensureEnabled();
|
||||
val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})");
|
||||
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
|
||||
@@ -496,7 +548,7 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
@JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
|
||||
@JSDocsParameter("url", "Url of live stream")
|
||||
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith {
|
||||
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith("getLiveChatWindow") {
|
||||
if(!capabilities.hasGetLiveChatWindow)
|
||||
return@isBusyWith null;
|
||||
ensureEnabled();
|
||||
@@ -505,7 +557,7 @@ open class JSClient : IPlatformClient {
|
||||
}
|
||||
@JSDocs(16, "source.getLiveEvents(url)", "Gets live events for a livestream")
|
||||
@JSDocsParameter("url", "Url of live stream")
|
||||
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith {
|
||||
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith("getLiveEvents") {
|
||||
if(!capabilities.hasGetLiveEvents)
|
||||
return@isBusyWith null;
|
||||
ensureEnabled();
|
||||
@@ -518,7 +570,7 @@ open class JSClient : IPlatformClient {
|
||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
||||
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
||||
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchPlaylists") {
|
||||
ensureEnabled();
|
||||
if(!capabilities.hasSearchPlaylists)
|
||||
throw IllegalStateException("This plugin does not support playlist search");
|
||||
@@ -528,15 +580,22 @@ open class JSClient : IPlatformClient {
|
||||
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
|
||||
@JSDocsParameter("url", "Url of playlist")
|
||||
override fun isPlaylistUrl(url: String): Boolean {
|
||||
ensureEnabled();
|
||||
if (!capabilities.hasGetPlaylist)
|
||||
return false;
|
||||
return plugin.executeBoolean("source.isPlaylistUrl(${Json.encodeToString(url)})") ?: false;
|
||||
|
||||
try {
|
||||
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
|
||||
.value;
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
announcePluginUnhandledException("isPlaylistUrl", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@JSOptional
|
||||
@JSDocs(21, "source.getPlaylist(url)", "Gets the playlist of the current user")
|
||||
@JSDocsParameter("url", "Url of playlist")
|
||||
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith {
|
||||
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith("getPlaylist") {
|
||||
ensureEnabled();
|
||||
return@isBusyWith JSPlaylistDetails(this, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
|
||||
}
|
||||
@@ -633,19 +692,24 @@ open class JSClient : IPlatformClient {
|
||||
}
|
||||
|
||||
|
||||
private fun <T> isBusyWith(handle: ()->T): T {
|
||||
private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
|
||||
try {
|
||||
synchronized(_busyLock) {
|
||||
_busyCounter++;
|
||||
}
|
||||
_busyAction = actionName;
|
||||
return handle();
|
||||
}
|
||||
finally {
|
||||
_busyAction = "";
|
||||
synchronized(_busyLock) {
|
||||
_busyCounter--;
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun <T> isBusyWith(handle: ()->T): T {
|
||||
return isBusyWith("Unknown", handle);
|
||||
}
|
||||
|
||||
private fun announcePluginUnhandledException(method: String, ex: Throwable) {
|
||||
if(ex is PluginEngineException)
|
||||
@@ -662,10 +726,43 @@ open class JSClient : IPlatformClient {
|
||||
|
||||
companion object {
|
||||
val TAG = "JSClient";
|
||||
private val _lock = Object();
|
||||
private var _docs: Map<String, String>? = null;
|
||||
|
||||
fun getMethodDocs(names: List<String>): Map<String, String>? {
|
||||
synchronized(_lock) {
|
||||
if(_docs == null) {
|
||||
val client = ManagedHttpClient();
|
||||
val docs = names
|
||||
.map { stringWithoutBrackets(it) }
|
||||
.distinct()
|
||||
.parallelStream()
|
||||
.map {
|
||||
val url = "https://github.com/futo-org/grayjay-android/blob/master/docs/source/${it}.md";
|
||||
val resp = client.head(url);
|
||||
if(resp.isOk)
|
||||
return@map Pair(it, url);
|
||||
else
|
||||
return@map null;
|
||||
}.asSequence()
|
||||
.filterNotNull()
|
||||
.toMap();
|
||||
_docs = docs;
|
||||
}
|
||||
return _docs;
|
||||
}
|
||||
}
|
||||
fun getMethodDocUrls(): Map<String, String>? {
|
||||
if(_docs != null)
|
||||
return _docs;
|
||||
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
|
||||
return getMethodDocs(methods.map { it.name });
|
||||
}
|
||||
|
||||
fun getJSDocs(): List<JSCallDocs> {
|
||||
val docs = mutableListOf<JSCallDocs>();
|
||||
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
|
||||
|
||||
for(method in methods.sortedBy { it.getAnnotation(JSDocs::class.java)?.order }) {
|
||||
val doc = method.getAnnotation(JSDocs::class.java);
|
||||
val parameters = method.kotlinFunction!!.findAnnotations<JSDocsParameter>();
|
||||
@@ -678,5 +775,12 @@ open class JSClient : IPlatformClient {
|
||||
}
|
||||
return docs;
|
||||
}
|
||||
|
||||
private fun stringWithoutBrackets(name: String): String {
|
||||
val index = name.indexOf('(');
|
||||
if(index >= 0)
|
||||
return name.substring(0, index);
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
-1
@@ -45,7 +45,9 @@ class SourcePluginConfig(
|
||||
var enableInSearch: Boolean = true,
|
||||
var enableInHome: Boolean = true,
|
||||
var supportedClaimTypes: List<Int> = listOf(),
|
||||
var primaryClaimFieldType: Int? = null
|
||||
var primaryClaimFieldType: Int? = null,
|
||||
var developerSubmitUrl: String? = null,
|
||||
var allowAllHttpHeaderAccess: Boolean = false,
|
||||
) : IV8PluginConfig {
|
||||
|
||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||
@@ -79,6 +81,44 @@ class SourcePluginConfig(
|
||||
return _allowUrlsLowerVal!!;
|
||||
};
|
||||
|
||||
fun isLowRiskUpdate(oldScript: String, newConfig: SourcePluginConfig, newScript: String): Boolean{
|
||||
//New allow header access
|
||||
if(!allowAllHttpHeaderAccess && newConfig.allowAllHttpHeaderAccess)
|
||||
return false;
|
||||
|
||||
//All urls should already be allowed
|
||||
for(url in newConfig.allowUrls) {
|
||||
if(!allowUrls.contains(url))
|
||||
return false;
|
||||
}
|
||||
//All packages should already be allowed
|
||||
for(pack in newConfig.packages) {
|
||||
if(!packages.contains(pack))
|
||||
return false;
|
||||
}
|
||||
//Developer Submit Url should be same or empty
|
||||
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
|
||||
return false;
|
||||
|
||||
//Should have a public key
|
||||
if(scriptPublicKey.isNullOrEmpty() || scriptSignature.isNullOrEmpty())
|
||||
return false;
|
||||
|
||||
//Should be same public key
|
||||
if(scriptPublicKey != newConfig.scriptPublicKey)
|
||||
return false;
|
||||
|
||||
//Old signature should be valid
|
||||
if(!validate(oldScript))
|
||||
return false;
|
||||
|
||||
//New signature should be valid
|
||||
if(!newConfig.validate(newScript))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fun getWarnings(scriptToCheck: String? = null) : List<Pair<String,String>> {
|
||||
val list = mutableListOf<Pair<String,String>>();
|
||||
|
||||
@@ -107,6 +147,11 @@ class SourcePluginConfig(
|
||||
list.add(Pair(
|
||||
"Unrestricted Web Access",
|
||||
"This plugin requires access to all URLs, this may include malicious URLs."));
|
||||
if(allowAllHttpHeaderAccess)
|
||||
list.add(Pair(
|
||||
"Unrestricted Http Header access",
|
||||
"Allows this plugin to access all headers (including cookies and authorization headers) for unauthenticated requests."
|
||||
))
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
+9
-1
@@ -8,6 +8,7 @@ import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.views.fields.DropdownFieldOptions
|
||||
import com.futo.platformplayer.views.fields.FieldForm
|
||||
import com.futo.platformplayer.views.fields.FormField
|
||||
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@@ -90,8 +91,10 @@ class SourcePluginDescriptor {
|
||||
@Serializable
|
||||
class AppPluginSettings {
|
||||
|
||||
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, 1)
|
||||
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, -1)
|
||||
var checkForUpdates: Boolean = true;
|
||||
@FormField(R.string.automatic_update_setting, FieldForm.TOGGLE, R.string.automatic_update_setting_description, 0)
|
||||
var automaticUpdate: Boolean = false;
|
||||
|
||||
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
|
||||
var tabEnabled = TabEnabled();
|
||||
@@ -130,6 +133,11 @@ class SourcePluginDescriptor {
|
||||
}
|
||||
|
||||
|
||||
|
||||
@FormField(R.string.allow_developer_submit, FieldForm.TOGGLE, R.string.allow_developer_submit_description, 1, "devSubmit")
|
||||
var allowDeveloperSubmit: Boolean = false;
|
||||
|
||||
|
||||
fun loadDefaults(config: SourcePluginConfig) {
|
||||
if(tabEnabled.enableHome == null)
|
||||
tabEnabled.enableHome = config.enableInHome
|
||||
|
||||
@@ -14,6 +14,6 @@ annotation class JSOptional()
|
||||
annotation class JSDocsParameter(val name: String, val description: String, val order: Int = 0)
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false);
|
||||
data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false, val docsUrl: String? = null);
|
||||
@kotlinx.serialization.Serializable
|
||||
data class JSParameterDocs(val name: String, val description: String);
|
||||
+27
-1
@@ -2,14 +2,22 @@ package com.futo.platformplayer.api.media.platforms.js.internal
|
||||
|
||||
import android.net.Uri
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
|
||||
import com.futo.platformplayer.developer.DeveloperEndpoints
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.matchesDomain
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import com.google.common.net.MediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okio.GzipSource
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.util.UUID
|
||||
|
||||
class JSHttpClient : ManagedHttpClient {
|
||||
@@ -28,7 +36,15 @@ class JSHttpClient : ManagedHttpClient {
|
||||
private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
|
||||
private var _otherCookieMap: HashMap<String, HashMap<String, String>>;
|
||||
|
||||
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super() {
|
||||
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super(
|
||||
//Temporary ugly solution for DevPortal proxy support
|
||||
(if(jsClient?.config?.id == StateDeveloper.DEV_ID && StateDeveloper.instance.devProxy != null)
|
||||
OkHttpClient.Builder().proxy(Proxy(Proxy.Type.HTTP,
|
||||
InetSocketAddress(StateDeveloper.instance.devProxy!!.url, StateDeveloper.instance.devProxy!!.port)
|
||||
))
|
||||
else
|
||||
OkHttpClient.Builder())
|
||||
) {
|
||||
_jsClient = jsClient;
|
||||
_jsConfig = config;
|
||||
_auth = auth;
|
||||
@@ -201,6 +217,16 @@ class JSHttpClient : ManagedHttpClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(_jsClient is DevJSClient) {
|
||||
//val peekBody = resp.peekBody(1000 * 1000).string();
|
||||
StateDeveloper.instance.addDevHttpExchange(
|
||||
StateDeveloper.DevHttpExchange(
|
||||
StateDeveloper.DevHttpRequest(resp.request.method, resp.request.url.toString(), mapOf(*resp.request.headers.map { Pair(it.first, it.second) }.toTypedArray()), ""),
|
||||
StateDeveloper.DevHttpRequest("RESP", resp.request.url.toString(), mapOf(*resp.headers.map { Pair(it.first, it.second) }.toTypedArray()), "", resp.code)
|
||||
));
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
|
||||
override val bearerToken: String
|
||||
override val licenseUri: String
|
||||
|
||||
@Suppress("ConvertSecondaryConstructorToPrimary")
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
|
||||
val contextName = "JSAudioUrlWidevineSource"
|
||||
val config = plugin.config
|
||||
bearerToken = _obj.getOrThrow(config, "bearerToken", contextName)
|
||||
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val url = getAudioUrl()
|
||||
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration, bearerToken=$bearerToken, licenseUri=$licenseUri)"
|
||||
}
|
||||
}
|
||||
+2
@@ -66,6 +66,7 @@ abstract class JSSource {
|
||||
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
|
||||
const val TYPE_DASH = "DashSource";
|
||||
const val TYPE_HLS = "HLSSource";
|
||||
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
|
||||
|
||||
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
|
||||
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource {
|
||||
@@ -88,6 +89,7 @@ abstract class JSSource {
|
||||
return when(type) {
|
||||
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
|
||||
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
|
||||
TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(plugin, obj);
|
||||
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
|
||||
else -> throw NotImplementedError("Unknown type ${type}");
|
||||
}
|
||||
|
||||
@@ -163,24 +163,25 @@ class AirPlayCastingDevice : CastingDevice {
|
||||
}
|
||||
|
||||
connectionState = CastConnectionState.CONNECTED;
|
||||
delay(1000);
|
||||
|
||||
val progressIndex = progressInfo.lowercase().indexOf("position: ");
|
||||
if (progressIndex == -1) {
|
||||
delay(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
val progress = progressInfo.substring(progressIndex + "position: ".length).toDoubleOrNull() ?: continue;
|
||||
setTime(progress);
|
||||
|
||||
|
||||
val durationIndex = progressInfo.lowercase().indexOf("duration: ");
|
||||
if (durationIndex == -1) {
|
||||
delay(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
val duration = progressInfo.substring(durationIndex + "duration: ".length).toDoubleOrNull() ?: continue;
|
||||
setDuration(duration);
|
||||
delay(1000);
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to get server info from AirPlay device.", e)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,9 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
|
||||
private var _socket: SSLSocket? = null;
|
||||
private var _outputStream: DataOutputStream? = null;
|
||||
private var _outputStreamLock = Object();
|
||||
private var _inputStream: DataInputStream? = null;
|
||||
private var _inputStreamLock = Object();
|
||||
private var _scopeIO: CoroutineScope? = null;
|
||||
private var _requestId = 1;
|
||||
private var _started: Boolean = false;
|
||||
@@ -383,39 +385,44 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
|
||||
getStatus();
|
||||
|
||||
val buffer = ByteArray(4096);
|
||||
val buffer = ByteArray(409600);
|
||||
|
||||
Logger.i(TAG, "Started receiving.");
|
||||
while (_scopeIO?.isActive == true) {
|
||||
try {
|
||||
val inputStream = _inputStream ?: break;
|
||||
Log.d(TAG, "Receiving next packet...");
|
||||
val b1 = inputStream.readUnsignedByte();
|
||||
val b2 = inputStream.readUnsignedByte();
|
||||
val b3 = inputStream.readUnsignedByte();
|
||||
val b4 = inputStream.readUnsignedByte();
|
||||
val size = ((b1.toLong() shl 24) or (b2.toLong() shl 16) or (b3.toLong() shl 8) or b4.toLong()).toInt();
|
||||
if (size > buffer.size) {
|
||||
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
|
||||
inputStream.skip(size.toLong());
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
||||
inputStream.read(buffer, 0, size);
|
||||
synchronized(_inputStreamLock)
|
||||
{
|
||||
Log.d(TAG, "Receiving next packet...");
|
||||
val b1 = inputStream.readUnsignedByte();
|
||||
val b2 = inputStream.readUnsignedByte();
|
||||
val b3 = inputStream.readUnsignedByte();
|
||||
val b4 = inputStream.readUnsignedByte();
|
||||
val size =
|
||||
((b1.toLong() shl 24) or (b2.toLong() shl 16) or (b3.toLong() shl 8) or b4.toLong()).toInt();
|
||||
if (size > buffer.size) {
|
||||
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
|
||||
inputStream.skip(size.toLong());
|
||||
return@synchronized
|
||||
}
|
||||
|
||||
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
|
||||
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
|
||||
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
||||
val message = ChromeCast.CastMessage.parseFrom(messageBytes);
|
||||
if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
|
||||
Logger.i(TAG, "Received message: $message");
|
||||
}
|
||||
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
||||
inputStream.read(buffer, 0, size);
|
||||
|
||||
try {
|
||||
handleMessage(message);
|
||||
} catch (e:Throwable) {
|
||||
Logger.w(TAG, "Failed to handle message.", e);
|
||||
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
|
||||
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
|
||||
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
||||
val message = ChromeCast.CastMessage.parseFrom(messageBytes);
|
||||
if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
|
||||
Logger.i(TAG, "Received message: $message");
|
||||
}
|
||||
|
||||
try {
|
||||
handleMessage(message);
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to handle message.", e);
|
||||
}
|
||||
}
|
||||
} catch (e: java.net.SocketException) {
|
||||
Logger.e(TAG, "Socket exception while receiving.", e);
|
||||
@@ -588,13 +595,16 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
return;
|
||||
}
|
||||
|
||||
val serializedSizeBE = ByteArray(4);
|
||||
serializedSizeBE[0] = (data.size shr 24 and 0xff).toByte();
|
||||
serializedSizeBE[1] = (data.size shr 16 and 0xff).toByte();
|
||||
serializedSizeBE[2] = (data.size shr 8 and 0xff).toByte();
|
||||
serializedSizeBE[3] = (data.size and 0xff).toByte();
|
||||
outputStream.write(serializedSizeBE);
|
||||
outputStream.write(data);
|
||||
synchronized(_outputStreamLock)
|
||||
{
|
||||
val serializedSizeBE = ByteArray(4);
|
||||
serializedSizeBE[0] = (data.size shr 24 and 0xff).toByte();
|
||||
serializedSizeBE[1] = (data.size shr 16 and 0xff).toByte();
|
||||
serializedSizeBE[2] = (data.size shr 8 and 0xff).toByte();
|
||||
serializedSizeBE[3] = (data.size and 0xff).toByte();
|
||||
outputStream.write(serializedSizeBE);
|
||||
outputStream.write(data);
|
||||
}
|
||||
|
||||
//Log.d(TAG, "Sent ${data.size} bytes.");
|
||||
}
|
||||
|
||||
@@ -242,6 +242,7 @@ class StateCasting {
|
||||
jmDNS.addServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
|
||||
jmDNS.addServiceListener("_airplay._tcp.local.", _airPlayServiceListener);
|
||||
jmDNS.addServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
|
||||
jmDNS.addServiceListener("_fcast._tcp.local.", _fastCastServiceListener);
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
jmDNS.addServiceTypeListener(_serviceTypeListener);
|
||||
|
||||
@@ -104,15 +104,22 @@ class DeveloperEndpoints(private val context: Context) {
|
||||
@HttpGET("/source_docs.js", "application/javascript")
|
||||
val devSourceDocsJS = "const sourceDocs = $devSourceDocsJson";
|
||||
|
||||
@HttpGET("/source_doc_urls.json", "application/json")
|
||||
fun devSourceDocUrlsJson(httpContext: HttpContext) {;
|
||||
val docs = JSClient.getMethodDocUrls();
|
||||
httpContext.respondCode(200, Json.encodeToString(docs), "application/json");
|
||||
}
|
||||
@HttpGET("/source_doc_urls.js", "application/javascript")
|
||||
fun devSourceDocUrlsJs(httpContext: HttpContext) {;
|
||||
val docs = JSClient.getMethodDocUrls();
|
||||
httpContext.respondCode(200, "const sourceDocUrls = " + Json.encodeToString(docs), "application/javascript");
|
||||
}
|
||||
|
||||
//Dependencies
|
||||
//@HttpGET("/dependencies/vue.js", "application/javascript")
|
||||
//val depVue = StateAssets.readAsset(context, "devportal/dependencies/vue.js", true);
|
||||
//@HttpGET("/dependencies/vuetify.js", "application/javascript")
|
||||
//val depVuetify = StateAssets.readAsset(context, "devportal/dependencies/vuetify.js", true);
|
||||
//@HttpGET("/dependencies/vuetify.min.css", "text/css")
|
||||
//val depVuetifyCss = StateAssets.readAsset(context, "devportal/dependencies/vuetify.min.css", true);
|
||||
@HttpGET("/dependencies/FutoMainLogo.svg", "image/svg+xml")
|
||||
val depFutoLogo = StateAssets.readAsset(context, "devportal/dependencies/FutoMainLogo.svg");
|
||||
@HttpGET("/favicon.svg", "image/svg+xml")
|
||||
val favicon = StateAssets.readAsset(context, "devportal/dependencies/favicon.svg");
|
||||
|
||||
@HttpGET("/reference_plugin.d.ts", "text/plain")
|
||||
fun devSourceTSWithRefs(httpContext: HttpContext) {
|
||||
@@ -437,6 +444,25 @@ class DeveloperEndpoints(private val context: Context) {
|
||||
context.respondCode(500, ex::class.simpleName + ":" + ex.message, "text/plain")
|
||||
}
|
||||
}
|
||||
@HttpGET("/dev/setDevProxy")
|
||||
fun devSetDevProxy(context: HttpContext) {
|
||||
try {
|
||||
val url = context.query.getOrDefault("url", "");
|
||||
val port = context.query.getOrDefault("port", "");
|
||||
if(url.isNullOrEmpty() || port.isNullOrEmpty() || port.toIntOrNull() == null)
|
||||
{
|
||||
StateDeveloper.instance.devProxy = null;
|
||||
context.respondCode(400);
|
||||
return;
|
||||
}
|
||||
StateDeveloper.instance.devProxy = StateDeveloper.DevProxySettings(url, port.toInt());
|
||||
context.respondCode(200, "true", "application/json");
|
||||
}
|
||||
catch(ex: Exception) {
|
||||
Logger.e("DeveloperEndpoints", ex.message, ex);
|
||||
context.respondCode(500, ex::class.simpleName + ":" + ex.message, "text/plain")
|
||||
}
|
||||
}
|
||||
|
||||
@HttpGET("/plugin/getDevLogs")
|
||||
fun pluginGetDevLogs(context: HttpContext) {
|
||||
@@ -448,6 +474,15 @@ class DeveloperEndpoints(private val context: Context) {
|
||||
context.respondCode(500, ex.message ?: "", "text/plain")
|
||||
}
|
||||
}
|
||||
@HttpGET("/plugin/getDevHttpExchanges")
|
||||
fun pluginGetDevExchanges(context: HttpContext) {
|
||||
try {
|
||||
context.respondJson(200, StateDeveloper.instance.getHttpExchangesAndClear());
|
||||
}
|
||||
catch(ex: Exception) {
|
||||
context.respondCode(500, ex.message ?: "", "text/plain")
|
||||
}
|
||||
}
|
||||
@HttpGET("/plugin/fakeDevLog")
|
||||
fun pluginFakeDevLog(context: HttpContext) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
package com.futo.platformplayer.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.media.MediaCas.PluginDescriptor
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.AddSourceActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
||||
import com.futo.platformplayer.assume
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class PluginUpdateDialog : AlertDialog {
|
||||
companion object {
|
||||
private val TAG = "PluginUpdateDialog";
|
||||
}
|
||||
private val _context: Context;
|
||||
|
||||
private lateinit var _buttonCancel1: Button;
|
||||
private lateinit var _buttonCancel2: Button;
|
||||
private lateinit var _buttonUpdate: LinearLayout;
|
||||
|
||||
private lateinit var _buttonOk: LinearLayout;
|
||||
private lateinit var _buttonInstall: LinearLayout;
|
||||
|
||||
private lateinit var _textPlugin: TextView;
|
||||
private lateinit var _textProgres: TextView;
|
||||
private lateinit var _textError: TextView;
|
||||
private lateinit var _textResult: TextView;
|
||||
|
||||
private lateinit var _uiChoiceTop: FrameLayout;
|
||||
private lateinit var _uiProgressTop: FrameLayout;
|
||||
private lateinit var _uiRiskTop: FrameLayout;
|
||||
|
||||
private lateinit var _uiChoiceBot: LinearLayout;
|
||||
private lateinit var _uiResultBot: LinearLayout;
|
||||
private lateinit var _uiRiskBot: LinearLayout;
|
||||
private lateinit var _uiProgressBot: LinearLayout;
|
||||
|
||||
private lateinit var _iconPlugin: ImageView;
|
||||
private lateinit var _updateSpinner: ImageView;
|
||||
|
||||
private var _isUpdating = false;
|
||||
|
||||
private val _oldConfig: SourcePluginConfig;
|
||||
private val _newConfig: SourcePluginConfig;
|
||||
|
||||
|
||||
constructor(context: Context, oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig): super(context) {
|
||||
_context = context;
|
||||
_oldConfig = oldConfig;
|
||||
_newConfig = newConfig;
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(LayoutInflater.from(context).inflate(R.layout.dialog_plugin_update, null));
|
||||
|
||||
_buttonCancel1 = findViewById(R.id.button_cancel_1);
|
||||
_buttonCancel2 = findViewById(R.id.button_cancel_2);
|
||||
_buttonUpdate = findViewById(R.id.button_update);
|
||||
|
||||
_buttonOk = findViewById(R.id.button_ok);
|
||||
_buttonInstall = findViewById(R.id.button_install);
|
||||
|
||||
_textPlugin = findViewById(R.id.text_plugin);
|
||||
_textProgres = findViewById(R.id.text_progress);
|
||||
_textError = findViewById(R.id.text_error);
|
||||
_textResult = findViewById(R.id.text_result);
|
||||
|
||||
_uiChoiceTop = findViewById(R.id.dialog_ui_choice_top);
|
||||
_uiProgressTop = findViewById(R.id.dialog_ui_progress_top);
|
||||
_uiRiskTop = findViewById(R.id.dialog_ui_risk_top);
|
||||
|
||||
_uiChoiceBot = findViewById(R.id.dialog_ui_bottom_choice);
|
||||
_uiResultBot = findViewById(R.id.dialog_ui_bottom_result);
|
||||
_uiRiskBot = findViewById(R.id.dialog_ui_bottom_risk);
|
||||
_uiProgressBot = findViewById(R.id.dialog_ui_bottom_progress);
|
||||
|
||||
_updateSpinner = findViewById(R.id.update_spinner);
|
||||
_iconPlugin = findViewById(R.id.icon_plugin);
|
||||
|
||||
_buttonCancel1.setOnClickListener {
|
||||
dismiss();
|
||||
};
|
||||
_buttonCancel2.setOnClickListener {
|
||||
dismiss();
|
||||
};
|
||||
_buttonUpdate.setOnClickListener {
|
||||
if (_isUpdating)
|
||||
return@setOnClickListener;
|
||||
_isUpdating = true;
|
||||
update();
|
||||
};
|
||||
|
||||
Glide.with(_iconPlugin)
|
||||
.load(_oldConfig.absoluteIconUrl)
|
||||
.fallback(R.drawable.ic_sources)
|
||||
.into(_iconPlugin);
|
||||
_textPlugin.text = _oldConfig.name;
|
||||
|
||||
val descriptor = StatePlugins.instance.getPlugin(_oldConfig.id);
|
||||
if(descriptor != null) {
|
||||
if(descriptor.appSettings.automaticUpdate) {
|
||||
if (_isUpdating)
|
||||
return;
|
||||
_isUpdating = true;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
super.dismiss();
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
_uiChoiceTop.visibility = View.GONE;
|
||||
_uiRiskTop.visibility = View.GONE;
|
||||
_uiChoiceBot.visibility = View.GONE;
|
||||
_uiResultBot.visibility = View.GONE;
|
||||
_uiRiskBot.visibility = View.GONE;
|
||||
_uiProgressTop.visibility = View.VISIBLE;
|
||||
_uiProgressBot.visibility = View.VISIBLE;
|
||||
|
||||
setCancelable(false);
|
||||
setCanceledOnTouchOutside(false);
|
||||
|
||||
Logger.i(TAG, "Keep screen on set import")
|
||||
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
|
||||
_updateSpinner.drawable?.assume<Animatable>()?.start();
|
||||
|
||||
val scope = StateApp.instance.scopeOrNull;
|
||||
scope?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val client = ManagedHttpClient();
|
||||
val script = StatePlugins.instance.getScript(_oldConfig.id) ?: "";
|
||||
|
||||
val newScript = client.get(_newConfig.absoluteScriptUrl)?.body?.string();
|
||||
if(newScript.isNullOrEmpty())
|
||||
throw IllegalStateException("No script found");
|
||||
|
||||
if(_oldConfig.isLowRiskUpdate(script, _newConfig, newScript)){
|
||||
|
||||
StatePlugins.instance.installPluginBackground(context, StateApp.instance.scope, _newConfig, newScript,
|
||||
{ text: String, progress: Double ->
|
||||
_textProgres.setText(text);
|
||||
},
|
||||
{ ex ->
|
||||
if(ex == null) {
|
||||
StatePlugins.instance.clearUpdateAvailable(_newConfig);
|
||||
_iconPlugin.setImageResource(R.drawable.ic_check);
|
||||
_textError.visibility = View.GONE;
|
||||
_textResult.visibility = View.VISIBLE;
|
||||
}
|
||||
else {
|
||||
_iconPlugin.setImageResource(R.drawable.ic_error_pred);
|
||||
_textError.text = ex.message + "\n\nYou can retry inside the sources tab";
|
||||
_textError.visibility = View.VISIBLE;
|
||||
_textResult.visibility = View.GONE;
|
||||
}
|
||||
try {
|
||||
_buttonOk.setOnClickListener {
|
||||
dismiss();
|
||||
}
|
||||
_uiProgressTop.visibility = View.GONE;
|
||||
_uiProgressBot.visibility = View.GONE;
|
||||
_uiChoiceTop.visibility = View.VISIBLE;
|
||||
_uiResultBot.visibility = View.VISIBLE;
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to update UI.", e)
|
||||
} finally {
|
||||
Logger.i(TAG, "Keep screen on unset update")
|
||||
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
else {
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
_buttonInstall.setOnClickListener {
|
||||
dismiss();
|
||||
|
||||
val intent = Intent(_context, AddSourceActivity::class.java).apply {
|
||||
data = Uri.parse(_newConfig.sourceUrl)
|
||||
};
|
||||
|
||||
_context.startActivity(intent);
|
||||
}
|
||||
|
||||
_uiProgressTop.visibility = View.GONE;
|
||||
_uiProgressBot.visibility = View.GONE;
|
||||
_uiRiskTop.visibility = View.VISIBLE;
|
||||
_uiRiskBot.visibility = View.VISIBLE;
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to update UI.", e)
|
||||
} finally {
|
||||
Logger.i(TAG, "Keep screen on unset update")
|
||||
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to update.", e);
|
||||
withContext(Dispatchers.Main) {
|
||||
_buttonOk.setOnClickListener {
|
||||
dismiss();
|
||||
}
|
||||
_iconPlugin.setImageResource(R.drawable.ic_error_pred);
|
||||
_textResult.visibility = View.GONE;
|
||||
_uiProgressTop.visibility = View.GONE;
|
||||
_uiProgressBot.visibility = View.GONE;
|
||||
_uiChoiceTop.visibility = View.VISIBLE;
|
||||
_uiResultBot.visibility = View.VISIBLE;
|
||||
_textError.visibility = View.VISIBLE;
|
||||
_textError.text = e.message + "\n\nYou can retry inside the sources tab"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -755,6 +755,7 @@ class VideoDownload {
|
||||
companion object {
|
||||
const val TAG = "VideoDownload";
|
||||
const val GROUP_PLAYLIST = "Playlist";
|
||||
const val GROUP_WATCHLATER= "WatchLater";
|
||||
|
||||
fun videoContainerToExtension(container: String): String? {
|
||||
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.documentfile.provider.DocumentFile
|
||||
import com.arthenica.ffmpegkit.*
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.toHumanBitrate
|
||||
@@ -63,7 +64,7 @@ class VideoExport {
|
||||
val outputFile: DocumentFile?;
|
||||
val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
||||
if (sourceCount > 1) {
|
||||
val outputFileName = toSafeFileName(videoLocal.name) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
||||
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
||||
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
||||
?: throw Exception("Failed to create file in external directory.");
|
||||
|
||||
@@ -79,7 +80,7 @@ class VideoExport {
|
||||
}
|
||||
outputFile = f;
|
||||
} else if (v != null) {
|
||||
val outputFileName = toSafeFileName(videoLocal.name) + "." + VideoDownload.videoContainerToExtension(v.container);
|
||||
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.videoContainerToExtension(v.container);
|
||||
val f = downloadRoot.createFile(v.container, outputFileName)
|
||||
?: throw Exception("Failed to create file in external directory.");
|
||||
|
||||
@@ -91,7 +92,7 @@ class VideoExport {
|
||||
|
||||
outputFile = f;
|
||||
} else if (a != null) {
|
||||
val outputFileName = toSafeFileName(videoLocal.name) + "." + VideoDownload.audioContainerToExtension(a.container);
|
||||
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container);
|
||||
val f = downloadRoot.createFile(a.container, outputFileName)
|
||||
?: throw Exception("Failed to create file in external directory.");
|
||||
|
||||
@@ -110,11 +111,6 @@ class VideoExport {
|
||||
return@coroutineScope outputFile;
|
||||
}
|
||||
|
||||
private fun toSafeFileName(input: String): String {
|
||||
val safeCharacters = ('a'..'z') + ('A'..'Z') + ('0'..'9') + listOf('-', '_')
|
||||
return input.map { if (it in safeCharacters) it else '_' }.joinToString(separator = "")
|
||||
}
|
||||
|
||||
private suspend fun combine(inputPathAudio: String?, inputPathVideo: String?, inputPathSubtitles: String?, outputPath: String, duration: Double, onProgress: ((Double) -> Unit)? = null) = withContext(Dispatchers.IO) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
//ffmpeg -i a.mp4 -i b.m4a -scodec mov_text -i c.vtt -map 0:v -map 1:a -map 2 -c:v copy -c:a copy -c:s mov_text output.mp4
|
||||
|
||||
@@ -11,7 +11,6 @@ import com.caoccao.javet.values.primitive.V8ValueBoolean
|
||||
import com.caoccao.javet.values.primitive.V8ValueInteger
|
||||
import com.caoccao.javet.values.primitive.V8ValueString
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
@@ -45,7 +44,6 @@ class V8Plugin {
|
||||
private val _clientAuth: ManagedHttpClient;
|
||||
private val _clientOthers: ConcurrentHashMap<String, JSHttpClient> = ConcurrentHashMap();
|
||||
|
||||
|
||||
val httpClient: ManagedHttpClient get() = _client;
|
||||
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
|
||||
val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers;
|
||||
@@ -71,6 +69,11 @@ class V8Plugin {
|
||||
private var _busyCounter = 0;
|
||||
val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 };
|
||||
|
||||
var allowDevSubmit: Boolean = false
|
||||
private set(value) {
|
||||
field = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before a busy counter is about to be removed.
|
||||
* Is primarily used to prevent additional calls to dead runtimes.
|
||||
@@ -92,6 +95,10 @@ class V8Plugin {
|
||||
withDependency(getPackage(pack));
|
||||
}
|
||||
|
||||
fun changeAllowDevSubmit(isAllowed: Boolean) {
|
||||
allowDevSubmit = isAllowed;
|
||||
}
|
||||
|
||||
fun withDependency(context: Context, assetPath: String) : V8Plugin {
|
||||
if(!_deps.containsKey(assetPath))
|
||||
_deps.put(assetPath, getAssetFile(context, assetPath));
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
@@ -12,6 +13,9 @@ import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class PackageBridge : V8Package {
|
||||
@Transient
|
||||
@@ -21,6 +25,7 @@ class PackageBridge : V8Package {
|
||||
@Transient
|
||||
private val _clientAuth: ManagedHttpClient
|
||||
|
||||
|
||||
override val name: String get() = "Bridge";
|
||||
override val variableName: String get() = "bridge";
|
||||
|
||||
@@ -47,6 +52,44 @@ class PackageBridge : V8Package {
|
||||
StateDeveloper.instance.logDevInfo(StateDeveloper.instance.currentDevID ?: "", str ?: "null");
|
||||
}
|
||||
|
||||
private val _jsonSerializer = Json { this.prettyPrintIndent = " "; this.prettyPrint = true; };
|
||||
private var _devSubmitClient: ManagedHttpClient? = null;
|
||||
@V8Function
|
||||
fun devSubmit(label: String, data: String) {
|
||||
if(_plugin.config !is SourcePluginConfig)
|
||||
return;
|
||||
if(!_plugin.allowDevSubmit)
|
||||
return;
|
||||
val devUrl = _plugin.config.developerSubmitUrl ?: return;
|
||||
if(_devSubmitClient == null)
|
||||
_devSubmitClient = ManagedHttpClient();
|
||||
|
||||
val stackTrace = Thread.currentThread().stackTrace;
|
||||
val callerMethod = stackTrace.findLast {
|
||||
it.className == JSClient::class.java.name
|
||||
}?.methodName ?: "";
|
||||
val session = StateApp.instance.sessionId;
|
||||
val pluginId = _plugin.config.id;
|
||||
val pluginVersion = _plugin.config.version;
|
||||
|
||||
val obj = DevSubmitData(pluginId, pluginVersion, callerMethod, session, label, data);
|
||||
|
||||
UIDialogs.toast("DevSubmit [${callerMethod}] (${_plugin.config.name})", false);
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val json = _jsonSerializer.encodeToString(obj);
|
||||
Logger.i(TAG, "DevSubmit [${callerMethod}] - ${devUrl}\n" + json);
|
||||
val resp = _devSubmitClient?.post(devUrl, json, mutableMapOf(Pair("Content-Type", "application/json")));
|
||||
Logger.i(TAG, "DevSubmit [${callerMethod}] - ${devUrl} Status: " + (resp?.code?.toString() ?: "-1"))
|
||||
}
|
||||
catch(ex: Exception) {
|
||||
Logger.e(TAG, "DevSubmission to [${devUrl}] failed due to:\n" + ex.message, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Serializable
|
||||
class DevSubmitData(val pluginId: String, val pluginVersion: Int, val caller: String, val session: String, val label: String, val data: String)
|
||||
|
||||
@V8Function
|
||||
fun throwTest(str: String) {
|
||||
throw IllegalStateException(str);
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.caoccao.javet.interop.V8Runtime
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
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.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
@@ -242,7 +243,8 @@ class PackageHttp: V8Package {
|
||||
val resp = client.requestMethod(method, url, headers);
|
||||
val responseBody = resp.body?.string();
|
||||
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -256,7 +258,8 @@ class PackageHttp: V8Package {
|
||||
val resp = client.requestMethod(method, url, body, headers);
|
||||
val responseBody = resp.body?.string();
|
||||
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -271,7 +274,8 @@ class PackageHttp: V8Package {
|
||||
val resp = client.get(url, headers);
|
||||
val responseBody = resp.body?.string();
|
||||
//logResponse("GET", url, resp.code, resp.headers, responseBody);
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -285,7 +289,8 @@ class PackageHttp: V8Package {
|
||||
val resp = client.post(url, body, headers);
|
||||
val responseBody = resp.body?.string();
|
||||
//logResponse("POST", url, resp.code, resp.headers, responseBody);
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -305,18 +310,31 @@ class PackageHttp: V8Package {
|
||||
}
|
||||
}
|
||||
|
||||
private fun sanitizeResponseHeaders(headers: Map<String, List<String>>?): Map<String, List<String>> {
|
||||
private fun sanitizeResponseHeaders(headers: Map<String, List<String>>?, onlyWhitelisted: Boolean = false): Map<String, List<String>> {
|
||||
val result = mutableMapOf<String, List<String>>()
|
||||
headers?.forEach { (header, values) ->
|
||||
val lowerCaseHeader = header.lowercase()
|
||||
if (WHITELISTED_RESPONSE_HEADERS.contains(lowerCaseHeader)) {
|
||||
result[lowerCaseHeader] = values
|
||||
if(onlyWhitelisted)
|
||||
headers?.forEach { (header, values) ->
|
||||
val lowerCaseHeader = header.lowercase()
|
||||
if (WHITELISTED_RESPONSE_HEADERS.contains(lowerCaseHeader)) {
|
||||
result[lowerCaseHeader] = values
|
||||
}
|
||||
}
|
||||
else {
|
||||
headers?.forEach { (header, values) ->
|
||||
val lowerCaseHeader = header.lowercase()
|
||||
if(lowerCaseHeader == "set-cookie") {
|
||||
result[lowerCaseHeader] = values.filter{
|
||||
!it.lowercase().contains("httponly")
|
||||
};
|
||||
}
|
||||
else
|
||||
result[lowerCaseHeader] = values;
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/*private fun logRequest(method: String, url: String, headers: Map<String, String> = HashMap(), body: String?) {
|
||||
private fun logRequest(method: String, url: String, headers: Map<String, String> = HashMap(), body: String?) {
|
||||
Logger.v(TAG) {
|
||||
val stringBuilder = StringBuilder();
|
||||
stringBuilder.appendLine("HTTP request (useAuth = )");
|
||||
@@ -333,7 +351,7 @@ class PackageHttp: V8Package {
|
||||
|
||||
return@v stringBuilder.toString();
|
||||
};
|
||||
}*/
|
||||
}
|
||||
|
||||
/*private fun logResponse(method: String, url: String, responseCode: Int? = null, responseHeaders: Map<String, List<String>> = HashMap(), responseBody: String? = null) {
|
||||
Logger.v(TAG) {
|
||||
|
||||
@@ -4,8 +4,11 @@ import android.util.Base64
|
||||
import com.caoccao.javet.annotations.V8Function
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.google.common.hash.Hashing.md5
|
||||
import java.security.MessageDigest
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
class PackageUtilities : V8Package {
|
||||
@Transient
|
||||
private val _config: IV8PluginConfig;
|
||||
@@ -19,7 +22,31 @@ class PackageUtilities : V8Package {
|
||||
|
||||
@V8Function
|
||||
fun toBase64(arr: ByteArray): String {
|
||||
return Base64.encodeToString(arr, Base64.NO_WRAP);
|
||||
return Base64.encodeToString(arr, Base64.NO_PADDING or Base64.NO_WRAP);
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun fromBase64(str: String): ByteArray {
|
||||
return Base64.decode(str, Base64.NO_PADDING or Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun md5(arr: ByteArray): ByteArray {
|
||||
return MessageDigest.getInstance("MD5").digest(arr);
|
||||
}
|
||||
@V8Function
|
||||
fun md5String(str: String): String {
|
||||
return md5(str.toByteArray(Charsets.UTF_8)).fold("") { str, it -> str + "%02x".format(it) };
|
||||
}
|
||||
|
||||
|
||||
@V8Function
|
||||
fun sha256(arr: ByteArray): ByteArray {
|
||||
return MessageDigest.getInstance("SHA-256").digest(arr);
|
||||
}
|
||||
@V8Function
|
||||
fun sha256String(str: String): String {
|
||||
return sha256(str.toByteArray(Charsets.UTF_8)).fold("") { str, it -> str + "%02x".format(it) };
|
||||
}
|
||||
|
||||
@V8Function
|
||||
|
||||
+6
-3
@@ -60,8 +60,10 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||
val onAddToClicked = Event1<IPlatformContent>();
|
||||
val onAddToQueueClicked = Event1<IPlatformContent>();
|
||||
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
|
||||
val onLongPress = Event1<IPlatformContent>();
|
||||
|
||||
|
||||
private fun getContentPager(channel: IPlatformChannel): IPager<IPlatformContent> {
|
||||
Logger.i(TAG, "getContentPager");
|
||||
|
||||
@@ -103,9 +105,9 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||
}).success {
|
||||
setLoading(false);
|
||||
val posBefore = _results.size;
|
||||
val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }
|
||||
_results.addAll(toAdd);
|
||||
_adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), toAdd.size); };
|
||||
//val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }
|
||||
_results.addAll(it);
|
||||
_adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), it.size); };
|
||||
}.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load next page.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadNextPage() });
|
||||
@@ -157,6 +159,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||
this.onChannelClicked.subscribe(this@ChannelContentsFragment.onChannelClicked::emit);
|
||||
this.onAddToClicked.subscribe(this@ChannelContentsFragment.onAddToClicked::emit);
|
||||
this.onAddToQueueClicked.subscribe(this@ChannelContentsFragment.onAddToQueueClicked::emit);
|
||||
this.onAddToWatchLaterClicked.subscribe(this@ChannelContentsFragment.onAddToWatchLaterClicked::emit);
|
||||
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
||||
}
|
||||
|
||||
|
||||
+9
-2
@@ -26,6 +26,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment
|
||||
import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment
|
||||
@@ -151,7 +152,7 @@ class ChannelFragment : MainFragment() {
|
||||
}
|
||||
.exception<Throwable> {
|
||||
Logger.e(TAG, "Failed to load channel.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadChannel() });
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadChannel() }, null, fragment);
|
||||
}
|
||||
|
||||
val tabs: TabLayout = findViewById(R.id.tabs);
|
||||
@@ -206,6 +207,12 @@ class ChannelFragment : MainFragment() {
|
||||
StatePlayer.instance.addToQueue(content);
|
||||
}
|
||||
}
|
||||
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
||||
if(content is IPlatformVideo) {
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content));
|
||||
UIDialogs.toast("Added to watch later\n[${content.name}]");
|
||||
}
|
||||
}
|
||||
adapter.onUrlClicked.subscribe { url ->
|
||||
fragment.navigate<BrowserFragment>(url);
|
||||
}
|
||||
@@ -264,7 +271,7 @@ class ChannelFragment : MainFragment() {
|
||||
_taskLoadPolycentricProfile.cancel();
|
||||
_selectedTabIndex = -1;
|
||||
|
||||
if (!isBack) {
|
||||
if (!isBack || _url == null) {
|
||||
_imageBanner.setImageDrawable(null);
|
||||
|
||||
if (parameter is String) {
|
||||
|
||||
+16
@@ -1,7 +1,9 @@
|
||||
package com.futo.platformplayer.fragment.mainactivity.main
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Browser
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -118,6 +120,7 @@ class CommentsFragment : MainFragment() {
|
||||
holder.onDelete.subscribe(::onDelete);
|
||||
holder.onRepliesClick.subscribe(::onRepliesClick);
|
||||
holder.onClick.subscribe(::onClick);
|
||||
holder.onAuthorClick.subscribe(::onAuthorClick);
|
||||
return@InsertedViewAdapterWithLoader holder;
|
||||
}
|
||||
);
|
||||
@@ -211,6 +214,19 @@ class CommentsFragment : MainFragment() {
|
||||
setRepliesOverlayVisible(true, true)
|
||||
}
|
||||
}
|
||||
private fun onAuthorClick(c: IPlatformComment) {
|
||||
if (c !is PolycentricPlatformComment) {
|
||||
return@onAuthorClick;
|
||||
}
|
||||
|
||||
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
||||
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
||||
//val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
|
||||
val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
||||
_fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
||||
//_fragment.navigate<BrowserFragment>(navUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRepliesClick(c: IPlatformComment) {
|
||||
val replyCount = c.replyCount ?: 0;
|
||||
|
||||
+9
@@ -12,10 +12,12 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.structures.*
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateMeta
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.video.PlayerManager
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||
@@ -81,6 +83,12 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
StatePlayer.instance.addToQueue(it);
|
||||
}
|
||||
};
|
||||
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it));
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
}
|
||||
};
|
||||
adapter.onLongPress.subscribe(this) {
|
||||
if (it is IPlatformVideo) {
|
||||
showVideoOptionsOverlay(it)
|
||||
@@ -135,6 +143,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
adapter.onChannelClicked.remove(this);
|
||||
adapter.onAddToClicked.remove(this);
|
||||
adapter.onAddToQueueClicked.remove(this);
|
||||
adapter.onAddToWatchLaterClicked.remove(this);
|
||||
adapter.onLongPress.remove(this);
|
||||
}
|
||||
|
||||
|
||||
+7
-3
@@ -99,7 +99,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||
.success { loadedResult(it); }.exception<ScriptCaptchaRequiredException> { }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load results.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }, null, fragment);
|
||||
}
|
||||
|
||||
setPreviewsEnabled(Settings.instance.search.previewFeedItems);
|
||||
@@ -129,7 +129,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||
onFilterClick.subscribe(this) {
|
||||
_overlayContainer.let {
|
||||
val filterValuesCopy = HashMap(_filterValues);
|
||||
val filtersOverlay = UISlideOverlays.showFiltersOverlay(lifecycleScope, it, _enabledClientIds!!, filterValuesCopy);
|
||||
val filtersOverlay = UISlideOverlays.showFiltersOverlay(lifecycleScope, it, _enabledClientIds!!, filterValuesCopy, _channelUrl != null);
|
||||
filtersOverlay.onOK.subscribe { enabledClientIds, changed ->
|
||||
if (changed) {
|
||||
setFilterValues(filtersOverlay.commonCapabilities, filterValuesCopy);
|
||||
@@ -170,7 +170,11 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val commonCapabilities = StatePlatform.instance.getCommonSearchCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
|
||||
val commonCapabilities =
|
||||
if(_channelUrl == null)
|
||||
StatePlatform.instance.getCommonSearchCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
|
||||
else
|
||||
StatePlatform.instance.getCommonSearchChannelContentsCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
|
||||
val sorts = commonCapabilities?.sorts ?: listOf();
|
||||
if (sorts.size > 1) {
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ class CreatorSearchResultsFragment : MainFragment() {
|
||||
.exception<ScriptCaptchaRequiredException> { }
|
||||
.exception<Throwable> {
|
||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }, null, fragment);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+21
-6
@@ -12,8 +12,10 @@ import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.downloads.VideoDownload
|
||||
import com.futo.platformplayer.downloads.VideoLocal
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Playlist
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
||||
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
||||
import com.futo.platformplayer.views.others.ProgressBar
|
||||
@@ -143,6 +145,7 @@ class DownloadsFragment : MainFragment() {
|
||||
|
||||
val activeDownloads = StateDownloads.instance.getDownloading();
|
||||
val playlists = StateDownloads.instance.getCachedPlaylists();
|
||||
val watchLaterDownload = StateDownloads.instance.getWatchLaterDescriptor();
|
||||
val downloaded = StateDownloads.instance.getDownloadedVideos()
|
||||
.filter { it.groupType != VideoDownload.GROUP_PLAYLIST || it.groupID == null || !StateDownloads.instance.hasCachedPlaylist(it.groupID!!) };
|
||||
|
||||
@@ -150,23 +153,35 @@ class DownloadsFragment : MainFragment() {
|
||||
_listActiveDownloadsContainer.visibility = GONE;
|
||||
else {
|
||||
_listActiveDownloadsContainer.visibility = VISIBLE;
|
||||
_listActiveDownloadsMeta.text = "(${activeDownloads.size})";
|
||||
_listActiveDownloadsMeta.text = "(${activeDownloads.size} videos)";
|
||||
|
||||
_listActiveDownloads.removeAllViews();
|
||||
for(view in activeDownloads.map { ActiveDownloadItem(context, it, _frag.lifecycleScope) })
|
||||
for(view in activeDownloads.take(4).map { ActiveDownloadItem(context, it, _frag.lifecycleScope) })
|
||||
_listActiveDownloads.addView(view);
|
||||
}
|
||||
|
||||
if(playlists.isEmpty())
|
||||
if(playlists.isEmpty() && watchLaterDownload == null)
|
||||
_listPlaylistsContainer.visibility = GONE;
|
||||
else {
|
||||
_listPlaylistsContainer.visibility = VISIBLE;
|
||||
_listPlaylistsMeta.text = "(${playlists.size} ${context.getString(R.string.playlists).lowercase()}, ${playlists.sumOf { it.playlist.videos.size }} ${context.getString(R.string.videos).lowercase()})";
|
||||
|
||||
val watchLater = if(watchLaterDownload != null) StatePlaylists.instance.getWatchLater() else listOf();
|
||||
|
||||
_listPlaylistsMeta.text = "(${playlists.size + (if(watchLaterDownload != null) 1 else 0)} ${context.getString(R.string.playlists).lowercase()}, ${playlists.sumOf { it.playlist.videos.size } + watchLater.size} ${context.getString(R.string.videos).lowercase()})";
|
||||
|
||||
_listPlaylists.removeAllViews();
|
||||
for(view in playlists.map { PlaylistDownloadItem(context, it) }) {
|
||||
if(watchLaterDownload != null) {
|
||||
val pdView = PlaylistDownloadItem(context, "Watch Later", watchLater.firstOrNull()?.thumbnails?.getHQThumbnail(), "WATCHLATER");
|
||||
pdView.setOnClickListener {
|
||||
_frag.navigate<WatchLaterFragment>();
|
||||
}
|
||||
_listPlaylists.addView(pdView);
|
||||
}
|
||||
for(view in playlists.map { PlaylistDownloadItem(context, it.playlist.name, it.playlist.videos.firstOrNull()?.thumbnails?.getHQThumbnail(), it.playlist) }) {
|
||||
view.setOnClickListener {
|
||||
_frag.navigate<PlaylistFragment>(view.playlist.playlist);
|
||||
if(view.obj is Playlist) {
|
||||
_frag.navigate<PlaylistFragment>(view.obj);
|
||||
}
|
||||
};
|
||||
_listPlaylists.addView(view);
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
Logger.w(TAG, "Failed to load next page.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
||||
loadNextPage();
|
||||
});
|
||||
}, null, fragment);
|
||||
//UIDialogs.showDataRetryDialog(layoutInflater, it.message, { loadNextPage() });
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -174,7 +174,7 @@ class HistoryFragment : MainFragment() {
|
||||
Logger.w(TAG, "Failed to load next page.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
||||
loadNextPage();
|
||||
});
|
||||
}, null, fragment);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -126,10 +126,10 @@ class HomeFragment : MainFragment() {
|
||||
Logger.w(TAG, "Failed to load channel.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_get_home), it, {
|
||||
loadResults()
|
||||
}) {
|
||||
}, {
|
||||
finishRefreshLayoutLoader();
|
||||
setLoading(false);
|
||||
};
|
||||
}, fragment);
|
||||
};
|
||||
|
||||
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||
|
||||
+16
-41
@@ -146,7 +146,7 @@ class PlaylistFragment : MainFragment() {
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load playlist.", it);
|
||||
val c = context ?: return@exception;
|
||||
UIDialogs.showGeneralRetryErrorDialog(c, context.getString(R.string.failed_to_load_playlist), it, ::fetchPlaylist);
|
||||
UIDialogs.showGeneralRetryErrorDialog(c, context.getString(R.string.failed_to_load_playlist), it, ::fetchPlaylist, null, fragment);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -201,14 +201,18 @@ class PlaylistFragment : MainFragment() {
|
||||
showConvertPlaylistButton();
|
||||
}
|
||||
|
||||
updateDownloadState();
|
||||
_playlist?.let {
|
||||
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this::download);
|
||||
}
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
StateDownloads.instance.onDownloadsChanged.subscribe(this) {
|
||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
updateDownloadState();
|
||||
_playlist?.let {
|
||||
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this@PlaylistView::download);
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
||||
}
|
||||
@@ -217,7 +221,9 @@ class PlaylistFragment : MainFragment() {
|
||||
StateDownloads.instance.onDownloadedChanged.subscribe(this) {
|
||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
updateDownloadState();
|
||||
_playlist?.let {
|
||||
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this@PlaylistView::download);
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
||||
}
|
||||
@@ -225,6 +231,12 @@ class PlaylistFragment : MainFragment() {
|
||||
};
|
||||
}
|
||||
|
||||
private fun download() {
|
||||
_playlist?.let {
|
||||
UISlideOverlays.showDownloadPlaylistOverlay(it, overlayContainer);
|
||||
}
|
||||
}
|
||||
|
||||
fun onPause() {
|
||||
StateDownloads.instance.onDownloadsChanged.remove(this);
|
||||
StateDownloads.instance.onDownloadedChanged.remove(this);
|
||||
@@ -268,43 +280,6 @@ class PlaylistFragment : MainFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDownloadState() {
|
||||
val playlist = _playlist ?: return;
|
||||
val isDownloading = StateDownloads.instance.getDownloading().any { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == playlist.id };
|
||||
val isDownloaded = StateDownloads.instance.isPlaylistCached(playlist.id);
|
||||
|
||||
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics);
|
||||
|
||||
if(isDownloaded && !isDownloading)
|
||||
_buttonDownload.setBackgroundResource(R.drawable.background_button_round_green);
|
||||
else
|
||||
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
|
||||
|
||||
if(isDownloading) {
|
||||
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
||||
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
||||
_buttonDownload.setOnClickListener {
|
||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
else if(isDownloaded) {
|
||||
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
||||
_buttonDownload.setOnClickListener {
|
||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
_buttonDownload.setImageResource(R.drawable.ic_download);
|
||||
_buttonDownload.setOnClickListener {
|
||||
UISlideOverlays.showDownloadPlaylistOverlay(playlist, overlayContainer);
|
||||
}
|
||||
}
|
||||
_buttonDownload.setPadding(dp10.toInt());
|
||||
}
|
||||
|
||||
override fun canEdit(): Boolean { return _playlist != null; }
|
||||
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ class PlaylistSearchResultsFragment : MainFragment() {
|
||||
.success { loadedResult(it); }
|
||||
.exception<Throwable> {
|
||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }, null, fragment);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -35,6 +35,7 @@ import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
@@ -161,7 +162,7 @@ class PostDetailFragment : MainFragment {
|
||||
.success { setPostDetails(it) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(ChannelFragment.TAG, context.getString(R.string.failed_to_load_post), it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost, null, _fragment);
|
||||
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
||||
|
||||
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
||||
@@ -211,6 +212,8 @@ class PostDetailFragment : MainFragment {
|
||||
|
||||
_repliesOverlay = findViewById(R.id.replies_overlay);
|
||||
|
||||
_textContent.setPlatformPlayerLinkMovementMethod(context);
|
||||
|
||||
_buttonSubscribe.onSubscribed.subscribe {
|
||||
//TODO: add overlay to layout
|
||||
//UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||
@@ -473,6 +476,7 @@ class PostDetailFragment : MainFragment {
|
||||
}
|
||||
|
||||
updateCommentType(true);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
fun setPostOverview(value: IPlatformPost) {
|
||||
|
||||
+37
-8
@@ -12,6 +12,7 @@ import android.webkit.CookieManager
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
@@ -100,6 +101,11 @@ class SourceDetailFragment : MainFragment() {
|
||||
loadConfig(parameter);
|
||||
updateSourceViews();
|
||||
}
|
||||
else if(parameter is UpdatePluginAction) {
|
||||
loadConfig(parameter.config);
|
||||
updateSourceViews();
|
||||
checkForUpdatesSource();
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -107,17 +113,20 @@ class SourceDetailFragment : MainFragment() {
|
||||
fun onHide() {
|
||||
val id = _config?.id ?: return;
|
||||
|
||||
if(_settingsChanged && _settings != null) {
|
||||
_settingsChanged = false;
|
||||
StatePlugins.instance.setPluginSettings(id, _settings!!);
|
||||
reloadSource(id);
|
||||
|
||||
UIDialogs.toast(context.getString(R.string.plugin_settings_saved), false);
|
||||
}
|
||||
var shouldReload = false;
|
||||
if(_settingsAppChanged) {
|
||||
_settingsAppForm.setObjectValues();
|
||||
StatePlugins.instance.savePlugin(id);
|
||||
shouldReload = true;
|
||||
}
|
||||
if(_settingsChanged && _settings != null) {
|
||||
_settingsChanged = false;
|
||||
StatePlugins.instance.setPluginSettings(id, _settings!!);
|
||||
shouldReload = true;
|
||||
UIDialogs.toast(context.getString(R.string.plugin_settings_saved), false);
|
||||
}
|
||||
if(shouldReload)
|
||||
reloadSource(id);
|
||||
}
|
||||
|
||||
|
||||
@@ -137,9 +146,25 @@ class SourceDetailFragment : MainFragment() {
|
||||
//App settings
|
||||
try {
|
||||
_settingsAppForm.fromObject(source.descriptor.appSettings);
|
||||
if(source.config.developerSubmitUrl.isNullOrEmpty()) {
|
||||
val field = _settingsAppForm.findField("devSubmit");
|
||||
field?.setValue(false);
|
||||
if(field is View)
|
||||
field.isVisible = false;
|
||||
}
|
||||
_settingsAppForm.onChanged.clear();
|
||||
_settingsAppForm.onChanged.subscribe { _, _ ->
|
||||
_settingsAppForm.onChanged.subscribe { field, value ->
|
||||
_settingsAppChanged = true;
|
||||
if(field.descriptor?.id == "devSubmit") {
|
||||
if(value is Boolean && value) {
|
||||
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow,
|
||||
"Are you sure you trust the developer?",
|
||||
"Developers may gain access to sensitive data. Only enable this when you are trying to help the developer fix a bug.\nThe following domain is used:",
|
||||
source.config.developerSubmitUrl ?: "", 0,
|
||||
UIDialogs.Action("Cancel", { field.setValue(false); }, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action("Enable", { }, UIDialogs.ActionStyle.DANGEROUS));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to load app settings form from plugin settings", e)
|
||||
@@ -547,4 +572,8 @@ class SourceDetailFragment : MainFragment() {
|
||||
const val TAG = "SourceDetailFragment";
|
||||
fun newInstance() = SourceDetailFragment().apply {}
|
||||
}
|
||||
|
||||
class UpdatePluginAction(val config: SourcePluginConfig) {
|
||||
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -262,7 +262,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
.exception<Throwable> {
|
||||
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
||||
if(it !is CancellationException)
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults(true) });
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults(true) }, null, fragment);
|
||||
else {
|
||||
finishRefreshLayoutLoader();
|
||||
setLoading(false);
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ class SuggestionsFragment : MainFragment {
|
||||
.success { suggestions -> updateSuggestions(suggestions, false) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(ChannelFragment.TAG, "Failed to load suggestions.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadSuggestions() });
|
||||
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadSuggestions() }, null, this);
|
||||
};
|
||||
|
||||
constructor(): super() {
|
||||
|
||||
+25
-6
@@ -23,6 +23,7 @@ import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.view.WindowManager
|
||||
import android.webkit.WebView
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
@@ -124,6 +125,7 @@ import com.futo.platformplayer.views.overlays.LiveChatOverlay
|
||||
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||
import com.futo.platformplayer.views.overlays.SupportOverlay
|
||||
import com.futo.platformplayer.views.overlays.WebviewOverlay
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuButtonList
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||
@@ -244,6 +246,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
private val _container_content_replies: RepliesOverlay;
|
||||
private val _container_content_description: DescriptionOverlay;
|
||||
private val _container_content_liveChat: LiveChatOverlay;
|
||||
private val _container_content_browser: WebviewOverlay;
|
||||
private val _container_content_support: SupportOverlay;
|
||||
|
||||
private var _container_content_current: View;
|
||||
@@ -349,7 +352,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
_container_content_replies = findViewById(R.id.videodetail_container_replies);
|
||||
_container_content_description = findViewById(R.id.videodetail_container_description);
|
||||
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
||||
_container_content_support = findViewById(R.id.videodetail_container_support)
|
||||
_container_content_support = findViewById(R.id.videodetail_container_support);
|
||||
_container_content_browser = findViewById(R.id.videodetail_container_webview)
|
||||
|
||||
_textComments = findViewById(R.id.text_comments);
|
||||
_addCommentView = findViewById(R.id.add_comment_view);
|
||||
@@ -624,6 +628,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_container_content_queue.onClose.subscribe { switchContentView(_container_content_main); };
|
||||
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
|
||||
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
|
||||
_container_content_browser.onClose.subscribe { switchContentView(_container_content_main); };
|
||||
|
||||
_description_viewMore.setOnClickListener {
|
||||
switchContentView(_container_content_description);
|
||||
@@ -644,6 +649,20 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
_container_content_current = _container_content_main;
|
||||
|
||||
_commentsList.onAuthorClick.subscribe { c ->
|
||||
if (c !is PolycentricPlatformComment) {
|
||||
return@subscribe;
|
||||
}
|
||||
|
||||
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
||||
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
||||
//val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
|
||||
val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
||||
fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
||||
//_container_content_browser.goto(navUrl);
|
||||
//switchContentView(_container_content_browser);
|
||||
}
|
||||
};
|
||||
_commentsList.onRepliesClick.subscribe { c ->
|
||||
val replyCount = c.replyCount ?: 0;
|
||||
var metadata = "";
|
||||
@@ -2212,11 +2231,11 @@ class VideoDetailView : ConstraintLayout {
|
||||
videoSourceHeight = 9;
|
||||
}
|
||||
val aspectRatio = videoSourceWidth.toDouble() / videoSourceHeight;
|
||||
if(aspectRatio > 3) {
|
||||
if(aspectRatio > 2.38) {
|
||||
videoSourceWidth = 16;
|
||||
videoSourceHeight = 9;
|
||||
}
|
||||
else if(aspectRatio < 0.3) {
|
||||
else if(aspectRatio < 0.43) {
|
||||
videoSourceHeight = 16;
|
||||
videoSourceWidth = 9;
|
||||
}
|
||||
@@ -2457,7 +2476,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
Logger.w(TAG, "exception<ScriptImplementationException>", it)
|
||||
|
||||
if (!nextVideo()) {
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, ::fetchVideo);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, ::fetchVideo, null, fragment);
|
||||
} else {
|
||||
StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_INVALIDVIDEO", context.getString(R.string.invalid_video), context.getString(
|
||||
R.string.there_was_an_invalid_video_in_your_queue_videoname_by_authorname_playback_was_skipped).replace("{videoName}", video?.name ?: "").replace("{authorName}", video?.author?.name ?: ""), AnnouncementType.SESSION)
|
||||
@@ -2493,7 +2512,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_retryJob = null;
|
||||
_liveTryJob?.cancel();
|
||||
_liveTryJob = null;
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, ::fetchVideo);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, ::fetchVideo, null, fragment);
|
||||
}
|
||||
}
|
||||
.exception<Throwable> {
|
||||
@@ -2505,7 +2524,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_retryJob = null;
|
||||
_liveTryJob?.cancel();
|
||||
_liveTryJob = null;
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo, null, fragment);
|
||||
}
|
||||
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
|
||||
|
||||
|
||||
+46
@@ -1,6 +1,7 @@
|
||||
package com.futo.platformplayer.fragment.mainactivity.main
|
||||
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
@@ -8,10 +9,17 @@ import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.setPadding
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.UISlideOverlays
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.assume
|
||||
import com.futo.platformplayer.downloads.VideoDownload
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.views.lists.VideoListEditorView
|
||||
|
||||
abstract class VideoListEditorView : LinearLayout {
|
||||
@@ -85,6 +93,44 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
|
||||
}
|
||||
|
||||
protected fun updateDownloadState(groupType: String, playlistId: String, onDownload: ()->Unit) {
|
||||
//val playlist = _playlist ?: return;
|
||||
val isDownloading = StateDownloads.instance.getDownloading().any { it.groupType == groupType && it.groupID == playlistId };
|
||||
val isDownloaded = StateDownloads.instance.isPlaylistCached(playlistId);
|
||||
|
||||
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics);
|
||||
|
||||
if(isDownloaded && !isDownloading)
|
||||
_buttonDownload.setBackgroundResource(R.drawable.background_button_round_green);
|
||||
else
|
||||
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
|
||||
|
||||
if(isDownloading) {
|
||||
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
||||
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
||||
_buttonDownload.setOnClickListener {
|
||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||
StateDownloads.instance.deleteCachedPlaylist(playlistId);
|
||||
});
|
||||
}
|
||||
}
|
||||
else if(isDownloaded) {
|
||||
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
||||
_buttonDownload.setOnClickListener {
|
||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||
StateDownloads.instance.deleteCachedPlaylist(playlistId);
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
_buttonDownload.setImageResource(R.drawable.ic_download);
|
||||
_buttonDownload.setOnClickListener {
|
||||
onDownload();
|
||||
//UISlideOverlays.showDownloadPlaylistOverlay(playlist, overlayContainer);
|
||||
}
|
||||
}
|
||||
_buttonDownload.setPadding(dp10.toInt());
|
||||
}
|
||||
|
||||
protected fun setName(name: String?) {
|
||||
_textName.text = name ?: "";
|
||||
|
||||
+41
@@ -5,10 +5,17 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.UISlideOverlays
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.downloads.VideoDownload
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class WatchLaterFragment : MainFragment() {
|
||||
override val isMainView : Boolean = true;
|
||||
@@ -28,6 +35,11 @@ class WatchLaterFragment : MainFragment() {
|
||||
return view;
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
_view?.onResume();
|
||||
}
|
||||
|
||||
override fun onDestroyMainView() {
|
||||
super.onDestroyMainView();
|
||||
_view = null;
|
||||
@@ -45,6 +57,34 @@ class WatchLaterFragment : MainFragment() {
|
||||
fun onShown() {
|
||||
setName("Watch Later");
|
||||
setVideos(StatePlaylists.instance.getWatchLater(), true);
|
||||
|
||||
setButtonDownloadVisible(true);
|
||||
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
|
||||
}
|
||||
|
||||
fun onResume(){
|
||||
StateDownloads.instance.onDownloadsChanged.subscribe(this) {
|
||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
||||
}
|
||||
}
|
||||
};
|
||||
StateDownloads.instance.onDownloadedChanged.subscribe(this) {
|
||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fun download(){
|
||||
UISlideOverlays.showDownloadWatchlaterOverlay(overlayContainer);
|
||||
}
|
||||
|
||||
override fun onPlayAllClick() {
|
||||
@@ -76,6 +116,7 @@ class WatchLaterFragment : MainFragment() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = "WatchLaterFragment";
|
||||
fun newInstance() = WatchLaterFragment().apply {}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,17 @@ package com.futo.platformplayer.helpers
|
||||
|
||||
class FileHelper {
|
||||
companion object {
|
||||
val allowedCharacters = HashSet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-.".toCharArray().toList());
|
||||
|
||||
|
||||
fun String.sanitizeFileName(): String {
|
||||
return this.filter { allowedCharacters.contains(it) };
|
||||
fun String.sanitizeFileName(allowSpace: Boolean = false): String {
|
||||
return this.filter {
|
||||
(it in '0' .. '9') ||
|
||||
(it in 'a'..'z') ||
|
||||
(it in 'A'..'Z') ||
|
||||
(it == '-' || it == '.' || it == '_' || (it == ' ' && allowSpace)) ||
|
||||
(it in '丁'..'龤') || //Chinese/Kanji
|
||||
(it in '\u3040'..'\u309f') || //Hiragana
|
||||
(it in '\u30A0'..'\u30ff') || //Katakana
|
||||
(it in '\u0600'..'\u06FF') //Arabic
|
||||
}; //Chinese
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,8 @@ class FileLogConsumer : ILogConsumer, Closeable {
|
||||
}
|
||||
|
||||
while (_linesToWrite.isNotEmpty()) {
|
||||
_writer?.appendLine(_linesToWrite.remove());
|
||||
val todo = _linesToWrite.remove()
|
||||
_writer?.appendLine(todo);
|
||||
}
|
||||
|
||||
_writer?.flush();
|
||||
@@ -85,7 +86,7 @@ class FileLogConsumer : ILogConsumer, Closeable {
|
||||
_running = false;
|
||||
_writer?.close();
|
||||
_writer = null;
|
||||
_logThread?.join();
|
||||
//_logThread?.join();
|
||||
_logThread = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ class Subscription {
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||
var lastPostUpdate : OffsetDateTime = OffsetDateTime.MIN;
|
||||
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||
var lastPeekVideo : OffsetDateTime = OffsetDateTime.MIN;
|
||||
|
||||
//Last video interval
|
||||
var uploadInterval : Int = 0;
|
||||
var uploadStreamInterval : Int = 0;
|
||||
@@ -126,6 +129,7 @@ class Subscription {
|
||||
else if(lastVideo.year > 3000)
|
||||
lastVideo = OffsetDateTime.MIN;
|
||||
lastVideoUpdate = OffsetDateTime.now();
|
||||
lastPeekVideo = OffsetDateTime.MIN;
|
||||
}
|
||||
ResultCapabilities.TYPE_MIXED -> {
|
||||
uploadInterval = interval;
|
||||
@@ -134,6 +138,7 @@ class Subscription {
|
||||
else if(lastVideo.year > 3000)
|
||||
lastVideo = OffsetDateTime.MIN;
|
||||
lastVideoUpdate = OffsetDateTime.now();
|
||||
lastPeekVideo = OffsetDateTime.MIN;
|
||||
}
|
||||
ResultCapabilities.TYPE_SUBSCRIPTIONS -> {
|
||||
uploadInterval = interval;
|
||||
|
||||
@@ -316,7 +316,6 @@ class PolycentricCache {
|
||||
.build();
|
||||
|
||||
private const val TAG = "PolycentricCache"
|
||||
const val STAGING_SERVER = "https://srv1-stg.polycentric.io"
|
||||
const val SERVER = "https://srv1-prod.polycentric.io"
|
||||
private var _instance: PolycentricCache? = null;
|
||||
private val CACHE_EXPIRATION_SECONDS = 60 * 5;
|
||||
|
||||
@@ -270,7 +270,7 @@ class DownloadService : Service() {
|
||||
|
||||
fun closeDownloadSession() {
|
||||
Logger.i(TAG, "closeDownloadSession");
|
||||
stopForeground(STOP_FOREGROUND_DETACH);
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
_notificationManager?.cancel(DOWNLOAD_NOTIF_ID);
|
||||
stopService();
|
||||
_started = false;
|
||||
|
||||
@@ -188,7 +188,7 @@ class ExportingService : Service() {
|
||||
|
||||
fun closeExportSession() {
|
||||
Logger.i(TAG, "closeExportSession");
|
||||
stopForeground(STOP_FOREGROUND_DETACH);
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
_notificationManager?.cancel(EXPORT_NOTIF_ID);
|
||||
stopService();
|
||||
_started = false;
|
||||
|
||||
@@ -54,6 +54,9 @@ import kotlin.system.measureTimeMillis
|
||||
class StateApp {
|
||||
val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active
|
||||
|
||||
val sessionId = UUID.randomUUID().toString();
|
||||
|
||||
|
||||
fun getExternalGeneralDirectory(context: Context): DocumentFile? {
|
||||
val generalUri = Settings.instance.storage.getStorageGeneralUri();
|
||||
if(isValidStorageUri(context, generalUri))
|
||||
@@ -330,7 +333,7 @@ class StateApp {
|
||||
suspend fun backgroundStarting(context: Context, scope: CoroutineScope, withFiles: Boolean, withPlugins: Boolean) {
|
||||
if(contextOrNull == null) {
|
||||
Logger.i(TAG, "BACKGROUND STATE: Starting");
|
||||
if(!Logger.hasConsumers && BuildConfig.DEBUG) {
|
||||
if(!Logger.hasConsumers && (BuildConfig.DEBUG)) {
|
||||
Logger.i(TAG, "BACKGROUND STATE: Initialize logger");
|
||||
Logger.setLogConsumers(listOf(AndroidLogConsumer()));
|
||||
}
|
||||
@@ -568,18 +571,22 @@ class StateApp {
|
||||
StateAnnouncement.instance.deleteAnnouncement("plugin-update")
|
||||
|
||||
scopeOrNull?.launch(Dispatchers.IO) {
|
||||
val updateAvailable = StatePlatform.instance.checkForUpdates()
|
||||
val updateAvailable = StatePlugins.instance.checkForUpdates()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (updateAvailable.isNotEmpty()) {
|
||||
UIDialogs.appToast(
|
||||
ToastView.Toast(updateAvailable
|
||||
.map { " - " + it.name }
|
||||
.map { " - " + it.first.name }
|
||||
.joinToString("\n"),
|
||||
true,
|
||||
null,
|
||||
"Plugin updates available"
|
||||
));
|
||||
|
||||
for(update in updateAvailable)
|
||||
if(StatePlatform.instance.isClientEnabled(update.first.id))
|
||||
UIDialogs.showPluginUpdateDialog(context, update.first, update.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ class StateDeveloper {
|
||||
|
||||
private var _devLogsIndex: Int = 0;
|
||||
private val _devLogs: MutableList<DevLog> = mutableListOf();
|
||||
private val _devHttpExchanges: MutableList<DevHttpExchange> = mutableListOf();
|
||||
|
||||
var devProxy: DevProxySettings? = null;
|
||||
|
||||
fun initializeDev(id: String) {
|
||||
currentDevID = id;
|
||||
@@ -94,6 +97,21 @@ class StateDeveloper {
|
||||
}
|
||||
}
|
||||
|
||||
fun addDevHttpExchange(exchange: DevHttpExchange) {
|
||||
synchronized(_devHttpExchanges) {
|
||||
if(_devHttpExchanges.size > 15)
|
||||
_devHttpExchanges.removeAt(0);
|
||||
_devHttpExchanges.add(exchange);
|
||||
}
|
||||
}
|
||||
fun getHttpExchangesAndClear(): List<DevHttpExchange> {
|
||||
synchronized(_devHttpExchanges) {
|
||||
val data = _devHttpExchanges.toList();
|
||||
_devHttpExchanges.clear();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
fun setDevClientSettings(settings: HashMap<String, String?>) {
|
||||
val client = StatePlatform.instance.getDevClient();
|
||||
client?.let {
|
||||
@@ -138,4 +156,12 @@ class StateDeveloper {
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class DevLog(val id: Int, val devId: String, val type: String, val log: String);
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class DevHttpRequest(val method: String, val url: String, val headers: Map<String, String>, val body: String, val status: Int = 0);
|
||||
@kotlinx.serialization.Serializable
|
||||
data class DevHttpExchange(val request: DevHttpRequest, val response: DevHttpRequest);
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class DevProxySettings(val url: String, val port: Int)
|
||||
}
|
||||
@@ -97,6 +97,9 @@ class StateDownloads {
|
||||
}
|
||||
}
|
||||
|
||||
fun getWatchLaterDescriptor(): PlaylistDownloadDescriptor? {
|
||||
return _downloadPlaylists.getItems().find { it.id == VideoDownload.GROUP_WATCHLATER };
|
||||
}
|
||||
fun getCachedPlaylists(): List<PlaylistDownloaded> {
|
||||
return _downloadPlaylists.getItems()
|
||||
.map { Pair(it, StatePlaylists.instance.getPlaylist(it.id)) }
|
||||
@@ -124,19 +127,32 @@ class StateDownloads {
|
||||
val pdl = getPlaylistDownload(id);
|
||||
if(pdl != null)
|
||||
_downloadPlaylists.delete(pdl);
|
||||
getDownloading().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
|
||||
.forEach { removeDownload(it) };
|
||||
getDownloadedVideos().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
|
||||
.forEach { deleteCachedVideo(it.id) };
|
||||
if(id == VideoDownload.GROUP_WATCHLATER) {
|
||||
getDownloading().filter { it.groupType == VideoDownload.GROUP_WATCHLATER && it.groupID == id }
|
||||
.forEach { removeDownload(it) };
|
||||
getDownloadedVideos().filter { it.groupType == VideoDownload.GROUP_WATCHLATER && it.groupID == id }
|
||||
.forEach { deleteCachedVideo(it.id) };
|
||||
}
|
||||
else {
|
||||
getDownloading().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
|
||||
.forEach { removeDownload(it) };
|
||||
getDownloadedVideos().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
|
||||
.forEach { deleteCachedVideo(it.id) };
|
||||
}
|
||||
}
|
||||
|
||||
fun getDownloadedVideos(): List<VideoLocal> {
|
||||
return _downloaded.getItems();
|
||||
}
|
||||
fun getDownloadedVideosPlaylist(str: String): List<VideoLocal> {
|
||||
val videos = _downloaded.findItems { it.groupID == str };
|
||||
return videos;
|
||||
}
|
||||
|
||||
fun getDownloadPlaylists(): List<PlaylistDownloadDescriptor> {
|
||||
return _downloadPlaylists.getItems();
|
||||
}
|
||||
|
||||
fun isPlaylistCached(id: String): Boolean {
|
||||
return getDownloadPlaylists().any{it.id == id};
|
||||
}
|
||||
@@ -177,6 +193,21 @@ class StateDownloads {
|
||||
DownloadService.getOrCreateService(it);
|
||||
}
|
||||
}
|
||||
|
||||
fun checkForOutdatedPlaylistVideos(playlistId: String) {
|
||||
val playlistVideos = if(playlistId == VideoDownload.GROUP_WATCHLATER)
|
||||
(if(getWatchLaterDescriptor() != null) StatePlaylists.instance.getWatchLater() else listOf())
|
||||
else
|
||||
getCachedPlaylist(playlistId)?.playlist?.videos ?: return;
|
||||
val playlistVideosDownloaded = getDownloadedVideosPlaylist(playlistId);
|
||||
val urls = playlistVideos.map { it.url }.toHashSet();
|
||||
for(item in playlistVideosDownloaded) {
|
||||
if(!urls.contains(item.url)) {
|
||||
Logger.i(TAG, "Playlist [${playlistId}] deleting removed video [${item.name}]");
|
||||
deleteCachedVideo(item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
fun checkForOutdatedPlaylists(): Boolean {
|
||||
var hasChanged = false;
|
||||
val playlistsDownloaded = getCachedPlaylists();
|
||||
@@ -192,9 +223,59 @@ class StateDownloads {
|
||||
else
|
||||
Logger.v(TAG, "Offline playlist [${playlist.playlist.name}] is up to date");
|
||||
}
|
||||
val downloadWatchLater = getWatchLaterDescriptor();
|
||||
if(downloadWatchLater != null) {
|
||||
continueDownloadWatchLater(downloadWatchLater);
|
||||
}
|
||||
return hasChanged;
|
||||
}
|
||||
|
||||
fun continueDownloadWatchLater(playlistDownload: PlaylistDownloadDescriptor) {
|
||||
var hasNew = false;
|
||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||
for(item in watchLater) {
|
||||
val existing = getCachedVideo(item.id);
|
||||
|
||||
if(!playlistDownload.shouldDownload(item)) {
|
||||
Logger.i(TAG, "Not downloading for watchlater [${playlistDownload.id}] Video [${item.name}]:${item.url}")
|
||||
continue;
|
||||
}
|
||||
if(existing == null) {
|
||||
val ongoingDownload = getDownloading().find { it.id.value == item.id.value && it.id.value != null };
|
||||
if(ongoingDownload != null) {
|
||||
Logger.i(TAG, "New watchlater video (already downloading) ${item.name}");
|
||||
ongoingDownload.groupID = VideoDownload.GROUP_WATCHLATER;
|
||||
ongoingDownload.groupType = VideoDownload.GROUP_WATCHLATER;
|
||||
}
|
||||
else {
|
||||
Logger.i(TAG, "New watchlater video ${item.name}");
|
||||
download(VideoDownload(item, playlistDownload.targetPxCount, playlistDownload.targetBitrate)
|
||||
.withGroup(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER), false);
|
||||
hasNew = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Logger.i(TAG, "New watchlater video (already downloaded) ${item.name}");
|
||||
if(existing.groupID == null) {
|
||||
existing.groupID = VideoDownload.GROUP_WATCHLATER;
|
||||
existing.groupType = VideoDownload.GROUP_WATCHLATER;
|
||||
synchronized(_downloadedSet) {
|
||||
_downloadedSet.add(existing.id);
|
||||
}
|
||||
_downloaded.save(existing);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(watchLater.isNotEmpty() && Settings.instance.downloads.shouldDownload()) {
|
||||
if(hasNew) {
|
||||
UIDialogs.toast("Downloading [Watch Later]")
|
||||
StateApp.withContext {
|
||||
DownloadService.getOrCreateService(it);
|
||||
}
|
||||
}
|
||||
onDownloadsChanged.emit();
|
||||
}
|
||||
}
|
||||
fun continueDownload(playlistDownload: PlaylistDownloadDescriptor, playlist: Playlist) {
|
||||
var hasNew = false;
|
||||
for(item in playlist.videos) {
|
||||
@@ -240,6 +321,11 @@ class StateDownloads {
|
||||
onDownloadsChanged.emit();
|
||||
}
|
||||
}
|
||||
fun downloadWatchLater(targetPixelCount: Long?, targetBitrate: Long?) {
|
||||
val playlistDownload = PlaylistDownloadDescriptor(VideoDownload.GROUP_WATCHLATER, targetPixelCount, targetBitrate);
|
||||
_downloadPlaylists.save(playlistDownload);
|
||||
continueDownloadWatchLater(playlistDownload);
|
||||
}
|
||||
fun download(playlist: Playlist, targetPixelcount: Long?, targetBitrate: Long?) {
|
||||
val playlistDownload = PlaylistDownloadDescriptor(playlist.id, targetPixelcount, targetBitrate);
|
||||
_downloadPlaylists.save(playlistDownload);
|
||||
@@ -370,6 +456,18 @@ class StateDownloads {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val currentDownloads = _downloaded.getItems().map { it.url }.toHashSet();
|
||||
val exporting = _exporting.findItems { !currentDownloads.contains(it.videoLocal.url) };
|
||||
for (export in exporting)
|
||||
_exporting.delete(export);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to delete dangling export:", ex);
|
||||
UIDialogs.toast("Failed to delete dangling export:\n" + ex);
|
||||
}
|
||||
|
||||
return Pair(totalDeletedCount, totalDeleted);
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,6 @@ class StatePlatform {
|
||||
private val _clientsLock = Object();
|
||||
private val _availableClients : ArrayList<IPlatformClient> = ArrayList();
|
||||
private val _enabledClients : ArrayList<IPlatformClient> = ArrayList();
|
||||
private var _updatesAvailableMap: HashSet<String> = hashSetOf();
|
||||
|
||||
//ClientPools are used to isolate plugin usage of certain components from others
|
||||
//This prevents for example a background task like subscriptions from blocking a user from opening a video
|
||||
@@ -529,12 +528,23 @@ class StatePlatform {
|
||||
}
|
||||
|
||||
fun getCommonSearchCapabilities(clientIds: List<String>): ResultCapabilities? {
|
||||
return getCommonSearchCapabilitiesType(clientIds){
|
||||
it.getSearchCapabilities()
|
||||
};
|
||||
}
|
||||
fun getCommonSearchChannelContentsCapabilities(clientIds: List<String>): ResultCapabilities? {
|
||||
return getCommonSearchCapabilitiesType(clientIds){
|
||||
it.getSearchChannelContentsCapabilities()
|
||||
};
|
||||
}
|
||||
|
||||
fun getCommonSearchCapabilitiesType(clientIds: List<String>, capabilitiesGetter: (client: IPlatformClient)-> ResultCapabilities): ResultCapabilities? {
|
||||
try {
|
||||
Logger.i(TAG, "Platform - getCommonSearchCapabilities");
|
||||
|
||||
val clients = getEnabledClients().filter { clientIds.contains(it.id) };
|
||||
val c = clients.firstOrNull() ?: return null;
|
||||
val cap = c.getSearchCapabilities();
|
||||
val cap = capabilitiesGetter(c)//c.getSearchCapabilities();
|
||||
|
||||
//var types = arrayListOf<String>();
|
||||
var sorts = cap.sorts.toMutableList();
|
||||
@@ -544,7 +554,7 @@ class StatePlatform {
|
||||
val filtersToRemove = arrayListOf<Int>();
|
||||
|
||||
for (i in 1 until clients.size) {
|
||||
val clientSearchCapabilities = clients[i].getSearchCapabilities();
|
||||
val clientSearchCapabilities = capabilitiesGetter(clients[i]);//.getSearchCapabilities();
|
||||
|
||||
for (j in 0 until sorts.size) {
|
||||
if (!clientSearchCapabilities.sorts.contains(sorts[j])) {
|
||||
@@ -665,8 +675,11 @@ class StatePlatform {
|
||||
|
||||
val pagerResult: IPager<IPlatformContent>;
|
||||
if(!clientCapabilities.hasType(ResultCapabilities.TYPE_MIXED) &&
|
||||
clientCapabilities.hasType(ResultCapabilities.TYPE_VIDEOS) &&
|
||||
clientCapabilities.hasType(ResultCapabilities.TYPE_STREAMS)) {
|
||||
( clientCapabilities.hasType(ResultCapabilities.TYPE_VIDEOS) ||
|
||||
clientCapabilities.hasType(ResultCapabilities.TYPE_STREAMS) ||
|
||||
clientCapabilities.hasType(ResultCapabilities.TYPE_LIVE) ||
|
||||
clientCapabilities.hasType(ResultCapabilities.TYPE_POSTS)
|
||||
)) {
|
||||
val toQuery = mutableListOf<String>();
|
||||
if(clientCapabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
||||
toQuery.add(ResultCapabilities.TYPE_VIDEOS);
|
||||
@@ -786,6 +799,10 @@ class StatePlatform {
|
||||
return client.getChannelContents(channelUrl, type, ordering) ;
|
||||
}
|
||||
|
||||
fun peekChannelContents(baseClient: IPlatformClient, channelUrl: String, type: String?): List<IPlatformContent> {
|
||||
val client = _channelClientPool.getClientPooled(baseClient, Settings.instance.subscriptions.getSubscriptionsConcurrency());
|
||||
return client.peekChannelContents(channelUrl, type) ;
|
||||
}
|
||||
|
||||
fun getChannelLive(url: String, updateSubscriptions: Boolean = true): IPlatformChannel {
|
||||
val channel = getChannelClient(url).getChannel(url);
|
||||
@@ -907,66 +924,7 @@ class StatePlatform {
|
||||
}
|
||||
}
|
||||
|
||||
fun hasUpdateAvailable(c: SourcePluginConfig): Boolean {
|
||||
val updatesAvailableMap = _updatesAvailableMap
|
||||
synchronized(updatesAvailableMap) {
|
||||
return updatesAvailableMap.contains(c.id)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkForUpdates(): List<SourcePluginConfig> = withContext(Dispatchers.IO) {
|
||||
var configs = mutableListOf<SourcePluginConfig>()
|
||||
val updatesAvailableFor = hashSetOf<String>()
|
||||
for (availableClient in getAvailableClients().filter { it is JSClient && it.descriptor.appSettings.checkForUpdates }) {
|
||||
if (availableClient !is JSClient) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (checkForUpdates(availableClient.config)) {
|
||||
configs.add(availableClient.config);
|
||||
updatesAvailableFor.add(availableClient.config.id)
|
||||
}
|
||||
}
|
||||
|
||||
_updatesAvailableMap = updatesAvailableFor
|
||||
return@withContext configs;
|
||||
}
|
||||
|
||||
fun clearUpdateAvailable(c: SourcePluginConfig) {
|
||||
val updatesAvailableMap = _updatesAvailableMap
|
||||
synchronized(updatesAvailableMap) {
|
||||
updatesAvailableMap.remove(c.id)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkForUpdates(c: SourcePluginConfig): Boolean = withContext(Dispatchers.IO) {
|
||||
val sourceUrl = c.sourceUrl ?: return@withContext false;
|
||||
|
||||
Logger.i(TAG, "Check for source updates '${c.name}'.");
|
||||
try {
|
||||
val client = ManagedHttpClient();
|
||||
val response = client.get(sourceUrl);
|
||||
Logger.i(TAG, "Downloading source config '$sourceUrl'.");
|
||||
|
||||
if (!response.isOk || response.body == null) {
|
||||
return@withContext false;
|
||||
}
|
||||
|
||||
val configJson = response.body.string();
|
||||
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
||||
|
||||
val config = SourcePluginConfig.fromJson(configJson);
|
||||
if (config.version <= c.version) {
|
||||
return@withContext false;
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Update is available (config.version=${config.version}, source.config.version=${c.version}).");
|
||||
return@withContext true;
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to check for updates.", e);
|
||||
return@withContext false;
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var _instance : StatePlatform? = null;
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Playlist
|
||||
import com.futo.platformplayer.services.MediaPlaybackService
|
||||
import com.futo.platformplayer.video.PlayerManager
|
||||
@@ -633,6 +634,7 @@ class StatePlayer {
|
||||
val instance = _instance;
|
||||
_instance = null;
|
||||
instance?.dispose();
|
||||
Logger.i(TAG, "Disposed StatePlayer");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.downloads.VideoDownload
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||
import com.futo.platformplayer.exceptions.ReconstructionException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -66,6 +67,10 @@ class StatePlaylists {
|
||||
_watchlistOrderStore.save();
|
||||
}
|
||||
onWatchLaterChanged.emit();
|
||||
|
||||
if(StateDownloads.instance.getWatchLaterDescriptor() != null) {
|
||||
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
||||
}
|
||||
}
|
||||
fun removeFromWatchLater(video: SerializedPlatformVideo) {
|
||||
synchronized(_watchlistStore) {
|
||||
@@ -74,6 +79,10 @@ class StatePlaylists {
|
||||
_watchlistOrderStore.save();
|
||||
}
|
||||
onWatchLaterChanged.emit();
|
||||
|
||||
if(StateDownloads.instance.getWatchLaterDescriptor() != null) {
|
||||
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
||||
}
|
||||
}
|
||||
fun addToWatchLater(video: SerializedPlatformVideo) {
|
||||
synchronized(_watchlistStore) {
|
||||
@@ -82,6 +91,8 @@ class StatePlaylists {
|
||||
_watchlistOrderStore.save();
|
||||
}
|
||||
onWatchLaterChanged.emit();
|
||||
|
||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||
}
|
||||
|
||||
fun getLastPlayedPlaylist() : Playlist? {
|
||||
@@ -131,6 +142,11 @@ class StatePlaylists {
|
||||
fun createOrUpdatePlaylist(playlist: Playlist) {
|
||||
playlist.dateUpdate = OffsetDateTime.now();
|
||||
playlistStore.saveAsync(playlist, true);
|
||||
if(playlist.id.isNotEmpty()) {
|
||||
if (StateDownloads.instance.isPlaylistCached(playlist.id)) {
|
||||
StateDownloads.instance.checkForOutdatedPlaylistVideos(playlist.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
fun addToPlaylist(id: String, video: IPlatformVideo) {
|
||||
synchronized(playlistStore) {
|
||||
@@ -143,6 +159,9 @@ class StatePlaylists {
|
||||
|
||||
fun removePlaylist(playlist: Playlist) {
|
||||
playlistStore.delete(playlist);
|
||||
if(StateDownloads.instance.isPlaylistCached(playlist.id)) {
|
||||
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
||||
}
|
||||
}
|
||||
|
||||
fun createPlaylistShareUri(context: Context, playlist: Playlist): Uri {
|
||||
|
||||
@@ -43,6 +43,7 @@ class StatePlugins {
|
||||
private var _embeddedSourcesDefault: List<String>? = null
|
||||
private var _sourcesUnderConstruction: Map<String, ImageVariable>? = null
|
||||
|
||||
private var _updatesAvailableMap: HashSet<String> = hashSetOf();
|
||||
|
||||
fun getPluginIconOrNull(id: String): ImageVariable? {
|
||||
if(iconsDir.hasIcon(id))
|
||||
@@ -55,6 +56,70 @@ class StatePlugins {
|
||||
.load();
|
||||
}
|
||||
|
||||
|
||||
suspend fun checkForUpdates(): List<Pair<SourcePluginConfig, SourcePluginConfig>> = withContext(Dispatchers.IO) {
|
||||
var configs = mutableListOf<Pair<SourcePluginConfig, SourcePluginConfig>>()
|
||||
val updatesAvailableFor = hashSetOf<String>()
|
||||
for (availableClient in StatePlatform.instance.getAvailableClients().filter { it is JSClient && it.descriptor.appSettings.checkForUpdates }) {
|
||||
if (availableClient !is JSClient) {
|
||||
continue
|
||||
}
|
||||
|
||||
val newConfig = checkForUpdates(availableClient.config);
|
||||
if (newConfig != null) {
|
||||
configs.add(Pair(availableClient.config, newConfig));
|
||||
updatesAvailableFor.add(availableClient.config.id)
|
||||
}
|
||||
}
|
||||
|
||||
_updatesAvailableMap = updatesAvailableFor
|
||||
return@withContext configs;
|
||||
}
|
||||
private 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}'.");
|
||||
try {
|
||||
val client = ManagedHttpClient();
|
||||
val response = client.get(sourceUrl);
|
||||
Logger.i(TAG, "Downloading source config '$sourceUrl'.");
|
||||
|
||||
if (!response.isOk || response.body == null) {
|
||||
return@withContext null;
|
||||
}
|
||||
|
||||
val configJson = response.body.string();
|
||||
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
||||
|
||||
val config = SourcePluginConfig.fromJson(configJson);
|
||||
if (config.version <= c.version) {
|
||||
return@withContext null;
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Update is available (config.version=${config.version}, source.config.version=${c.version}).");
|
||||
return@withContext config;
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to check for updates.", e);
|
||||
return@withContext null;
|
||||
}
|
||||
}
|
||||
fun hasUpdateAvailable(c: SourcePluginConfig): Boolean {
|
||||
val updatesAvailableMap = _updatesAvailableMap
|
||||
synchronized(updatesAvailableMap) {
|
||||
return updatesAvailableMap.contains(c.id)
|
||||
}
|
||||
}
|
||||
fun clearUpdateAvailable(c: SourcePluginConfig) {
|
||||
val updatesAvailableMap = _updatesAvailableMap
|
||||
synchronized(updatesAvailableMap) {
|
||||
updatesAvailableMap.remove(c.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
fun loginPlugin(context: Context, id: String, afterLogin: ()->Unit): Boolean {
|
||||
val descriptor = getPlugin(id) ?: return false;
|
||||
val config = descriptor.config;
|
||||
@@ -134,8 +199,11 @@ class StatePlugins {
|
||||
val embeddedConfig = getEmbeddedPluginConfig(context, embedded.value);
|
||||
if(embeddedConfig != null) {
|
||||
val existing = getPlugin(embedded.key);
|
||||
if(existing != null && (existing.config.version < embeddedConfig.version || (force || FORCE_REINSTALL_EMBEDDED))) {
|
||||
Logger.i(TAG, "Outdated Embedded plugin [${existing.config.id}] ${existing.config.name} (${existing.config.version} < ${embeddedConfig.version}), reinstalling");
|
||||
if(existing == null || (existing.config.version < embeddedConfig.version || (force || FORCE_REINSTALL_EMBEDDED))) {
|
||||
if (existing != null)
|
||||
Logger.i(TAG, "Outdated Embedded plugin [${existing.config.id}] ${existing.config.name} (${existing.config.version} < ${embeddedConfig.version}), reinstalling");
|
||||
else
|
||||
Logger.i(TAG, "Embedded plugin nog installed [${embeddedConfig.id}] ${embeddedConfig.name} (${embeddedConfig.version}), installing");
|
||||
installEmbeddedPlugin(context, embedded.value)
|
||||
}
|
||||
else if(existing != null && _isFirstEmbedUpdate) {
|
||||
@@ -350,6 +418,49 @@ class StatePlugins {
|
||||
else verifyCanInstall();
|
||||
}
|
||||
|
||||
fun installPluginBackground(context: Context, scope: CoroutineScope, config: SourcePluginConfig, script: String, onProgress: (text: String, progress: Double)->Unit, onConcluded: (ex: Throwable?)->Unit) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val client = ManagedHttpClient();
|
||||
try {
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress.invoke("Validating script", 0.25);
|
||||
}
|
||||
|
||||
val tempDescriptor = SourcePluginDescriptor(config);
|
||||
val plugin = JSClient(context, tempDescriptor, null, script);
|
||||
plugin.validate();
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress.invoke("Downloading Icon", 0.5);
|
||||
}
|
||||
|
||||
val icon = config.absoluteIconUrl?.let { absIconUrl ->
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress.invoke("Saving plugin", 0.75);
|
||||
}
|
||||
val iconResp = client.get(absIconUrl);
|
||||
if(iconResp.isOk)
|
||||
return@let iconResp.body?.byteStream()?.use { it.readBytes() };
|
||||
return@let null;
|
||||
}
|
||||
val installEx = StatePlugins.instance.createPlugin(config, script, icon, true);
|
||||
if(installEx != null)
|
||||
throw installEx;
|
||||
StatePlatform.instance.updateAvailableClients(context);
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress.invoke("Finished", 1.0)
|
||||
onConcluded.invoke(null);
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Logger.e(TAG, ex.message ?: "null", ex);
|
||||
withContext(Dispatchers.Main) {
|
||||
onConcluded.invoke(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getPlugin(id: String): SourcePluginDescriptor? {
|
||||
if(id == StateDeveloper.DEV_ID)
|
||||
throw IllegalStateException("Attempted to retrieve a persistent developer plugin, this is not allowed");
|
||||
|
||||
+21
-4
@@ -1,5 +1,6 @@
|
||||
package com.futo.platformplayer.subscription
|
||||
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.getNowDiffHours
|
||||
@@ -7,6 +8,7 @@ import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
|
||||
class SmartSubscriptionAlgorithm(
|
||||
@@ -70,18 +72,30 @@ class SmartSubscriptionAlgorithm(
|
||||
} else {
|
||||
val fetchTasks = mutableListOf<SubscriptionTask>();
|
||||
val cacheTasks = mutableListOf<SubscriptionTask>();
|
||||
var peekTasks = mutableListOf<SubscriptionTask>();
|
||||
|
||||
for(task in clientTasks.second) {
|
||||
if (!task.fromCache && fetchTasks.size < limit) {
|
||||
fetchTasks.add(task);
|
||||
} else {
|
||||
task.fromCache = true;
|
||||
cacheTasks.add(task);
|
||||
if(peekTasks.size < 100 &&
|
||||
Settings.instance.subscriptions.peekChannelContents &&
|
||||
(task.sub.lastPeekVideo.year < 1971 || task.sub.lastPeekVideo < task.sub.lastVideoUpdate) &&
|
||||
task.client.capabilities.hasPeekChannelContents &&
|
||||
task.client.getPeekChannelTypes().contains(task.type)) {
|
||||
task.fromPeek = true;
|
||||
task.fromCache = true;
|
||||
peekTasks.add(task);
|
||||
}
|
||||
else {
|
||||
task.fromCache = true;
|
||||
cacheTasks.add(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
Logger.i(TAG, "Subscription Client Budget [${clientTasks.first.name}]: ${fetchTasks.size}/${limit}")
|
||||
|
||||
finalTasks.addAll(fetchTasks + cacheTasks);
|
||||
finalTasks.addAll(fetchTasks + peekTasks + cacheTasks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +129,9 @@ class SmartSubscriptionAlgorithm(
|
||||
val lastUpdateHoursAgo = lastUpdate.getNowDiffHours();
|
||||
val expectedHours = (interval * 24) - lastUpdateHoursAgo.toDouble();
|
||||
|
||||
return (expectedHours * 100).toInt();
|
||||
if((type == ResultCapabilities.TYPE_MIXED || type == ResultCapabilities.TYPE_VIDEOS) && (sub.lastPeekVideo.year > 1970 && sub.lastPeekVideo > sub.lastVideoUpdate))
|
||||
return 0;
|
||||
else
|
||||
return (expectedHours * 100).toInt();
|
||||
}
|
||||
}
|
||||
+31
-5
@@ -24,6 +24,7 @@ import com.futo.platformplayer.states.StateCache
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
import java.util.concurrent.ForkJoinTask
|
||||
@@ -48,15 +49,17 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
val tasksGrouped = tasks.groupBy { it.client }
|
||||
|
||||
Logger.i(TAG, "Starting Subscriptions Fetch:\n" +
|
||||
tasksGrouped.map { " ${it.key.name}: ${it.value.count { !it.fromCache }}, Cached(${it.value.count { it.fromCache } })" }.joinToString("\n"));
|
||||
tasksGrouped.map { " ${it.key.name}: ${it.value.count { !it.fromCache }}, Cached(${it.value.count { it.fromCache } - it.value.count { it.fromPeek && it.fromCache }}), Peek(${it.value.count { it.fromPeek }})" }.joinToString("\n"));
|
||||
|
||||
try {
|
||||
for(clientTasks in tasksGrouped) {
|
||||
val clientTaskCount = clientTasks.value.filter { !it.fromCache }.size;
|
||||
val clientCacheCount = clientTasks.value.size - clientTaskCount;
|
||||
val clientTaskCount = clientTasks.value.count { !it.fromCache };
|
||||
val clientCacheCount = clientTasks.value.count { it.fromCache && !it.fromPeek };
|
||||
val clientPeekCount = clientTasks.value.count { it.fromPeek };
|
||||
val limit = clientTasks.key.getSubscriptionRateLimit();
|
||||
if(clientCacheCount > 0 && clientTaskCount > 0 && limit != null && clientTaskCount >= limit && StateApp.instance.contextOrNull?.let { it is MainActivity && it.isFragmentActive<SubscriptionsFeedFragment>() } == true) {
|
||||
UIDialogs.appToast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)");
|
||||
UIDialogs.appToast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). " +
|
||||
"(${if(clientPeekCount > 0) "${clientPeekCount} peek, " else ""}${clientCacheCount} cached)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,8 +138,30 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
|
||||
for(task in tasks) {
|
||||
val forkTask = threadPool.submit<SubscriptionTaskResult> {
|
||||
if(task.fromPeek) {
|
||||
try {
|
||||
|
||||
val time = measureTimeMillis {
|
||||
val peekResults = StatePlatform.instance.peekChannelContents(task.client, task.url, task.type);
|
||||
val mostRecent = peekResults.firstOrNull();
|
||||
task.sub.lastPeekVideo = mostRecent?.datetime ?: OffsetDateTime.MIN;
|
||||
task.sub.saveAsync();
|
||||
val cacheItems = peekResults.filter { it.datetime != null && it.datetime!! > task.sub.lastVideoUpdate };
|
||||
//Fix for current situation
|
||||
for(item in cacheItems) {
|
||||
if(item.author.thumbnail.isNullOrEmpty())
|
||||
item.author.thumbnail = task.sub.channel.thumbnail;
|
||||
}
|
||||
StateCache.instance.cacheContents(cacheItems, false);
|
||||
}
|
||||
Logger.i("StateSubscriptions", "Subscription peek [${task.sub.channel.name}]:${task.type} results in ${time}ms");
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(StateSubscriptions.TAG, "Subscription peek [${task.sub.channel.name}] failed", ex);
|
||||
}
|
||||
}
|
||||
synchronized(cachedChannels) {
|
||||
if(task.fromCache) {
|
||||
if(task.fromCache || task.fromPeek) {
|
||||
finished++;
|
||||
onProgress.emit(finished, forkTasks.size);
|
||||
if(cachedChannels.contains(task.url)) {
|
||||
@@ -218,6 +243,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
val url: String,
|
||||
val type: String,
|
||||
var fromCache: Boolean = false,
|
||||
var fromPeek: Boolean = false,
|
||||
var urgency: Int = 0
|
||||
);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||
val onAddToClicked = Event1<IPlatformContent>();
|
||||
val onAddToQueueClicked = Event1<IPlatformContent>();
|
||||
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
|
||||
val onLongPress = Event1<IPlatformContent>();
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
@@ -56,6 +57,7 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
|
||||
onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit);
|
||||
onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit);
|
||||
onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit);
|
||||
onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit);
|
||||
onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit);
|
||||
};
|
||||
1 -> ChannelListFragment.newInstance().apply { onClickChannel.subscribe(onChannelClicked::emit) };
|
||||
|
||||
@@ -48,6 +48,7 @@ class CommentViewHolder : ViewHolder {
|
||||
|
||||
var onRepliesClick = Event1<IPlatformComment>();
|
||||
var onDelete = Event1<IPlatformComment>();
|
||||
var onAuthorClick = Event1<IPlatformComment>();
|
||||
var comment: IPlatformComment? = null
|
||||
private set;
|
||||
|
||||
@@ -95,6 +96,19 @@ class CommentViewHolder : ViewHolder {
|
||||
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
|
||||
};
|
||||
|
||||
_creatorThumbnail.onClick.subscribe {
|
||||
val c = comment ?: return@subscribe;
|
||||
onAuthorClick.emit(c);
|
||||
}
|
||||
|
||||
_creatorThumbnail.setOnClickListener {
|
||||
val c = comment ?: return@setOnClickListener;
|
||||
onAuthorClick.emit(c);
|
||||
}
|
||||
_textAuthor.setOnClickListener {
|
||||
val c = comment ?: return@setOnClickListener;
|
||||
onAuthorClick.emit(c);
|
||||
}
|
||||
_buttonReplies.onClick.subscribe {
|
||||
val c = comment ?: return@subscribe;
|
||||
onRepliesClick.emit(c);
|
||||
|
||||
+12
-3
@@ -53,9 +53,10 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
||||
hideLikesDislikesReplies()
|
||||
}
|
||||
|
||||
var onRepliesClick = Event1<IPlatformComment>();
|
||||
var onDelete = Event1<IPlatformComment>();
|
||||
var onClick = Event1<IPlatformComment>();
|
||||
val onRepliesClick = Event1<IPlatformComment>();
|
||||
val onDelete = Event1<IPlatformComment>();
|
||||
val onClick = Event1<IPlatformComment>();
|
||||
val onAuthorClick = Event1<IPlatformComment>();
|
||||
var comment: IPlatformComment? = null
|
||||
private set;
|
||||
|
||||
@@ -99,6 +100,14 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
||||
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
|
||||
};
|
||||
|
||||
_creatorThumbnail.onClick.subscribe {
|
||||
val c = comment ?: return@subscribe;
|
||||
onAuthorClick.emit(c);
|
||||
}
|
||||
_textAuthor.setOnClickListener {
|
||||
val c = comment ?: return@setOnClickListener;
|
||||
onAuthorClick.emit(c);
|
||||
}
|
||||
_buttonReplies.onClick.subscribe {
|
||||
val c = comment ?: return@subscribe;
|
||||
onRepliesClick.emit(c);
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
|
||||
class DisabledSourceView : LinearLayout {
|
||||
private val _root: LinearLayout;
|
||||
@@ -37,7 +38,7 @@ class DisabledSourceView : LinearLayout {
|
||||
|
||||
_textSource.text = client.name;
|
||||
|
||||
if (client is JSClient && StatePlatform.instance.hasUpdateAvailable(client.config)) {
|
||||
if (client is JSClient && StatePlugins.instance.hasUpdateAvailable(client.config)) {
|
||||
_textSourceSubtitle.text = context.getString(R.string.update_available_exclamation)
|
||||
_textSourceSubtitle.setTextColor(context.getColor(R.color.light_blue_400))
|
||||
_textSourceSubtitle.typeface = resources.getFont(R.font.inter_regular)
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
|
||||
class EnabledSourceViewHolder : ViewHolder {
|
||||
private val _imageSource: ImageView;
|
||||
@@ -61,7 +62,7 @@ class EnabledSourceViewHolder : ViewHolder {
|
||||
|
||||
_textSource.text = client.name
|
||||
|
||||
if (client is JSClient && StatePlatform.instance.hasUpdateAvailable(client.config)) {
|
||||
if (client is JSClient && StatePlugins.instance.hasUpdateAvailable(client.config)) {
|
||||
_textSourceSubtitle.text = itemView.context.getString(R.string.update_available_exclamation)
|
||||
_textSourceSubtitle.setTextColor(itemView.context.getColor(R.color.light_blue_400))
|
||||
_textSourceSubtitle.typeface = itemView.resources.getFont(R.font.inter_regular)
|
||||
|
||||
+4
@@ -39,6 +39,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||
val onAddToClicked = Event1<IPlatformContent>();
|
||||
val onAddToQueueClicked = Event1<IPlatformContent>();
|
||||
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
|
||||
val onLongPress = Event1<IPlatformContent>();
|
||||
|
||||
private var _taskLoadContent = TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>(
|
||||
@@ -95,6 +96,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||
this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit);
|
||||
this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit);
|
||||
this.onAddToQueueClicked.subscribe(this@PreviewContentListAdapter.onAddToQueueClicked::emit);
|
||||
this.onAddToWatchLaterClicked.subscribe(this@PreviewContentListAdapter.onAddToWatchLaterClicked::emit);
|
||||
};
|
||||
private fun createLockedViewHolder(viewGroup: ViewGroup): PreviewLockedViewHolder = PreviewLockedViewHolder(viewGroup, _feedStyle).apply {
|
||||
this.onLockedUrlClicked.subscribe(this@PreviewContentListAdapter.onUrlClicked::emit);
|
||||
@@ -106,6 +108,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||
this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit);
|
||||
this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit);
|
||||
this.onAddToQueueClicked.subscribe(this@PreviewContentListAdapter.onAddToQueueClicked::emit);
|
||||
this.onAddToWatchLaterClicked.subscribe(this@PreviewContentListAdapter.onAddToWatchLaterClicked::emit);
|
||||
this.onLongPress.subscribe(this@PreviewContentListAdapter.onLongPress::emit);
|
||||
};
|
||||
private fun createPlaylistViewHolder(viewGroup: ViewGroup): PreviewPlaylistViewHolder = PreviewPlaylistViewHolder(viewGroup, _feedStyle).apply {
|
||||
@@ -161,6 +164,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||
onChannelClicked.clear();
|
||||
onAddToClicked.clear();
|
||||
onAddToQueueClicked.clear();
|
||||
onAddToWatchLaterClicked.clear();
|
||||
}
|
||||
|
||||
private fun previewContentDetails(viewHolder: ContentPreviewViewHolder, videoDetails: IPlatformContentDetails?) {
|
||||
|
||||
+2
@@ -19,6 +19,7 @@ class PreviewNestedVideoViewHolder : ContentPreviewViewHolder {
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||
val onAddToClicked = Event1<IPlatformVideo>();
|
||||
val onAddToQueueClicked = Event1<IPlatformVideo>();
|
||||
val onAddToWatchLaterClicked = Event1<IPlatformVideo>();
|
||||
|
||||
override val content: IPlatformContent? get() = view.content;
|
||||
private val view: PreviewNestedVideoView get() = itemView as PreviewNestedVideoView;
|
||||
@@ -31,6 +32,7 @@ class PreviewNestedVideoViewHolder : ContentPreviewViewHolder {
|
||||
view.onChannelClicked.subscribe(onChannelClicked::emit);
|
||||
view.onAddToClicked.subscribe(onAddToClicked::emit);
|
||||
view.onAddToQueueClicked.subscribe(onAddToQueueClicked::emit);
|
||||
view.onAddToWatchLaterClicked.subscribe(onAddToWatchLaterClicked::emit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+4
-1
@@ -61,6 +61,7 @@ open class PreviewVideoView : LinearLayout {
|
||||
protected val _layoutDownloaded: FrameLayout;
|
||||
|
||||
protected val _button_add_to_queue : View;
|
||||
protected val _button_add_to_watch_later : View;
|
||||
protected val _button_add_to : View;
|
||||
|
||||
protected val _exoPlayer: PlayerManager?;
|
||||
@@ -80,6 +81,7 @@ open class PreviewVideoView : LinearLayout {
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||
val onAddToClicked = Event1<IPlatformVideo>();
|
||||
val onAddToQueueClicked = Event1<IPlatformVideo>();
|
||||
val onAddToWatchLaterClicked = Event1<IPlatformVideo>();
|
||||
|
||||
var currentVideo: IPlatformVideo? = null
|
||||
private set
|
||||
@@ -104,6 +106,7 @@ open class PreviewVideoView : LinearLayout {
|
||||
_containerDuration = findViewById(R.id.thumbnail_duration_container);
|
||||
_containerLive = findViewById(R.id.thumbnail_live_container);
|
||||
_button_add_to_queue = findViewById(R.id.button_add_to_queue);
|
||||
_button_add_to_watch_later = findViewById(R.id.button_add_to_watch_later);
|
||||
_button_add_to = findViewById(R.id.button_add_to);
|
||||
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
|
||||
_layoutDownloaded = findViewById(R.id.layout_downloaded);
|
||||
@@ -124,7 +127,7 @@ open class PreviewVideoView : LinearLayout {
|
||||
_textVideoMetadata.setOnClickListener { currentVideo?.let { onChannelClicked.emit(it.author) } };
|
||||
_button_add_to.setOnClickListener { currentVideo?.let { onAddToClicked.emit(it) } };
|
||||
_button_add_to_queue.setOnClickListener { currentVideo?.let { onAddToQueueClicked.emit(it) } };
|
||||
|
||||
_button_add_to_watch_later.setOnClickListener { currentVideo?.let { onAddToWatchLaterClicked.emit(it); } }
|
||||
}
|
||||
|
||||
protected open fun inflate(feedStyle: FeedStyle) {
|
||||
|
||||
+2
@@ -18,6 +18,7 @@ class PreviewVideoViewHolder : ContentPreviewViewHolder {
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||
val onAddToClicked = Event1<IPlatformVideo>();
|
||||
val onAddToQueueClicked = Event1<IPlatformVideo>();
|
||||
val onAddToWatchLaterClicked = Event1<IPlatformVideo>();
|
||||
val onLongPress = Event1<IPlatformVideo>();
|
||||
|
||||
//val context: Context;
|
||||
@@ -34,6 +35,7 @@ class PreviewVideoViewHolder : ContentPreviewViewHolder {
|
||||
view.onChannelClicked.subscribe(onChannelClicked::emit);
|
||||
view.onAddToClicked.subscribe(onAddToClicked::emit);
|
||||
view.onAddToQueueClicked.subscribe(onAddToQueueClicked::emit);
|
||||
view.onAddToWatchLaterClicked.subscribe(onAddToWatchLaterClicked::emit);
|
||||
view.onLongPress.subscribe(onLongPress::emit);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,16 +9,16 @@ import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.models.PlaylistDownloaded
|
||||
|
||||
class PlaylistDownloadItem(context: Context, val playlist: PlaylistDownloaded): LinearLayout(context) {
|
||||
class PlaylistDownloadItem(context: Context, playlistName: String, playlistThumbnail: String?, val obj: Any): LinearLayout(context) {
|
||||
init { inflate(context, R.layout.list_downloaded_playlist, this) }
|
||||
|
||||
var imageView: ImageView = findViewById(R.id.downloaded_playlist_image);
|
||||
var imageText: TextView = findViewById(R.id.downloaded_playlist_name);
|
||||
|
||||
init {
|
||||
imageText.text = playlist.playlist.name;
|
||||
imageText.text = playlistName;
|
||||
Glide.with(imageView)
|
||||
.load(playlist.playlist.videos.firstOrNull()?.thumbnails?.getHQThumbnail())
|
||||
.load(playlistThumbnail)
|
||||
.crossfade()
|
||||
.into(imageView);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.futo.platformplayer.views.overlays
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.webkit.WebView
|
||||
import android.widget.LinearLayout
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.views.SupportView
|
||||
|
||||
class WebviewOverlay : LinearLayout {
|
||||
val onClose = Event0();
|
||||
|
||||
private val _topbar: OverlayTopbar;
|
||||
private val _webview: WebView;
|
||||
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
inflate(context, R.layout.overlay_webview, this)
|
||||
_topbar = findViewById(R.id.topbar);
|
||||
_webview = findViewById(R.id.webview);
|
||||
_webview.settings.javaScriptEnabled = true;
|
||||
|
||||
_topbar.onClose.subscribe(this, onClose::emit);
|
||||
}
|
||||
|
||||
fun goto(url: String) {
|
||||
Logger.i("WebviewOverlay", "Loading [${url}]");
|
||||
_topbar.setInfo(url, "");
|
||||
_webview.loadUrl(url);
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
_topbar.onClose.remove(this);
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -28,13 +28,17 @@ class SlideUpMenuFilters {
|
||||
private var _changed: Boolean = false;
|
||||
private val _lifecycleScope: CoroutineScope;
|
||||
|
||||
private var _isChannelSearch = false;
|
||||
|
||||
var commonCapabilities: ResultCapabilities? = null;
|
||||
|
||||
constructor(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>) {
|
||||
|
||||
constructor(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false) {
|
||||
_lifecycleScope = lifecycleScope;
|
||||
_container = container;
|
||||
_enabledClientsIds = enabledClientsIds;
|
||||
_filterValues = filterValues;
|
||||
_isChannelSearch = isChannelSearch;
|
||||
_slideUpMenuOverlay = SlideUpMenuOverlay(_container.context, _container, container.context.getString(R.string.filters), container.context.getString(R.string.done), true, listOf());
|
||||
_slideUpMenuOverlay.onOK.subscribe {
|
||||
onOK.emit(_enabledClientsIds, _changed);
|
||||
@@ -47,7 +51,10 @@ class SlideUpMenuFilters {
|
||||
private fun updateCommonCapabilities() {
|
||||
_lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val caps = StatePlatform.instance.getCommonSearchCapabilities(_enabledClientsIds);
|
||||
val caps = if(!_isChannelSearch)
|
||||
StatePlatform.instance.getCommonSearchCapabilities(_enabledClientsIds);
|
||||
else
|
||||
StatePlatform.instance.getCommonSearchChannelContentsCapabilities(_enabledClientsIds);
|
||||
synchronized(_filterValues) {
|
||||
if (caps != null) {
|
||||
val keysToRemove = arrayListOf<String>();
|
||||
|
||||
@@ -88,6 +88,7 @@ class CommentsList : ConstraintLayout {
|
||||
private val _layoutScrollToTop: FrameLayout;
|
||||
|
||||
var onRepliesClick = Event1<IPlatformComment>();
|
||||
var onAuthorClick = Event1<IPlatformComment>();
|
||||
var onCommentsLoaded = Event1<Int>();
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
@@ -120,6 +121,7 @@ class CommentsList : ConstraintLayout {
|
||||
childViewHolderFactory = { viewGroup, _ ->
|
||||
val holder = CommentViewHolder(viewGroup);
|
||||
holder.onRepliesClick.subscribe { c -> onRepliesClick.emit(c) };
|
||||
holder.onAuthorClick.subscribe { c -> onAuthorClick.emit(c) };
|
||||
holder.onDelete.subscribe(::onDelete);
|
||||
return@InsertedViewAdapterWithLoader holder;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.datasource.DefaultHttpDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.dash.DashMediaSource
|
||||
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider
|
||||
import androidx.media3.exoplayer.hls.HlsMediaSource
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.MergingMediaSource
|
||||
@@ -27,6 +28,7 @@ import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
@@ -389,6 +391,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
is LocalAudioSource -> swapAudioSourceLocal(audioSource);
|
||||
is JSAudioUrlRangeSource -> swapAudioSourceUrlRange(audioSource);
|
||||
is JSHLSManifestAudioSource -> swapAudioSourceHLS(audioSource);
|
||||
is IAudioUrlWidevineSource -> swapAudioSourceUrlWidevine(audioSource)
|
||||
is IAudioUrlSource -> swapAudioSourceUrl(audioSource);
|
||||
null -> _lastAudioMediaSource = null;
|
||||
else -> throw IllegalArgumentException("Unsupported video source [${audioSource.javaClass.simpleName}]");
|
||||
@@ -508,6 +511,31 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
.createMediaSource(MediaItem.fromUri(audioSource.url));
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapAudioSourceUrlWidevine(audioSource: IAudioUrlWidevineSource) {
|
||||
Logger.i(TAG, "Loading AudioSource [UrlWidevine]")
|
||||
val dataSource = if (audioSource is JSSource && audioSource.hasRequestModifier)
|
||||
audioSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)
|
||||
|
||||
val httpRequestHeaders = mapOf("Authorization" to "Bearer " + audioSource.bearerToken)
|
||||
val provider = DefaultDrmSessionManagerProvider()
|
||||
provider.setDrmHttpDataSourceFactory(dataSource)
|
||||
_lastAudioMediaSource = ProgressiveMediaSource.Factory(dataSource)
|
||||
.setDrmSessionManagerProvider(provider)
|
||||
.createMediaSource(
|
||||
MediaItem.Builder()
|
||||
.setUri(audioSource.getAudioUrl()).setDrmConfiguration(
|
||||
MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
|
||||
.setLicenseUri(audioSource.licenseUri)
|
||||
.setMultiSession(true)
|
||||
.setLicenseRequestHeaders(httpRequestHeaders)
|
||||
.build()
|
||||
).build()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//Prefered source selection
|
||||
fun getPreferredVideoSource(video: IPlatformVideoDetails, targetPixels: Int = -1): IVideoSource? {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M612,668L668,612L520,464L520,280L440,280L440,496L612,668ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM480,800Q613,800 706.5,706.5Q800,613 800,480Q800,347 706.5,253.5Q613,160 480,160Q347,160 253.5,253.5Q160,347 160,480Q160,613 253.5,706.5Q347,800 480,800Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,315 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:background="@color/gray_1d">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:paddingTop="30dp"
|
||||
android:paddingBottom="30dp">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/dialog_ui_choice_top"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible">
|
||||
<ImageView
|
||||
android:id="@+id/icon_plugin"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
app:srcCompat="@drawable/ic_sources" />
|
||||
|
||||
</FrameLayout>
|
||||
<FrameLayout
|
||||
android:id="@+id/dialog_ui_risk_top"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone">
|
||||
<ImageView
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
app:srcCompat="@drawable/ic_warning_yellow" />
|
||||
|
||||
</FrameLayout>
|
||||
<FrameLayout
|
||||
android:id="@+id/dialog_ui_progress_top"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/update_spinner"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
app:srcCompat="@drawable/ic_update_animated"
|
||||
android:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Plugin Update"
|
||||
android:textSize="15sp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_extra_light"
|
||||
android:layout_marginTop="15dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
<TextView
|
||||
android:id="@+id/text_plugin"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Some Plugin Name"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/dialog_ui_bottom_choice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text="A new update is available.\nWould you like to update this plugin?"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text="Updates may be critical to functionality"
|
||||
android:textSize="13sp"
|
||||
android:textColor="@color/pastel_red"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="28dp">
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_cancel_1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/cancel"
|
||||
android:textSize="14dp"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:background="@color/transparent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_update"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_button_primary"
|
||||
android:layout_marginEnd="28dp"
|
||||
android:clickable="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Update"
|
||||
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>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/dialog_ui_bottom_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/text_progress"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text="This plugin has modified its permissions"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/dialog_ui_bottom_risk"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text="This plugin has modified its permissions"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text="Make sure you read the installation screen"
|
||||
android:textSize="13sp"
|
||||
android:textColor="@color/pastel_red"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="28dp">
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_cancel_2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/cancel"
|
||||
android:textSize="14dp"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:background="@color/transparent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_install"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_button_primary"
|
||||
android:layout_marginEnd="28dp"
|
||||
android:clickable="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Reinstall"
|
||||
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>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/dialog_ui_bottom_result"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:id="@+id/text_error"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text=""
|
||||
android:textSize="13sp"
|
||||
android:textColor="@color/pastel_red"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_result"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text="Succesfully updated plugin."
|
||||
android:textSize="13sp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="28dp">
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_ok"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_button_primary"
|
||||
android:layout_marginEnd="28dp"
|
||||
android:clickable="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Ok"
|
||||
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>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -13,7 +13,7 @@
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_construction" />
|
||||
app:srcCompat="@drawable/foreground" />
|
||||
|
||||
<!--<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
@@ -22,17 +22,19 @@
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_futo_logo_text" />-->
|
||||
|
||||
<!--
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:textSize="28dp"
|
||||
android:fontFamily="@font/inter_bold"
|
||||
android:text="TEST BUILD"
|
||||
android:textSize="22dp"
|
||||
android:layout_marginTop="-2dp"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="Grayjay"
|
||||
android:textColor="@color/white"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"/>-->
|
||||
android:layout_marginStart="8dp"/>
|
||||
|
||||
<!--
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
@@ -54,7 +56,7 @@
|
||||
android:text="@string/construction"
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginTop="-8dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>-->
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
|
||||
@@ -542,6 +542,12 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.futo.platformplayer.views.overlays.WebviewOverlay
|
||||
android:id="@+id/videodetail_container_webview"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
||||
android:id="@+id/videodetail_container_queue"
|
||||
android:visibility="gone"
|
||||
|
||||
@@ -226,10 +226,18 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_watch_later"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
app:srcCompat="@drawable/ic_clock_white" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_queue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginEnd="1dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue"
|
||||
@@ -242,20 +250,18 @@
|
||||
<LinearLayout
|
||||
android:id="@+id/button_add_to"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp">
|
||||
|
||||
<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:contentDescription="@string/options"
|
||||
app:srcCompat="@drawable/ic_add_white_8dp" />
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="16dp"
|
||||
android:paddingTop="1dp"
|
||||
android:src="@drawable/ic_settings" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -262,45 +262,53 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:orientation="horizontal"
|
||||
android:paddingEnd="6dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_watch_later"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
app:srcCompat="@drawable/ic_clock_white" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_queue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginEnd="1dp"
|
||||
android:paddingTop="7dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:paddingBottom="3dp"
|
||||
app:srcCompat="@drawable/ic_queue_16dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue" />
|
||||
app:srcCompat="@drawable/ic_queue_16dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_add_to"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp">
|
||||
<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:srcCompat="@drawable/ic_add_white_8dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:contentDescription="@string/options" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="16dp"
|
||||
android:paddingTop="1dp"
|
||||
android:src="@drawable/ic_settings" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/options"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@color/transparent"
|
||||
android:textSize="12dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:layout_marginEnd="4dp"/>
|
||||
android:text="@string/options"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12dp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -150,14 +150,11 @@
|
||||
android:padding="5dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
<ImageButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:srcCompat="@drawable/ic_add_white_8dp"
|
||||
android:background="@color/transparent"
|
||||
android:layout_marginStart="4dp"
|
||||
android:contentDescription="@string/options" />
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="16dp"
|
||||
android:paddingTop="1dp"
|
||||
android:src="@drawable/ic_settings" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -180,13 +177,28 @@
|
||||
android:paddingBottom="2dp"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="7dp"
|
||||
android:layout_marginLeft="7dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_add_to"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_watch_later"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="27dp"
|
||||
android:src="@drawable/ic_clock_white"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:paddingLeft="9dp"
|
||||
android:paddingRight="9dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@drawable/edit_text_background"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_add_to_queue"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_video_name"
|
||||
android:layout_width="fill_parent"
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_queue"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -215,13 +216,28 @@
|
||||
android:paddingBottom="2dp"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="7dp"
|
||||
android:layout_marginLeft="7dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:contentDescription="@string/add_to_queue"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_add_to"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_add_to_watch_later"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="27dp"
|
||||
android:src="@drawable/ic_clock_white"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:paddingLeft="9dp"
|
||||
android:paddingRight="9dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@drawable/edit_text_background"
|
||||
app:layout_constraintLeft_toRightOf="@id/button_add_to_queue"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_video_name"
|
||||
android:layout_width="fill_parent"
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:maxWidth="300dp"
|
||||
tools:text="Queue" />
|
||||
|
||||
<TextView
|
||||
@@ -24,16 +27,21 @@
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@id/text_name"
|
||||
app:layout_constraintRight_toLeftOf="@id/button_container"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="#ACACAC"
|
||||
android:textSize="13dp"
|
||||
android:layout_marginLeft="15dp"
|
||||
android:layout_marginBottom="7dp"
|
||||
android:layout_marginRight="45dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
tools:text="3 videos" />
|
||||
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<com.futo.platformplayer.views.overlays.OverlayTopbar
|
||||
android:id="@+id/topbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
app:title="Web"
|
||||
app:metadata=""
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/topbar"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
</WebView>
|
||||
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -342,6 +342,8 @@
|
||||
<string name="fetch_on_tab_opened_description">Fetch new results when the tab is opened (if no results yet, disabling is not recommended unless you have issues)</string>
|
||||
<string name="always_reload_from_cache">Always reload from cache</string>
|
||||
<string name="always_reload_from_cache_description">This is not recommended, but a possible workaround for some issues.</string>
|
||||
<string name="peek_channel_contents">Peek Channel Contents</string>
|
||||
<string name="peek_channel_contents_description">Peek channel contents if supported by plugin of rate-limited calls, may increase subscription reload time.</string>
|
||||
<string name="get_answers_to_common_questions">Get answers to common questions</string>
|
||||
<string name="give_feedback_on_the_application">Give feedback on the application</string>
|
||||
<string name="info">Info</string>
|
||||
@@ -487,6 +489,11 @@
|
||||
<string name="visibility">Visibility</string>
|
||||
<string name="check_for_updates_setting">Check for updates</string>
|
||||
<string name="check_for_updates_setting_description">If a plugin should be checked for updates on startup</string>
|
||||
<string name="automatic_update_setting">Automatic Update</string>
|
||||
<string name="automatic_update_setting_description">Update automatically on boot if no permissions changed and plugin is enabled</string>
|
||||
<string name="allow_developer_submit">Allow Developer Submissions</string>
|
||||
<string name="allow_developer_submit_description">Allows the developer to send data to their server, be careful as this might include sensitive data.</string>
|
||||
<string name="allow_developer_submit_warning">Make sure you trust the developer. They may gain access to sensitive data. Only enable this when you are trying to help the developer fix a bug.</string>
|
||||
<string name="ratelimit">Rate-limit</string>
|
||||
<string name="ratelimit_description">Settings related to rate-limiting this plugin\'s behavior</string>
|
||||
<string name="ratelimit_sub_setting">Rate-limit Subscriptions</string>
|
||||
|
||||
Submodule
+1
Submodule app/src/stable/assets/sources/bilibili added at 59d2200f92
Submodule app/src/stable/assets/sources/patreon updated: bc13b38411...cee1fda4e8
Submodule app/src/stable/assets/sources/youtube updated: bef199baa9...37e2ed9438
@@ -8,12 +8,12 @@
|
||||
"c0f315f9-0992-4508-a061-f2738724c331": "sources/twitch/TwitchConfig.json",
|
||||
"4a78c2ff-c20f-43ac-8f75-34515df1d320": "sources/kick/KickConfig.json",
|
||||
"aac9e9f0-24b5-11ee-be56-0242ac120002": "sources/patreon/PatreonConfig.json",
|
||||
"9d703ff5-c556-4962-a990-4f000829cb87": "sources/nebula/NebulaConfig.json"
|
||||
"9d703ff5-c556-4962-a990-4f000829cb87": "sources/nebula/NebulaConfig.json",
|
||||
"cf8ea74d-ad9b-489e-a083-539b6aa8648c": "sources/bilibili/build/BiliBiliConfig.json"
|
||||
},
|
||||
"SOURCES_EMBEDDED_DEFAULT": [
|
||||
"35ae969a-a7db-11ed-afa1-0242ac120002"
|
||||
],
|
||||
"SOURCES_UNDER_CONSTRUCTION": {
|
||||
"Subscribestar": "logo_subscribestar"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class ExtensionsFileTests {
|
||||
@Test
|
||||
fun test_sanitizeFileName1() {
|
||||
assertEquals("Helloworld", "Hello world".sanitizeFileName());
|
||||
assertEquals("Hello world", "Hello world".sanitizeFileName(true));
|
||||
assertEquals("漫漫听-点唱-公主冠", "漫漫听-点唱- 公主冠".sanitizeFileName());
|
||||
assertEquals("食べる", "食べ る".sanitizeFileName()); //Hiragana
|
||||
assertEquals("テレビ", "テレ ビ".sanitizeFileName()); //Katakana
|
||||
assertEquals("يخبر", "ي خبر".sanitizeFileName()); //Arabic
|
||||
assertEquals("..testing", "../testing".sanitizeFileName()); //Escaping
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user