Compare commits

..

1 Commits

Author SHA1 Message Date
Koen 816d3c58f2 POST fix. 2024-01-21 18:37:02 +01:00
184 changed files with 1280 additions and 4671 deletions
-12
View File
@@ -58,15 +58,3 @@
[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
[submodule "app/src/stable/assets/sources/spotify"]
path = app/src/stable/assets/sources/spotify
url = ../plugins/spotify.git
[submodule "app/src/unstable/assets/sources/spotify"]
path = app/src/unstable/assets/sources/spotify
url = ../plugins/spotify.git
+32
View File
@@ -0,0 +1,32 @@
# FUTO TEMPORARY LICENSE
This license grants you the rights, and only the rights, set out below in respect of the source code provided. If you take advantage of these rights, you accept this license. If you do not accept the license, do not access the code.
Words used in the Terms of Service have the same meaning in this license. Where there is any inconsistency between this license and those Terms of Service, these terms prevail.
## Section 1: Definitions
- "code" means the source code made available from time, in our sole discretion, for access under this license. Reference to code in this license means the code and any part of it and any derivative of it.
- “compilation” means to compile the code from source code to machine code.
- "defect" means a defect, bug, backdoor, security issue or other deficiency in the code.
- “non-commercial distribution” means distribution of the code or any compilation of the code, or of any other application or program containing the code or any compilation of the code, where such distribution is not intended for or directed towards commercial advantage or monetary compensation.
- "review" means to access, analyse, test and otherwise review the code as a reference, for the sole purpose of analysing it for defects.
- "you" means the licensee of rights set out in this license.
## Section 2: Grant of Rights
1. Subject to the terms of this license, we grant you a non-transferable, non-exclusive, worldwide, royalty-free license to access and use the code solely for the purposes of review, compilation and non-commercial distribution.
2. You may provide the code to anyone else and publish excerpts of it for the purposes of review, compilation and non-commercial distribution, provided that when you do so you make any recipient of the code aware of the terms of this license, they must agree to be bound by the terms of this license and you must attribute the code to the provider.
3. Other than in respect of those parts of the code that were developed by other parties and as specified strictly in accordance with the open source and other licenses under which those parts of the code have been made available, as set out on our website or in those items of code, you are not entitled to use or do anything with the code for any commercial or other purpose, other than review, compilation and non-commercial distribution in accordance with the terms of this license.
4. Subject to the terms of this license, you must at all times comply with and shall be bound by our Terms of Use, Privacy and Data Policy.
## Section 3: Limitations
1. This license does not grant you any rights to use the provider's name, logo, or trademarks and you must not in any way indicate you are authorised to speak on behalf of the provider.
2. If you issue proceedings in any jurisdiction against the provider because you consider the provider has infringed copyright or any patent right in respect of the code (including any joinder or counterclaim), your license to the code is automatically terminated.
3. THE CODE IS MADE AVAILABLE "AS-IS" AND WITHOUT ANY EXPRESS OR IMPLIED GUARANTEES AS TO FITNESS, MERCHANTABILITY, NON-INFRINGEMENT OR OTHERWISE. IT IS NOT BEING PROVIDED IN TRADE BUT ON A VOLUNTARY BASIS ON OUR PART AND IS NOT MADE AVAILABLE FOR ANY USE OUTSIDE THE TERMS OF THIS LICENSE. ANYONE ACCESSING THE CODE MUST ENSURE THEY HAVE THE REQUISITE EXPERTISE TO SECURE THEIR OWN SYSTEM AND DEVICES AND TO ACCESS AND USE THE CODE IN ACCORDANCE WITH THE TERMS OF THIS LICENSE. YOU BEAR THE RISK OF ACCESSING AND USING THE CODE. IN PARTICULAR, THE PROVIDER BEARS NO LIABILITY FOR ANY INTERFERENCE WITH OR ADVERSE EFFECT ON YOUR SYSTEM OR DEVICES AS A RESULT OF YOUR ACCESSING AND USING THE CODE IN ACCORDANCE WITH THE TERMS OF THIS LICENSE OR OTHERWISE.
## Section 4: Termination, suspension and variation
1. We may suspend, terminate or vary the terms of this license and any access to the code at any time, without notice, for any reason or no reason, in respect of any licensee, group of licensees or all licensees including as may be applicable any sub-licensees.
## Section 5: General
1. This license and its interpretation and operation are governed solely by the local law. You agree to submit to the exclusive jurisdiction of the local arbitral tribunals as further described in our Terms of Service and you agree not to raise any jurisdictional issue if we need to enforce an arbitral award or judgment in our jurisdiction or another country.
2. Questions and comments regarding this license are welcomed and should be addressed at https://chat.futo.org/login/.
Last updated 7 June 2023.
-43
View File
@@ -1,43 +0,0 @@
# Grayjay Core License 1.0
## Acceptance
By using the software, you agree to all of the terms and conditions below.
## Copyright License
FUTO Holdings, Inc. (the “Licensor”) grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations below.
## Limitations
You may use or modify the software only for non-commercial purposes such as personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, all without any anticipated commercial application.
You may distribute the software or provide it to others only if you do so free of charge for non-commercial purposes.
Notwithstanding the above, you may not remove or obscure any functionality in the software related to payment to the Licensor in any copy you distribute to others.
You may not alter, remove, or obscure any licensing, copyright, or other notices of the Licensor in the software. Any use of the Licensors trademarks is subject to applicable law.
## Patents
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
## Notices
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. If you modify the software, you must include in any modified copies of the software a prominent notice stating that you have modified the software, such as but not limited to, a statement in a readme file or an in-application about section.
## Fair Use
You may have "fair use" rights for the software under the law. These terms do not limit them.
## No Other Rights
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the Licensor from granting licenses to anyone else. These terms do not imply any other licenses.
## Termination
If you use the software in violation of these terms, such use is not licensed, and your license will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your license will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your license to terminate automatically and permanently.
## No Liability
As far as the law allows, the software comes as is, without any warranty or condition, and the Licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.
## Definitions
- The “Licensor” is the entity offering these terms, FUTO Holdings, Inc.
- The “software” is the software the licensor makes available under these terms, including any portion of it.
- “You” refers to the individual or entity agreeing to these terms.
- “Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. Control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
- “Your license” is the license granted to you for the software under these terms.
- “Use” means anything you do with the software requiring your license.
- “Trademark” means trademarks, service marks, and similar rights.
@@ -1,15 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -262,17 +262,6 @@ 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, {});
}
+5 -205
View File
@@ -7,9 +7,6 @@
<!--<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>
@@ -153,7 +150,7 @@
.pastPluginUrl {
margin-left: auto;
margin-right: auto;
width: 700px;
width: 500px;
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
@@ -163,122 +160,13 @@
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>
<div v-cloak id="cloakLoader" v-if="!page">
<h2>Loading..</h2>
First load may take longer
</div>
<v-main v-cloak>
<v-main>
<div id="topMenu">
<div style="height: 100%; display: inline-block; padding-left: 10px; padding-right: 20px;">
<img src="./dependencies/FutoMainLogo.svg"
@@ -362,13 +250,10 @@
</div>
<div v-if="pastPluginUrls" style="margin-top: 60px; margin-left: 25px;">
<div v-if="pastPluginUrls" style="margin-top: 60px;">
<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>
@@ -517,11 +402,6 @@
<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">
@@ -620,62 +500,7 @@
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<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-btn>Clear</v-btn>
</v-card-actions>
</v-card>
</div>
@@ -713,7 +538,6 @@
<!--<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>
@@ -732,9 +556,7 @@
lastLogIndex: -1,
lastLogDevID: "",
logs: [],
httpExchanges: [],
lastInjectTime: "",
showHttpRequests: false
lastInjectTime: ""
},
Plugin: {
loadUsingTag: false,
@@ -752,9 +574,6 @@
Testing: {
requests: sourceDocs.map(x=>{
x.parameters.forEach(y=>y.value = null);
if(sourceDocUrls[x.title])
x.docUrl = sourceDocUrls[x.title];
return x;
}),
lastResult: "",
@@ -818,16 +637,6 @@
});
}
});
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);
@@ -869,12 +678,6 @@
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(()=>{
@@ -1110,9 +913,6 @@
},
showTestResults(results) {
},
toggleHttpExchange(exchange) {
exchange.response.show = !exchange.response.show;
},
copyClipboard(cpy) {
if(navigator.clipboard)
+5 -3
View File
@@ -127,7 +127,7 @@ declare class PlatformVideoDetails extends PlatformVideo {
}
declare interface PlatformPostDef extends PlatformContentDef {
thumbnails: Thumbnails[],
thumbnails: string[],
images: string[],
description: string
}
@@ -243,7 +243,9 @@ declare class DashSource implements IVideoSource {
declare interface IRequest {
url: string,
headers: Map<string, string>
headers: Map<string, string>,
method: string,
body?: string
}
declare interface IRequestModifierDef {
allowByteSkip: boolean
@@ -251,7 +253,7 @@ declare interface IRequestModifierDef {
declare class RequestModifier {
constructor(obj: IRequestModifierDef) { }
modifyRequest(url: string, headers: Map<string, string>): IRequest;
modifyRequest(url: string, headers: Map<string, string>, method: string, body?: string): IRequest;
}
//Channel
+1 -10
View File
@@ -357,15 +357,6 @@ 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);
@@ -436,7 +427,7 @@ class PlatformPlaylist extends PlatformContent {
constructor(obj) {
super(obj, 4);
this.plugin_type = "PlatformPlaylist";
this.videoCount = obj.videoCount ?? -1;
this.videoCount = obj.videoCount ?? 0;
this.thumbnail = obj.thumbnail;
}
}
@@ -1,4 +1,4 @@
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource
@@ -13,8 +13,6 @@ import java.text.DecimalFormat
import java.time.OffsetDateTime
import java.time.temporal.ChronoUnit
import kotlin.math.abs
import kotlin.math.roundToInt
import kotlin.math.roundToLong
//Long
@@ -121,8 +119,7 @@ fun OffsetDateTime.getNowDiffMonths(): Long {
return ChronoUnit.MONTHS.between(this, OffsetDateTime.now());
}
fun OffsetDateTime.getNowDiffYears(): Long {
val diff = ChronoUnit.MONTHS.between(this, OffsetDateTime.now()) / 12.0;
return diff.roundToLong();
return ChronoUnit.YEARS.between(this, OffsetDateTime.now());
}
fun OffsetDateTime.getDiffDays(otherDate: OffsetDateTime): Long {
@@ -153,7 +150,6 @@ fun OffsetDateTime.toHumanNowDiffString(abs: Boolean = false) : String {
if(value >= secondsInYear) {
value = getNowDiffYears();
if(abs) value = abs(value);
value = Math.max(1, value);
unit = "year";
}
else if(value >= secondsInMonth) {
@@ -50,9 +50,12 @@ 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)) {
Logger.w("Backfill", "Polycentric prod server not added, adding it.")
addServer(PolycentricCache.SERVER)
removeServer(PolycentricCache.SERVER)
}
val exceptions = fullyBackfillServers()
@@ -311,10 +311,7 @@ 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.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)
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 15)
fun clearChannelCache() {
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
StateCache.instance.clear();
@@ -549,8 +546,6 @@ 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) {
@@ -33,7 +33,6 @@ import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.fields.ButtonField
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldWarning
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -494,17 +493,6 @@ class SettingsDev : FragmentedStorageFileJson() {
}
@FormField(R.string.networking, FieldForm.GROUP, -1, 18)
var networking = Networking();
@Serializable
class Networking {
@FormField(R.string.allow_all_certificates, FieldForm.TOGGLE, -1, 0)
@FormFieldWarning(R.string.allow_all_certificates_warning)
var allowAllCertificates: Boolean = false;
}
@Contextual
@Transient
@FormField(R.string.info, FieldForm.GROUP, -1, 19)
@@ -515,8 +503,6 @@ class SettingsDev : FragmentedStorageFileJson() {
var channelCacheStartupCount = StateCache.instance.channelCacheStartupCount;
}
//region BOILERPLATE
override fun encode(): String {
return Json.encodeToString(this);
@@ -18,7 +18,6 @@ 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
@@ -32,17 +31,11 @@ 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
@@ -190,14 +183,6 @@ 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);
@@ -283,48 +268,22 @@ class UIDialogs {
}, UIDialogs.ActionStyle.PRIMARY)
);
}
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;
fun showGeneralRetryErrorDialog(context: Context, msg: String, ex: Throwable? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null) {
val pluginInfo = if(ex is PluginException)
"\nPlugin [${ex.config.name}]" else "";
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)
);
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)
);
}
fun showSingleButtonDialog(context: Context, icon: Int, text: String, buttonText: String, action: (() -> Unit)) {
@@ -384,8 +343,8 @@ class UIDialogs {
}
}
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, cache: ImportCache?, onConcluded: () -> Unit) {
val dialog = ImportDialog(context, store, name, reconstructions, cache, onConcluded);
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, onConcluded: () -> Unit) {
val dialog = ImportDialog(context, store, name, reconstructions, onConcluded);
registerDialogOpened(dialog);
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
@@ -6,7 +6,6 @@ import android.content.Context
import android.content.Intent
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
@@ -38,17 +37,11 @@ import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.states.StateSubscriptionGroups
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.LoaderView
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuFilters
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuRecycler
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
import com.futo.platformplayer.views.pills.RoundButton
import com.futo.platformplayer.views.pills.RoundButtonGroup
@@ -94,37 +87,7 @@ class UISlideOverlays {
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
}, false),
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
SlideUpMenuGroup(container.context, "Subscription Groups",
"You can select which groups this subscription is part of.",
-1, listOf()) else null,
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
SlideUpMenuRecycler(container.context, "as") {
val groups = ArrayList<SubscriptionGroup>(StateSubscriptionGroups.instance.getSubscriptionGroups()
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
.sortedBy { !it.selected });
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? = null;
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
it.onClick.subscribe {
if(it is SubscriptionGroup.Selectable) {
val actualGroup = StateSubscriptionGroups.instance.getSubscriptionGroup(it.id)
?: return@subscribe;
groups.clear();
if(it.selected)
actualGroup.urls.remove(subscription.channel.url);
else
actualGroup.urls.add(subscription.channel.url);
StateSubscriptionGroups.instance.updateSubscriptionGroup(actualGroup);
groups.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups()
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
.sortedBy { !it.selected });
adapter?.notifyContentChanged();
}
}
};
return@SlideUpMenuRecycler adapter;
} else null,
SlideUpMenuGroup(container.context, "Fetch Settings",
"Depending on the platform you might not need to enable a type for it to be available.",
-1, listOf()),
@@ -510,15 +473,10 @@ class UISlideOverlays {
}
}
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
showUnknownVideoDownload(container.context.getString(R.string.playlist), container) { px, bitrate ->
showUnknownVideoDownload(container.context.getString(R.string.video), 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;
@@ -688,17 +646,9 @@ class UISlideOverlays {
val watchLater = StatePlaylists.instance.getWatchLater();
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
(listOf(
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), "download", {
showDownloadVideoOverlay(video, container, true);
}, false),
SlideUpMenuItem(container.context, R.drawable.ic_share, container.context.getString(R.string.share), "Share the video", "share", {
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
container.context.startActivity(Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND;
putExtra(Intent.EXTRA_TEXT, url);
type = "text/plain";
}, null));
}, false),
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download), {
showDownloadVideoOverlay(video, container, true);
}, false),
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
StateMeta.instance.addHiddenCreator(video.author.url);
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
@@ -788,8 +738,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>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters {
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues);
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) {
StatePlugins.instance.clearUpdateAvailable(config)
StatePlatform.instance.clearUpdateAvailable(config)
if(isNew)
lifecycleScope.launch {
StatePlatform.instance.enableClient(listOf(config.id));
@@ -41,14 +41,12 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFrag
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
import com.futo.platformplayer.listeners.OrientationManager
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.states.*
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.*
@@ -104,7 +102,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragMainTutorial: TutorialFragment;
lateinit var _fragMainPlaylists: PlaylistsFragment;
lateinit var _fragMainPlaylist: PlaylistFragment;
lateinit var _fragMainRemotePlaylist: RemotePlaylistFragment;
lateinit var _fragWatchlist: WatchLaterFragment;
lateinit var _fragHistory: HistoryFragment;
lateinit var _fragSourceDetail: SourceDetailFragment;
@@ -156,8 +153,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
constructor() : super() {
ApiMethods.UserAgent = "Grayjay Android (${BuildConfig.VERSION_CODE})";
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
val writer = StringWriter();
@@ -196,7 +191,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
override fun onCreate(savedInstanceState: Bundle?) {
Logger.i(TAG, "MainActivity Starting");
StateApp.instance.setGlobalContext(this, lifecycleScope);
StateApp.instance.mainAppStarting(this);
@@ -247,7 +241,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainSources = SourcesFragment.newInstance();
_fragMainPlaylists = PlaylistsFragment.newInstance();
_fragMainPlaylist = PlaylistFragment.newInstance();
_fragMainRemotePlaylist = RemotePlaylistFragment.newInstance();
_fragPostDetail = PostDetailFragment.newInstance();
_fragWatchlist = WatchLaterFragment.newInstance();
_fragHistory = HistoryFragment.newInstance();
@@ -333,7 +326,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragMainSources.topBar = _fragTopBarAdd;
_fragMainPlaylists.topBar = _fragTopBarGeneral;
_fragMainPlaylist.topBar = _fragTopBarNavigation;
_fragMainRemotePlaylist.topBar = _fragTopBarNavigation;
_fragPostDetail.topBar = _fragTopBarNavigation;
_fragWatchlist.topBar = _fragTopBarNavigation;
_fragHistory.topBar = _fragTopBarNavigation;
@@ -611,7 +603,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
UIDialogs.showSingleButtonDialog(
this,
R.drawable.ic_play,
getString(R.string.unknown_content_format) + " [${url}]\n[${intent.type}]",
getString(R.string.unknown_content_format) + " [${url}]",
"Ok",
{ });
}
@@ -701,22 +693,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if(!recon.trim().startsWith("["))
return handleUnknownJson(recon);
var reconLines = Json.decodeFromString<List<String>>(recon);
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
var cache: ImportCache? = null;
try {
if(cacheStr != null)
cache = Json.decodeFromString(cacheStr);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to deserialize cache");
}
val reconLines = Json.decodeFromString<List<String>>(recon);
recon = reconLines.joinToString("\n");
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
handleReconstruction(recon, cache);
handleReconstruction(recon);
return true;
}
else if(file.lowercase().endsWith(".zip") || mime == "application/zip") {
@@ -731,25 +711,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
fun handleFile(file: String): Boolean {
Logger.i(TAG, "handleFile(url=$file)");
if(file.lowercase().endsWith(".json")) {
var recon = String(readSharedFile(file));
val recon = String(readSharedFile(file));
if(!recon.startsWith("["))
return handleUnknownJson(recon);
var reconLines = Json.decodeFromString<List<String>>(recon);
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
var cache: ImportCache? = null;
try {
if(cacheStr != null)
cache = Json.decodeFromString(cacheStr);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to deserialize cache");
}
recon = reconLines.joinToString("\n");
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
handleReconstruction(recon, cache);
handleReconstruction(recon);
return true;
}
else if(file.lowercase().endsWith(".zip")) {
@@ -761,7 +728,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
return false;
}
fun handleReconstruction(recon: String, cache: ImportCache? = null) {
fun handleReconstruction(recon: String) {
val type = ManagedStore.getReconstructionIdentifier(recon);
val store: ManagedStore<*> = when(type) {
"Playlist" -> StatePlaylists.instance.playlistStore
@@ -778,7 +745,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if(!type.isNullOrEmpty()) {
UIDialogs.showImportDialog(this, store, name, listOf(recon), cache) {
UIDialogs.showImportDialog(this, store, name, listOf(recon)) {
}
}
@@ -1047,7 +1014,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
SourcesFragment::class -> _fragMainSources as T;
PlaylistsFragment::class -> _fragMainPlaylists as T;
PlaylistFragment::class -> _fragMainPlaylist as T;
RemotePlaylistFragment::class -> _fragMainRemotePlaylist as T;
PostDetailFragment::class -> _fragPostDetail as T;
WatchLaterFragment::class -> _fragWatchlist as T;
HistoryFragment::class -> _fragHistory as T;
@@ -6,7 +6,6 @@ import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.util.TypedValue
import android.view.View
@@ -20,12 +19,7 @@ import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.SignedEvent
import com.futo.polycentric.core.StorageTypeCRDTItem
import com.futo.polycentric.core.StorageTypeCRDTSetItem
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.toBase64Url
import com.futo.polycentric.core.*
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitMatrix
@@ -70,8 +64,11 @@ class PolycentricBackupActivity : AppCompatActivity() {
}
_buttonShare.onClick.subscribe {
val shareIntent = Intent(Intent.ACTION_VIEW, Uri.parse(_exportBundle))
startActivity(Intent.createChooser(shareIntent, "Share ID"));
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain";
putExtra(Intent.EXTRA_TEXT, _exportBundle);
}
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_text)));
};
_buttonCopy.onClick.subscribe {
@@ -12,8 +12,6 @@ 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
import com.futo.platformplayer.states.StatePolycentric
@@ -72,13 +70,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
processHandle = ProcessHandle.create();
Store.instance.addProcessSecret(processHandle.processSecret);
try {
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
processHandle.addServer(PolycentricCache.SERVER);
processHandle.addServer("https://srv1-stg.polycentric.io");
processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle);
} catch (e: Throwable) {
@@ -13,7 +13,6 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
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
import com.futo.platformplayer.states.StatePolycentric
@@ -127,12 +126,6 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
val processSecret = ProcessSecret(keyPair, Process.random());
Store.instance.addProcessSecret(processSecret);
try {
PolycentricStorage.instance.addProcessSecret(processSecret)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
val processHandle = processSecret.toProcessHandle();
for (e in exportBundle.events.eventsList) {
@@ -1,7 +1,5 @@
package com.futo.platformplayer.api.http
import androidx.collection.arrayMapOf
import com.futo.platformplayer.SettingsDev
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.ensureNotMainThread
import com.futo.platformplayer.logging.Logger
@@ -15,11 +13,6 @@ import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
import kotlin.system.measureTimeMillis
open class ManagedHttpClient {
@@ -32,29 +25,8 @@ open class ManagedHttpClient {
var user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
private val trustAllCerts = arrayOf<TrustManager>(
object: X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) { }
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) { }
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf();
}
}
);
private fun trustAllCertificates(builder: OkHttpClient.Builder) {
val sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, SecureRandom());
builder.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager);
builder.hostnameVerifier { a, b ->
return@hostnameVerifier true;
}
Logger.w(TAG, "Creating INSECURE client (TrustAll)");
}
constructor(builder: OkHttpClient.Builder = OkHttpClient.Builder()) {
_builderTemplate = builder;
if(SettingsDev.instance.developerMode && SettingsDev.instance.networking.allowAllCertificates)
trustAllCertificates(builder);
client = builder.addNetworkInterceptor { chain ->
val request = beforeRequest(chain.request());
val response = afterRequest(chain.proceed(request));
@@ -0,0 +1,103 @@
package com.futo.platformplayer.api.media
import androidx.collection.LruCache
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.models.ImageVariable
/**
* A temporary class that caches video results
* In future this should be part of a bigger system
*/
class CachedPlatformClient : IPlatformClient {
private val _client : IPlatformClient;
override val id: String get() = _client.id;
override val name: String get() = _client.name;
override val icon: ImageVariable? get() = _client.icon;
private val _cache: LruCache<String, IPlatformContentDetails>;
override val capabilities: PlatformClientCapabilities
get() = _client.capabilities;
constructor(client : IPlatformClient, cacheSize : Int = 10 * 1024 * 1024) {
this._client = client;
this._cache = LruCache<String, IPlatformContentDetails>(cacheSize);
}
override fun initialize() { _client.initialize() }
override fun disable() { _client.disable() }
override fun isContentDetailsUrl(url: String): Boolean = _client.isContentDetailsUrl(url);
override fun getContentDetails(url: String): IPlatformContentDetails {
var result = _cache.get(url);
if(result == null) {
result = _client.getContentDetails(url);
_cache.put(url, result);
}
return result;
}
override fun getContentChapters(url: String): List<IChapter> = _client.getContentChapters(url);
override fun getPlaybackTracker(url: String): IPlaybackTracker? = _client.getPlaybackTracker(url);
override fun isChannelUrl(url: String): Boolean = _client.isChannelUrl(url);
override fun getChannel(channelUrl: String): IPlatformChannel = _client.getChannel(channelUrl);
override fun getChannelCapabilities(): ResultCapabilities = _client.getChannelCapabilities();
override fun getChannelContents(
channelUrl: String,
type: String?,
order: String?,
filters: Map<String, List<String>>?
): IPager<IPlatformContent> = _client.getChannelContents(channelUrl);
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String? = _client.getChannelUrlByClaim(claimType, claimValues)
override fun searchSuggestions(query: String): Array<String> = _client.searchSuggestions(query);
override fun getSearchCapabilities(): ResultCapabilities = _client.getSearchCapabilities();
override fun search(
query: String,
type: String?,
order: String?,
filters: Map<String, List<String>>?
): IPager<IPlatformContent> = _client.search(query, type, order, filters);
override fun getSearchChannelContentsCapabilities(): ResultCapabilities = _client.getSearchChannelContentsCapabilities();
override fun searchChannelContents(
channelUrl: String,
query: String,
type: String?,
order: String?,
filters: Map<String, List<String>>?
): IPager<IPlatformContent> = _client.searchChannelContents(channelUrl, query, type, order, filters);
override fun searchChannels(query: String) = _client.searchChannels(query);
override fun getComments(url: String): IPager<IPlatformComment> = _client.getComments(url);
override fun getSubComments(comment: IPlatformComment): IPager<IPlatformComment> = _client.getSubComments(comment);
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = _client.getLiveChatWindow(url);
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = _client.getLiveEvents(url);
override fun getHome(): IPager<IPlatformContent> = _client.getHome();
override fun getUserSubscriptions(): Array<String> { return arrayOf(); };
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = _client.searchPlaylists(query, type, order, filters);
override fun isPlaylistUrl(url: String): Boolean = _client.isPlaylistUrl(url);
override fun getPlaylist(url: String): IPlatformPlaylistDetails = _client.getPlaylist(url);
override fun getUserPlaylists(): Array<String> { return arrayOf(); };
override fun isClaimTypeSupported(claimType: Int): Boolean {
return _client.isClaimTypeSupported(claimType);
}
}
@@ -10,7 +10,6 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.models.ImageVariable
@@ -85,20 +84,6 @@ 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 all playlists of a channel
*/
fun getChannelPlaylists(channelUrl: String): IPager<IPlatformPlaylist>
/**
* Gets the channel url associated with a claimType
*/
@@ -121,11 +106,6 @@ interface IPlatformClient {
*/
fun getPlaybackTracker(url: String): IPlaybackTracker?;
/**
* Get content recommendations
*/
fun getContentRecommendations(url: String): IPager<IPlatformContent>?;
//Comments
/**
@@ -13,14 +13,10 @@ 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 hasPeekChannelContents: Boolean = false,
val hasGetChannelPlaylists: Boolean = false,
val hasGetContentRecommendations: Boolean = false
val hasGetContentChapters: Boolean = false
) {
}
@@ -14,7 +14,7 @@ open class PlatformAuthorLink {
val id: PlatformID;
val name: String;
val url: String;
var thumbnail: String?;
val thumbnail: String?;
var subscribers: Long? = null; //Optional
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null)
@@ -3,8 +3,6 @@ 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
@@ -33,7 +31,7 @@ class Thumbnails {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
.toArray()
.map { Thumbnail.fromV8(config, it as V8ValueObject) }
.map { Thumbnail.fromV8(it as V8ValueObject) }
.toTypedArray());
}
}
@@ -42,10 +40,10 @@ class Thumbnails {
data class Thumbnail(val url : String?, val quality : Int = 0) {
companion object {
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnail {
fun fromV8(value: V8ValueObject): Thumbnail {
return Thumbnail(
value.getOrDefault<String>(config,"url", "Thumbnail", null),
value.getOrDefault(config, "quality", "Thumbnail", 0) ?: 0);
value.getString("url"),
value.getInteger("quality"));
}
}
};
@@ -37,10 +37,6 @@ class SerializedChannel(
TODO("Not yet implemented")
}
fun isSameUrl(url: String): Boolean {
return this.url == url || urlAlternatives.contains(url);
}
companion object {
fun fromChannel(channel: IPlatformChannel): SerializedChannel {
return SerializedChannel(
@@ -10,6 +10,4 @@ interface IPlatformContentDetails : IPlatformContent {
fun getComments(client: IPlatformClient): IPager<IPlatformComment>?;
fun getPlaybackTracker(): IPlaybackTracker?;
fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>?;
}
@@ -3,5 +3,4 @@ package com.futo.platformplayer.api.media.models.live
interface ILiveChatWindowDescriptor {
val url: String;
val removeElements: List<String>;
val removeElementsInterval: List<String>;
}
@@ -1,14 +1,14 @@
package com.futo.platformplayer.api.media.models.modifier
class AdhocRequestModifier: IRequestModifier {
val _handler: (String, Map<String,String>)->IRequest;
val _handler: (String, Map<String,String>, String, String?)->IRequest;
override var allowByteSkip: Boolean = false;
constructor(modifyReq: (String, Map<String,String>)->IRequest) {
constructor(modifyReq: (String, Map<String,String>, String, String?)->IRequest) {
_handler = modifyReq;
}
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
return _handler(url, headers);
override fun modifyRequest(url: String, headers: Map<String, String>, method: String, body: String?): IRequest {
return _handler(url, headers, method, body);
}
}
@@ -1,6 +1,8 @@
package com.futo.platformplayer.api.media.models.modifier
interface IRequest {
val method: String?;
val url: String?;
val headers: Map<String, String>;
val headers: Map<String, String>?;
val body: String?;
}
@@ -3,5 +3,5 @@ package com.futo.platformplayer.api.media.models.modifier
interface IRequestModifier {
var allowByteSkip: Boolean;
fun modifyRequest(url: String, headers: Map<String, String>): IRequest
fun modifyRequest(url: String, headers: Map<String, String>, method: String, body: String?): IRequest
}
@@ -7,5 +7,4 @@ interface IPlaybackTracker {
fun onInit(seconds: Double);
fun onProgress(seconds: Double, isPlaying: Boolean);
fun onConcluded();
}
@@ -8,5 +8,5 @@ interface IPlatformPlaylistDetails: IPlatformPlaylist {
//TODO: Determine if this should be IPlatformContent (probably not?)
val contents: IPager<IPlatformVideo>;
fun toPlaylist(onProgress: ((progress: Int) -> Unit)? = null): Playlist;
fun toPlaylist(): Playlist;
}
@@ -1,6 +0,0 @@
package com.futo.platformplayer.api.media.models.streams.sources
interface IAudioUrlWidevineSource : IAudioUrlSource {
val bearerToken: String
val licenseUri: String
}
@@ -7,7 +7,6 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.streams.sources.*
@@ -57,7 +56,6 @@ open class SerializedPlatformVideoDetails(
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
companion object {
fun fromVideo(video : IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) : SerializedPlatformVideoDetails {
@@ -8,8 +8,6 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.util.UUID
class DevJSClient : JSClient {
@@ -117,7 +115,7 @@ class DevJSClient : JSClient {
//Video
override fun isContentDetailsUrl(url: String): Boolean {
return StateDeveloper.instance.handleDevCall(devID, "isVideoDetailsUrl(${Json.encodeToString(url)})"){
return StateDeveloper.instance.handleDevCall(devID, "isVideoDetailsUrl"){
super.isContentDetailsUrl(url);
};
}
@@ -20,7 +20,6 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.platforms.js.internal.JSCallDocs
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
@@ -28,7 +27,6 @@ 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
@@ -40,7 +38,6 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSLiveChatWindowDes
import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaybackTracker
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistDetails
import com.futo.platformplayer.api.media.platforms.js.models.JSPlaylistPager
import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1
@@ -48,7 +45,6 @@ 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
@@ -60,10 +56,8 @@ 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;
@@ -79,7 +73,6 @@ 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;
@@ -98,11 +91,7 @@ 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;
@@ -161,8 +150,6 @@ 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;
@@ -186,8 +173,6 @@ open class JSClient : IPlatformClient {
if(it is ScriptCaptchaRequiredException)
onCaptchaException.emit(this, it);
};
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
}
open fun getCopy(): JSClient {
@@ -229,12 +214,9 @@ 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,
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false
);
try {
@@ -278,7 +260,7 @@ open class JSClient : IPlatformClient {
}
@JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform")
override fun getHome(): IPager<IPlatformContent> = isBusyWith("getHome") {
override fun getHome(): IPager<IPlatformContent> = isBusyWith {
ensureEnabled();
return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.getHome()"));
@@ -286,7 +268,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("searchSuggestions") {
override fun searchSuggestions(query: String): Array<String> = isBusyWith {
ensureEnabled();
return@isBusyWith plugin.executeTyped<V8ValueArray>("source.searchSuggestions(${Json.encodeToString(query)})")
.toArray()
@@ -316,7 +298,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("search") {
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
ensureEnabled();
return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
@@ -324,9 +306,6 @@ 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!!;
@@ -340,7 +319,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("searchChannelContents") {
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
ensureEnabled();
if(!capabilities.hasSearchChannelContents)
throw IllegalStateException("This plugin does not support channel search");
@@ -352,7 +331,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("searchChannels") {
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith {
ensureEnabled();
return@isBusyWith JSChannelPager(config, this,
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
@@ -372,7 +351,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("getChannel") {
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith {
ensureEnabled();
return@isBusyWith JSChannel(config,
plugin.executeTyped("source.getChannel(${Json.encodeToString(channelUrl)})"));
@@ -399,57 +378,12 @@ 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("getChannelContents") {
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
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.getChannelPlaylists(url)", "Gets playlists of a channel")
@JSDocsParameter("channelUrl", "A channel url (this platform)")
override fun getChannelPlaylists(channelUrl: String): IPager<IPlatformPlaylist> = isBusyWith("getChannelPlaylists") {
ensureEnabled();
if(!capabilities.hasGetChannelPlaylists)
return@isBusyWith EmptyPager();
return@isBusyWith JSPlaylistPager(config, this,
plugin.executeTyped("source.getChannelPlaylists(${Json.encodeToString(channelUrl)})"));
}
@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")
@@ -510,7 +444,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("getContentDetails") {
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith {
ensureEnabled();
return@isBusyWith IJSContentDetails.fromV8(this,
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
@@ -519,7 +453,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("getContentChapters") {
override fun getContentChapters(url: String): List<IChapter> = isBusyWith {
if(!capabilities.hasGetContentChapters)
return@isBusyWith listOf();
ensureEnabled();
@@ -530,7 +464,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("getPlaybackTracker") {
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith {
if(!capabilities.hasGetPlaybackTracker)
return@isBusyWith null;
ensureEnabled();
@@ -544,7 +478,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("getComments") {
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith {
ensureEnabled();
val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})");
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
@@ -560,45 +494,31 @@ open class JSClient : IPlatformClient {
plugin.executeTyped("source.getSubComments(${Json.encodeToString(comment as JSComment)})"));
}
@JSDocs(18, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
@JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
@JSDocsParameter("url", "Url of live stream")
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith("getLiveChatWindow") {
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith {
if(!capabilities.hasGetLiveChatWindow)
return@isBusyWith null;
ensureEnabled();
return@isBusyWith JSLiveChatWindowDescriptor(config,
plugin.executeTyped("source.getLiveChatWindow(${Json.encodeToString(url)})"));
}
@JSDocs(19, "source.getLiveEvents(url)", "Gets live events for a livestream")
@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("getLiveEvents") {
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith {
if(!capabilities.hasGetLiveEvents)
return@isBusyWith null;
ensureEnabled();
return@isBusyWith JSLiveEventPager(config, this,
plugin.executeTyped("source.getLiveEvents(${Json.encodeToString(url)})"));
}
@JSDocs(19, "source.getContentRecommendations(url)", "Gets recommendations of a content page")
@JSDocsParameter("url", "Url of content")
override fun getContentRecommendations(url: String): IPager<IPlatformContent>? = isBusyWith("getContentRecommendations") {
if(!capabilities.hasGetContentRecommendations)
return@isBusyWith null;
ensureEnabled();
return@isBusyWith JSContentPager(config, this,
plugin.executeTyped("source.getContentRecommendations(${Json.encodeToString(url)})"));
}
@JSDocs(19, "source.searchPlaylists(query)", "Searches for playlists on the platform")
@JSDocsParameter("query", "Query that search results should match")
@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")
@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("searchPlaylists") {
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
ensureEnabled();
if(!capabilities.hasSearchPlaylists)
throw IllegalStateException("This plugin does not support playlist search");
@@ -608,22 +528,15 @@ 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;
try {
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
.value;
}
catch(ex: Throwable) {
announcePluginUnhandledException("isPlaylistUrl", ex);
return false;
}
return plugin.executeBoolean("source.isPlaylistUrl(${Json.encodeToString(url)})") ?: 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("getPlaylist") {
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith {
ensureEnabled();
return@isBusyWith JSPlaylistDetails(this, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
}
@@ -720,24 +633,19 @@ open class JSClient : IPlatformClient {
}
private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
private fun <T> isBusyWith(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)
@@ -754,43 +662,10 @@ 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>();
@@ -803,12 +678,5 @@ 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;
}
}
}
@@ -45,9 +45,7 @@ class SourcePluginConfig(
var enableInSearch: Boolean = true,
var enableInHome: Boolean = true,
var supportedClaimTypes: List<Int> = listOf(),
var primaryClaimFieldType: Int? = null,
var developerSubmitUrl: String? = null,
var allowAllHttpHeaderAccess: Boolean = false,
var primaryClaimFieldType: Int? = null
) : IV8PluginConfig {
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
@@ -81,44 +79,6 @@ 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>>();
@@ -147,11 +107,6 @@ 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;
}
@@ -8,7 +8,6 @@ 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
@@ -91,10 +90,8 @@ 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();
@@ -133,11 +130,6 @@ 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, val docsUrl: String? = null);
data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false);
@kotlinx.serialization.Serializable
data class JSParameterDocs(val name: String, val description: String);
@@ -2,22 +2,14 @@ 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 {
@@ -36,15 +28,7 @@ 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(
//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())
) {
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super() {
_jsClient = jsClient;
_jsConfig = config;
_auth = auth;
@@ -217,16 +201,6 @@ 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;
}
@@ -7,7 +7,6 @@ import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrDefaultList
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.getOrThrowNullable
@@ -38,7 +37,7 @@ class JSChannel : IPlatformChannel {
description = _channel.getOrThrowNullable(config, "description", contextName);
url = _channel.getOrThrow(config, "url", contextName);
urlAlternatives = _channel.getOrDefaultList(config, "urlAlternatives", contextName, listOf()) ?: listOf();
links = HashMap(_channel.getOrDefault<Map<String, String>>(config, "links", contextName, mapOf()) ?: mapOf());
links = HashMap();
}
override fun getContents(client: IPlatformClient): IPager<IPlatformContent> {
@@ -14,13 +14,12 @@ import java.time.ZoneOffset
class JSLiveChatWindowDescriptor: ILiveChatWindowDescriptor {
override val url: String;
override val removeElements: List<String>;
override val removeElementsInterval: List<String>;
constructor(config: SourcePluginConfig, obj: V8ValueObject) {
val contextName = "LiveChatWindowDescriptor";
url = obj.getOrThrow(config, "url", contextName);
removeElements = obj.getOrDefault(config, "removeElements", contextName, listOf()) ?: listOf();
removeElementsInterval = obj.getOrDefault(config, "removeElementsInterval", contextName, listOf()) ?: listOf();
}
}
@@ -17,8 +17,6 @@ class JSPlaybackTracker: IPlaybackTracker {
private var _lastRequest: Long = Long.MIN_VALUE;
private val _hasOnConcluded: Boolean;
override var nextRequest: Int = 1000
private set;
@@ -28,7 +26,6 @@ class JSPlaybackTracker: IPlaybackTracker {
throw ScriptImplementationException(config, "Missing onProgress on PlaybackTracker");
if(!obj.has("nextRequest"))
throw ScriptImplementationException(config, "Missing nextRequest on PlaybackTracker");
_hasOnConcluded = obj.has("onConcluded");
this._config = config;
this._obj = obj;
@@ -62,16 +59,6 @@ class JSPlaybackTracker: IPlaybackTracker {
}
}
}
override fun onConcluded() {
warnIfMainThread("JSPlaybackTracker.onConcluded");
if(_hasOnConcluded) {
synchronized(_obj) {
Logger.i("JSPlaybackTracker", "onConcluded");
_obj.invokeVoid("onConcluded", -1);
}
}
}
override fun shouldUpdate(): Boolean = (_lastRequest < 0 || (System.currentTimeMillis() - _lastRequest) > nextRequest);
}
@@ -14,6 +14,6 @@ open class JSPlaylist : JSContent, IPlatformPlaylist {
constructor(config: SourcePluginConfig, obj: V8ValueObject) : super(config, obj) {
val contextName = "Playlist";
thumbnail = obj.getOrDefault(config, "thumbnail", contextName, null);
videoCount = obj.getOrDefault(config, "videoCount", contextName, -1)!!;
videoCount = obj.getOrDefault(config, "videoCount", contextName, 0)!!;
}
}
@@ -7,7 +7,7 @@ import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.ReusablePager
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.models.Playlist
@@ -15,26 +15,22 @@ class JSPlaylistDetails: JSPlaylist, IPlatformPlaylistDetails {
override val contents: IPager<IPlatformVideo>;
constructor(plugin: JSClient, config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
contents = ReusablePager(JSVideoPager(config, plugin, obj.getOrThrow(config, "contents", "PlaylistDetails")));
contents = JSVideoPager(config, plugin, obj.getOrThrow(config, "contents", "PlaylistDetails"));
}
override fun toPlaylist(onProgress: ((progress: Int) -> Unit)?): Playlist {
val playlist = if (contents is ReusablePager) contents.getWindow() else contents;
val videos = playlist.getResults().toMutableList();
onProgress?.invoke(videos.size);
override fun toPlaylist(): Playlist {
val videos = contents.getResults().toMutableList();
//Download all pages
var allowedEmptyCount = 2;
while(playlist.hasMorePages()) {
playlist.nextPage();
if(!videos.addAll(playlist.getResults())) {
while(contents.hasMorePages()) {
contents.nextPage();
if(!videos.addAll(contents.getResults())) {
allowedEmptyCount--;
if(allowedEmptyCount <= 0)
break;
}
else allowedEmptyCount = 2;
onProgress?.invoke(videos.size);
}
return Playlist(id.toString(), name, videos.map { SerializedPlatformVideo.fromVideo(it)});
@@ -3,7 +3,6 @@ package com.futo.platformplayer.api.media.platforms.js.models
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.post.IPlatformPost
import com.futo.platformplayer.api.media.models.post.IPlatformPostDetails
@@ -19,7 +18,6 @@ import com.futo.platformplayer.states.StateDeveloper
class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean;
override val rating: IRating;
@@ -36,7 +34,6 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
content = obj.getOrDefault(config, "content", contextName, "") ?: "";
_hasGetComments = _content.has("getComments");
_hasGetContentRecommendations = _content.has("getContentRecommendations");
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
@@ -54,27 +51,9 @@ class JSPostDetails : JSPost, IPlatformPost, IPlatformPostDetails {
}
override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
if(!_hasGetContentRecommendations || _content.isClosed)
return null;
if(client is DevJSClient)
return StateDeveloper.instance.handleDevCall(client.devID, "postDetail.getContentRecommendations()") {
return@handleDevCall getContentRecommendationsJS(client);
}
else if(client is JSClient)
return getContentRecommendationsJS(client);
return null;
}
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager);
}
private fun getCommentsJS(client: JSClient): JSCommentPager {
val commentPager = _content.invoke<V8ValueObject>("getComments", arrayOf<Any>());
return JSCommentPager(_pluginConfig, client, commentPager);
}
}
@@ -12,31 +12,40 @@ import com.futo.platformplayer.getOrDefault
class JSRequest : IRequest {
private val _v8Url: String?;
private val _v8Headers: Map<String, String>?;
private val _v8Method: String?;
private val _v8Body: String?;
private val _v8Options: Options?;
override var url: String? = null;
override lateinit var headers: Map<String, String>;
override var method: String? = null;
override var body: String? = null;
constructor(plugin: JSClient, url: String?, headers: Map<String, String>?, options: Options?, originalUrl: String?, originalHeaders: Map<String, String>?) {
constructor(plugin: JSClient, url: String?, headers: Map<String, String>?, method: String?, body: String?, options: Options?, originalUrl: String?, originalHeaders: Map<String, String>?, originalMethod: String?, originalBody: String?) {
_v8Url = url;
_v8Headers = headers;
_v8Method = method;
_v8Body = body;
_v8Options = options;
initialize(plugin, originalUrl, originalHeaders);
initialize(plugin, originalUrl, originalHeaders, originalMethod, originalBody);
}
constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?, applyOtherHeadersByDefault: Boolean = false) {
constructor(plugin: JSClient, obj: V8ValueObject, originalUrl: String?, originalHeaders: Map<String, String>?, originalMethod: String?, originalBody: String?, applyOtherHeadersByDefault: Boolean = false) {
val contextName = "ModifyRequestResponse";
val config = plugin.config;
_v8Url = obj.getOrDefault<String>(config, "url", contextName, null);
_v8Headers = obj.getOrDefault<Map<String, String>>(config, "headers", contextName, null);
_v8Method = obj.getOrDefault<String>(config, "method", contextName, null);
_v8Body = obj.getOrDefault<String>(config, "body", contextName, null);
_v8Options = obj.getOrDefault<V8ValueObject>(config, "options", "JSRequestModifier.options", null)?.let {
Options(config, it, applyOtherHeadersByDefault);
} ?: Options(null, null, applyOtherHeadersByDefault);
initialize(plugin, originalUrl, originalHeaders);
initialize(plugin, originalUrl, originalHeaders, originalMethod, originalBody);
}
private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?) {
val config = plugin.config;
private fun initialize(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?, originalMethod: String?, originalBody: String?) {
url = _v8Url ?: originalUrl;
method = _v8Method ?: originalMethod;
body = _v8Body ?: originalBody;
if(_v8Options?.applyOtherHeaders ?: false) {
val headersToSet = _v8Headers?.toMutableMap() ?: mutableMapOf();
@@ -69,8 +78,8 @@ class JSRequest : IRequest {
}
}
fun modify(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?): JSRequest {
return JSRequest(plugin, _v8Url, _v8Headers, _v8Options, originalUrl, originalHeaders);
fun modify(plugin: JSClient, originalUrl: String?, originalHeaders: Map<String, String>?, originalMethod: String?, originalBody: String?): JSRequest {
return JSRequest(plugin, _v8Url, _v8Headers, _v8Method, _v8Body, _v8Options, originalUrl, originalHeaders, originalMethod, originalBody);
}
@@ -1,6 +1,5 @@
package com.futo.platformplayer.api.media.platforms.js.models
import android.net.Uri
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.modifier.IRequest
import com.futo.platformplayer.api.media.models.modifier.IRequestModifier
@@ -8,9 +7,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
class JSRequestModifier: IRequestModifier {
private val _plugin: JSClient;
@@ -30,20 +27,20 @@ class JSRequestModifier: IRequestModifier {
throw ScriptImplementationException(config, "RequestModifier is missing modifyRequest", null);
}
override fun modifyRequest(url: String, headers: Map<String, String>): IRequest {
override fun modifyRequest(url: String, headers: Map<String, String>, method: String, body: String?): IRequest {
if (_modifier.isClosed) {
return Request(url, headers);
return Request(url, headers, method, body);
}
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSRequestModifier", "builder.modifyRequest()") {
_modifier.invoke("modifyRequest", url, headers);
_modifier.invoke("modifyRequest", url, headers, method, body);
} as V8ValueObject;
val req = JSRequest(_plugin, result, url, headers);
val req = JSRequest(_plugin, result, url, headers, method, body);
result.close();
return req;
}
data class Request(override val url: String, override val headers: Map<String, String>) : IRequest;
data class Request(override val url: String, override val headers: Map<String, String>, override val method: String, override val body: String?) : IRequest;
}
@@ -6,7 +6,6 @@ import com.caoccao.javet.values.reference.V8ValueArray
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
@@ -28,7 +27,6 @@ import com.futo.platformplayer.states.StateDeveloper
class JSVideoDetails : JSVideo, IPlatformVideoDetails {
private val _hasGetComments: Boolean;
private val _hasGetContentRecommendations: Boolean;
private val _hasGetPlaybackTracker: Boolean;
//Details
@@ -68,7 +66,6 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
_hasGetComments = _content.has("getComments");
_hasGetPlaybackTracker = _content.has("getPlaybackTracker");
_hasGetContentRecommendations = _content.has("getContentRecommendations");
}
override fun getPlaybackTracker(): IPlaybackTracker? {
@@ -92,24 +89,6 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails {
};
}
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
if(!_hasGetContentRecommendations || _content.isClosed)
return null;
if(client is DevJSClient)
return StateDeveloper.instance.handleDevCall(client.devID, "videoDetail.getContentRecommendations()") {
return@handleDevCall getContentRecommendationsJS(client);
}
else if(client is JSClient)
return getContentRecommendationsJS(client);
return null;
}
private fun getContentRecommendationsJS(client: JSClient): JSContentPager {
val contentPager = _content.invoke<V8ValueObject>("getContentRecommendations", arrayOf<Any>());
return JSContentPager(_pluginConfig, client, contentPager);
}
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
if(client !is JSClient || !_hasGetComments || _content.isClosed)
return null;
@@ -1,24 +0,0 @@
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)"
}
}
@@ -1,7 +1,5 @@
package com.futo.platformplayer.api.media.platforms.js.models.sources
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource
import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.modifier.AdhocRequestModifier
@@ -15,7 +13,6 @@ import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.orNull
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
abstract class JSSource {
protected val _plugin: JSClient;
@@ -33,15 +30,15 @@ abstract class JSSource {
this.type = type;
_requestModifier = obj.getOrDefault<V8ValueObject>(_config, "requestModifier", "JSSource.requestModifier", null)?.let {
JSRequest(plugin, it, null, null, true);
JSRequest(plugin, it, null, null, null, null, true);
}
hasRequestModifier = _requestModifier != null || obj.has("getRequestModifier");
}
fun getRequestModifier(): IRequestModifier? {
if(_requestModifier != null)
return AdhocRequestModifier { url, headers ->
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers);
return AdhocRequestModifier { url, headers, method, body ->
return@AdhocRequestModifier _requestModifier.modify(_plugin, url, headers, method, body);
};
if (!hasRequestModifier || _obj.isClosed) {
@@ -66,7 +63,6 @@ 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 {
@@ -89,7 +85,6 @@ 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,25 +163,24 @@ 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,9 +44,7 @@ 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;
@@ -385,44 +383,39 @@ class ChromecastCastingDevice : CastingDevice {
getStatus();
val buffer = ByteArray(409600);
val buffer = ByteArray(4096);
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;
}
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
}
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
inputStream.read(buffer, 0, size);
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
inputStream.read(buffer, 0, size);
//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");
}
//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);
}
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);
@@ -595,16 +588,13 @@ class ChromecastCastingDevice : CastingDevice {
return;
}
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);
}
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,7 +242,6 @@ 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,22 +104,15 @@ 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) {
@@ -444,25 +437,6 @@ 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) {
@@ -474,15 +448,6 @@ 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 {
@@ -22,9 +22,7 @@ import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
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.stores.v2.ManagedStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -68,15 +66,13 @@ class ImportDialog : AlertDialog {
private val _name: String;
private val _toImport: List<String>;
private val _cache: ImportCache?;
constructor(context: Context, importStore: ManagedStore<*>, name: String, toReconstruct: List<String>, cache: ImportCache?, onConcluded: ()->Unit): super(context) {
constructor(context: Context, importStore: ManagedStore<*>, name: String, toReconstruct: List<String>, onConcluded: ()->Unit): super(context) {
_context = context;
_store = importStore;
_onConcluded = onConcluded;
_name = name;
_toImport = ArrayList(toReconstruct);
_cache = cache;
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -150,7 +146,7 @@ class ImportDialog : AlertDialog {
val scope = StateApp.instance.scopeOrNull;
scope?.launch(Dispatchers.IO) {
try {
val migrationResult = _store.importReconstructions(_toImport, _cache) { finished, total ->
val migrationResult = _store.importReconstructions(_toImport) { finished, total ->
scope.launch(Dispatchers.Main) {
_textProgress.text = "${finished}/${total}";
}
@@ -1,253 +0,0 @@
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,7 +755,6 @@ 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,7 +7,6 @@ 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
@@ -64,7 +63,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 = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
val outputFileName = toSafeFileName(videoLocal.name) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
val f = downloadRoot.createFile("video/mp4", outputFileName)
?: throw Exception("Failed to create file in external directory.");
@@ -80,7 +79,7 @@ class VideoExport {
}
outputFile = f;
} else if (v != null) {
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.videoContainerToExtension(v.container);
val outputFileName = toSafeFileName(videoLocal.name) + "." + VideoDownload.videoContainerToExtension(v.container);
val f = downloadRoot.createFile(v.container, outputFileName)
?: throw Exception("Failed to create file in external directory.");
@@ -92,7 +91,7 @@ class VideoExport {
outputFile = f;
} else if (a != null) {
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container);
val outputFileName = toSafeFileName(videoLocal.name) + "." + VideoDownload.audioContainerToExtension(a.container);
val f = downloadRoot.createFile(a.container, outputFileName)
?: throw Exception("Failed to create file in external directory.");
@@ -111,6 +110,11 @@ 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
@@ -6,7 +6,6 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
@@ -82,8 +81,6 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
fun toPlatformVideo() : IPlatformVideoDetails {
throw NotImplementedError();
@@ -11,6 +11,7 @@ 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
@@ -44,6 +45,7 @@ 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;
@@ -69,11 +71,6 @@ 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.
@@ -95,10 +92,6 @@ 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));
@@ -1,13 +1,10 @@
package com.futo.platformplayer.engine.packages
import com.caoccao.javet.annotations.V8Function
import com.caoccao.javet.annotations.V8Property
import com.futo.platformplayer.BuildConfig
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
@@ -15,9 +12,6 @@ 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
@@ -27,7 +21,6 @@ class PackageBridge : V8Package {
@Transient
private val _clientAuth: ManagedHttpClient
override val name: String get() = "Bridge";
override val variableName: String get() = "bridge";
@@ -37,19 +30,6 @@ class PackageBridge : V8Package {
_clientAuth = plugin.httpClientAuth;
}
@V8Property
fun buildVersion(): Int {
//If debug build, assume max version
if(BuildConfig.VERSION_CODE == 1)
return Int.MAX_VALUE;
return BuildConfig.VERSION_CODE;
}
@V8Property
fun buildFlavor(): String {
return BuildConfig.FLAVOR;
}
@V8Function
fun toast(str: String) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
@@ -67,44 +47,6 @@ 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,7 +9,6 @@ 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
@@ -191,8 +190,6 @@ class PackageHttp: V8Package {
@Transient
private val _client: ManagedHttpClient;
val parentConfig: IV8PluginConfig get() = _package._config;
@Transient
private val _defaultHeaders = mutableMapOf<String, String>();
@Transient
@@ -211,24 +208,28 @@ class PackageHttp: V8Package {
}
@V8Function
fun setDefaultHeaders(defaultHeaders: Map<String, String>) {
fun setDefaultHeaders(defaultHeaders: Map<String, String>): PackageHttpClient {
for(pair in defaultHeaders)
_defaultHeaders[pair.key] = pair.value;
return this;
}
@V8Function
fun setDoApplyCookies(apply: Boolean) {
fun setDoApplyCookies(apply: Boolean): PackageHttpClient {
if(_client is JSHttpClient)
_client.doApplyCookies = apply;
return this;
}
@V8Function
fun setDoUpdateCookies(update: Boolean) {
fun setDoUpdateCookies(update: Boolean): PackageHttpClient {
if(_client is JSHttpClient)
_client.doUpdateCookies = update;
return this;
}
@V8Function
fun setDoAllowNewCookies(allow: Boolean) {
fun setDoAllowNewCookies(allow: Boolean): PackageHttpClient {
if(_client is JSHttpClient)
_client.doAllowNewCookies = allow;
return this;
}
@V8Function
@@ -241,8 +242,7 @@ 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,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
}
};
}
@@ -256,8 +256,7 @@ 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,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
}
};
}
@@ -272,8 +271,7 @@ 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,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
}
};
}
@@ -287,8 +285,7 @@ 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,
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
}
};
}
@@ -308,31 +305,18 @@ class PackageHttp: V8Package {
}
}
private fun sanitizeResponseHeaders(headers: Map<String, List<String>>?, onlyWhitelisted: Boolean = false): Map<String, List<String>> {
private fun sanitizeResponseHeaders(headers: Map<String, List<String>>?): Map<String, List<String>> {
val result = mutableMapOf<String, List<String>>()
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;
headers?.forEach { (header, values) ->
val lowerCaseHeader = header.lowercase()
if (WHITELISTED_RESPONSE_HEADERS.contains(lowerCaseHeader)) {
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 = )");
@@ -349,7 +333,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) {
@@ -429,7 +413,7 @@ class PackageHttp: V8Package {
val hasClosed = socketObj.has("closed");
val hasFailure = socketObj.has("failure");
socketObj.setWeak(); //We have to manage this lifecycle
//socketObj.setWeak(); //We have to manage this lifecycle
_listeners = socketObj;
_socket = _packageClient.logExceptions {
@@ -438,14 +422,8 @@ class PackageHttp: V8Package {
override fun open() {
Logger.i(TAG, "Websocket opened: " + _url);
_isOpen = true;
if(hasOpen) {
try {
_listeners?.invokeVoid("open", arrayOf<Any>());
}
catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex);
}
}
if(hasOpen)
_listeners?.invokeVoid("open", arrayOf<Any>());
}
override fun message(msg: String) {
if(hasMessage) {
@@ -457,37 +435,18 @@ class PackageHttp: V8Package {
}
override fun closing(code: Int, reason: String) {
if(hasClosing)
{
try {
_listeners?.invokeVoid("closing", code, reason);
}
catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex);
}
}
_listeners?.invokeVoid("closing", code, reason);
}
override fun closed(code: Int, reason: String) {
_isOpen = false;
if(hasClosed) {
try {
_listeners?.invokeVoid("closed", code, reason);
}
catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
}
}
if(hasClosed)
_listeners?.invokeVoid("closed", code, reason);
}
override fun failure(exception: Throwable) {
_isOpen = false;
Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception);
if(hasFailure) {
try {
_listeners?.invokeVoid("failure", exception.message);
}
catch(ex: Throwable){
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
}
}
if(hasFailure)
_listeners?.invokeVoid("failure", exception.message);
}
});
};
@@ -497,16 +456,6 @@ class PackageHttp: V8Package {
fun send(msg: String) {
_socket?.send(msg);
}
@V8Function
fun close() {
_socket?.close(1000, "");
}
@V8Function
fun close(code: Int?, reason: String?) {
_socket?.close(code ?: 1000, reason ?: "");
_listeners?.close()
}
}
data class RequestDescriptor(
@@ -4,11 +4,8 @@ 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;
@@ -22,31 +19,7 @@ class PackageUtilities : V8Package {
@V8Function
fun toBase64(arr: ByteArray): String {
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) };
return Base64.encodeToString(arr, Base64.NO_WRAP);
}
@V8Function
@@ -114,7 +114,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
}
override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
_lastPolycentricProfile = polycentricProfile;
if (polycentricProfile == null) {
@@ -60,10 +60,8 @@ 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");
@@ -105,9 +103,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(it);
_adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), it.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); };
}.exception<Throwable> {
Logger.w(TAG, "Failed to load next page.", it);
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadNextPage() });
@@ -159,7 +157,6 @@ 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);
}
@@ -309,7 +306,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
_adapterResults?.setLoading(loading);
}
override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
val p = _lastPolycentricProfile;
if (p != null && polycentricProfile != null && p.system == polycentricProfile.system) {
Logger.i(TAG, "setPolycentricProfile skipped because previous was same");
@@ -124,7 +124,7 @@ class ChannelListFragment : Fragment, IChannelTabFragment {
}
}
override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
_taskLoadChannel.cancel();
_lastPolycentricProfile = polycentricProfile;
@@ -46,7 +46,7 @@ class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
_lastChannel = channel;
}
override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
_lastPolycentricProfile = polycentricProfile
if (polycentricProfile != null) {
_supportView?.setPolycentricProfile(polycentricProfile)
@@ -1,297 +0,0 @@
package com.futo.platformplayer.fragment.channel.tab
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.platforms.js.models.JSPager
import com.futo.platformplayer.api.media.structures.IAsyncPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.IRefreshPager
import com.futo.platformplayer.api.media.structures.IReplacerPager
import com.futo.platformplayer.api.media.structures.MultiPager
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
private var _recyclerResults: RecyclerView? = null
private var _llmPlaylist: LinearLayoutManager? = null
private var _loading = false
private var _pagerParent: IPager<IPlatformPlaylist>? = null
private var _pager: IPager<IPlatformPlaylist>? = null
private var _channel: IPlatformChannel? = null
private var _results: ArrayList<IPlatformContent> = arrayListOf()
private var _adapterResults: InsertedViewAdapterWithLoader<ContentPreviewViewHolder>? = null
val onContentClicked = Event2<IPlatformContent, Long>()
val onContentUrlClicked = Event2<String, ContentType>()
val onUrlClicked = Event1<String>()
val onChannelClicked = Event1<PlatformAuthorLink>()
val onAddToClicked = Event1<IPlatformContent>()
val onAddToQueueClicked = Event1<IPlatformContent>()
val onAddToWatchLaterClicked = Event1<IPlatformContent>()
val onLongPress = Event1<IPlatformContent>()
private fun getPlaylistPager(channel: IPlatformChannel): IPager<IPlatformPlaylist> {
Logger.i(TAG, "getPlaylistPager")
return StatePlatform.instance.getChannelPlaylists(channel.url)
}
private val _taskLoadPlaylists =
TaskHandler<IPlatformChannel, IPager<IPlatformPlaylist>>({ lifecycleScope }, {
val livePager = getPlaylistPager(it)
return@TaskHandler livePager
}).success { livePager ->
setLoading(false)
setPager(livePager)
}.exception<ScriptCaptchaRequiredException> { }.exception<Throwable> {
Logger.w(TAG, "Failed to load initial playlists.", it)
UIDialogs.showGeneralRetryErrorDialog(requireContext(),
it.message ?: "",
it,
{ loadNextPage() })
}
private var _nextPageHandler: TaskHandler<IPager<IPlatformPlaylist>, List<IPlatformPlaylist>> =
TaskHandler<IPager<IPlatformPlaylist>, List<IPlatformPlaylist>>({ lifecycleScope }, {
if (it is IAsyncPager<*>) it.nextPageAsync()
else it.nextPage()
processPagerExceptions(it)
return@TaskHandler it.getResults()
}).success {
setLoading(false)
val posBefore = _results.size
_results.addAll(it)
_adapterResults?.let { adapterResult ->
adapterResult.notifyItemRangeInserted(
adapterResult.childToParentPosition(
posBefore
), it.size
)
}
}.exception<Throwable> {
Logger.w(TAG, "Failed to load next page.", it)
UIDialogs.showGeneralRetryErrorDialog(requireContext(),
it.message ?: "",
it,
{ loadNextPage() })
}
private val _scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val recyclerResults = _recyclerResults ?: return
val llmPlaylist = _llmPlaylist ?: return
val visibleItemCount = recyclerResults.childCount
val firstVisibleItem = llmPlaylist.findFirstVisibleItemPosition()
val visibleThreshold = 15
if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= _results.size) {
loadNextPage()
}
}
}
override fun setChannel(channel: IPlatformChannel) {
val c = _channel
if (c != null && c.url == channel.url) {
Logger.i(TAG, "setChannel skipped because previous was same")
return
}
Logger.i(TAG, "setChannel setChannel=${channel}")
_taskLoadPlaylists.cancel()
_channel = channel
_results.clear()
_adapterResults?.notifyDataSetChanged()
loadInitial()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_channel_videos, container, false)
_recyclerResults = view.findViewById(R.id.recycler_videos)
_adapterResults = PreviewContentListAdapter(
view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar
).apply {
this.onContentUrlClicked.subscribe(this@ChannelPlaylistsFragment.onContentUrlClicked::emit)
this.onUrlClicked.subscribe(this@ChannelPlaylistsFragment.onUrlClicked::emit)
this.onContentClicked.subscribe(this@ChannelPlaylistsFragment.onContentClicked::emit)
this.onChannelClicked.subscribe(this@ChannelPlaylistsFragment.onChannelClicked::emit)
this.onAddToClicked.subscribe(this@ChannelPlaylistsFragment.onAddToClicked::emit)
this.onAddToQueueClicked.subscribe(this@ChannelPlaylistsFragment.onAddToQueueClicked::emit)
this.onAddToWatchLaterClicked.subscribe(this@ChannelPlaylistsFragment.onAddToWatchLaterClicked::emit)
this.onLongPress.subscribe(this@ChannelPlaylistsFragment.onLongPress::emit)
}
_llmPlaylist = LinearLayoutManager(view.context)
_recyclerResults?.adapter = _adapterResults
_recyclerResults?.layoutManager = _llmPlaylist
_recyclerResults?.addOnScrollListener(_scrollListener)
return view
}
override fun onDestroyView() {
super.onDestroyView()
_recyclerResults?.removeOnScrollListener(_scrollListener)
_recyclerResults = null
_pager = null
_taskLoadPlaylists.cancel()
_nextPageHandler.cancel()
}
private fun setPager(
pager: IPager<IPlatformPlaylist>
) {
if (_pagerParent != null && _pagerParent is IRefreshPager<*>) {
(_pagerParent as IRefreshPager<*>).onPagerError.remove(this)
(_pagerParent as IRefreshPager<*>).onPagerChanged.remove(this)
_pagerParent = null
}
if (_pager is IReplacerPager<*>) (_pager as IReplacerPager<*>).onReplaced.remove(this)
val pagerToSet: IPager<IPlatformPlaylist>?
if (pager is IRefreshPager<*>) {
_pagerParent = pager
pagerToSet = pager.getCurrentPager() as IPager<IPlatformPlaylist>
pager.onPagerChanged.subscribe(this) {
lifecycleScope.launch(Dispatchers.Main) {
try {
loadPagerInternal(it as IPager<IPlatformPlaylist>)
} catch (e: Throwable) {
Logger.e(TAG, "loadPagerInternal failed.", e)
}
}
}
pager.onPagerError.subscribe(this) {
Logger.e(TAG, "Search pager failed: ${it.message}", it)
if (it is PluginException) UIDialogs.toast("Plugin [${it.config.name}] failed due to:\n${it.message}")
else UIDialogs.toast("Plugin failed due to:\n${it.message}")
}
} else pagerToSet = pager
loadPagerInternal(pagerToSet)
}
private fun loadPagerInternal(
pager: IPager<IPlatformPlaylist>
) {
if (_pager is IReplacerPager<*>) (_pager as IReplacerPager<*>).onReplaced.remove(this)
if (pager is IReplacerPager<*>) {
pager.onReplaced.subscribe(this) { oldItem, newItem ->
if (_pager != pager) return@subscribe
lifecycleScope.launch(Dispatchers.Main) {
val toReplaceIndex = _results.indexOfFirst { it == oldItem }
if (toReplaceIndex >= 0) {
_results[toReplaceIndex] = newItem as IPlatformPlaylist
_adapterResults?.let {
it.notifyItemChanged(it.childToParentPosition(toReplaceIndex))
}
}
}
}
}
_pager = pager
processPagerExceptions(pager)
_results.clear()
val toAdd = pager.getResults()
_results.addAll(toAdd)
_adapterResults?.notifyDataSetChanged()
_recyclerResults?.scrollToPosition(0)
}
private fun loadInitial() {
val channel: IPlatformChannel = _channel ?: return
setLoading(true)
_taskLoadPlaylists.run(channel)
}
private fun loadNextPage() {
val pager: IPager<IPlatformPlaylist> = _pager ?: return
if (_pager?.hasMorePages() == true) {
setLoading(true)
_nextPageHandler.run(pager)
}
}
private fun setLoading(loading: Boolean) {
_loading = loading
_adapterResults?.setLoading(loading)
}
private fun processPagerExceptions(pager: IPager<*>) {
if (pager is MultiPager<*> && pager.allowFailure) {
val ex = pager.getResultExceptions()
for (kv in ex) {
val jsPager: JSPager<*>? = when (kv.key) {
is MultiPager<*> -> (kv.key as MultiPager<*>).findPager { it is JSPager<*> } as JSPager<*>?
is JSPager<*> -> kv.key as JSPager<*>
else -> null
}
context?.let {
lifecycleScope.launch(Dispatchers.Main) {
try {
val channel =
if (kv.value is ChannelException) (kv.value as ChannelException).channelNameOrUrl else null
if (jsPager != null) UIDialogs.toast(
it,
"Plugin ${jsPager.getPluginConfig().name} failed:\n" + (if (!channel.isNullOrEmpty()) "(${channel}) " else "") + "${kv.value.message}",
false
)
else UIDialogs.toast(it, kv.value.message ?: "", false)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show toast.", e)
}
}
}
}
}
}
companion object {
const val TAG = "PlaylistsFragment"
fun newInstance() = ChannelPlaylistsFragment().apply { }
}
}
@@ -1,11 +1,7 @@
package com.futo.platformplayer.fragment.channel.tab
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
interface IChannelTabFragment {
fun setChannel(channel: IPlatformChannel)
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
}
}
fun setChannel(channel: IPlatformChannel);
}
@@ -15,9 +15,8 @@ import androidx.appcompat.widget.AppCompatImageView
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.ViewPager2
import com.bumptech.glide.Glide
import com.futo.platformplayer.*
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
@@ -27,33 +26,26 @@ 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.assume
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp
import com.futo.platformplayer.fragment.channel.tab.IChannelTabFragment
import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment
import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment
import com.futo.platformplayer.fragment.channel.tab.ChannelListFragment
import com.futo.platformplayer.fragment.channel.tab.ChannelMonetizationFragment
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.selectHighestResolutionImage
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.toHumanNumber
import com.futo.platformplayer.views.adapters.ChannelTab
import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.subscriptions.SubscribeButton
import com.futo.polycentric.core.OwnedClaim
import com.futo.polycentric.core.PublicKey
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.futo.polycentric.core.*
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.Dispatchers
@@ -62,530 +54,453 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
@Serializable
data class PolycentricProfile(
val system: PublicKey, val systemState: SystemState, val ownedClaims: List<OwnedClaim>
)
data class PolycentricProfile(val system: PublicKey, val systemState: SystemState, val ownedClaims: List<OwnedClaim>);
class ChannelFragment : MainFragment() {
override val isMainView: Boolean = true
override val hasBottomBar: Boolean = true
private var _view: ChannelView? = null
override val isMainView : Boolean = true;
override val hasBottomBar: Boolean = true;
private var _view: ChannelView? = null;
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack)
_view?.onShown(parameter, isBack)
super.onShownWithView(parameter, isBack);
_view?.onShown(parameter, isBack);
}
override fun onCreateMainView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
val view = ChannelView(this, inflater)
_view = view
return view
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = ChannelView(this, inflater);
_view = view;
return view;
}
override fun onBackPressed(): Boolean {
return _view?.onBackPressed() ?: false
return _view?.onBackPressed() ?: false;
}
override fun onDestroyMainView() {
super.onDestroyMainView()
super.onDestroyMainView();
_view?.cleanup()
_view = null
_view?.cleanup();
_view = null;
}
fun selectTab(tab: ChannelTab) {
_view?.selectTab(tab)
fun selectTab(selectedTabIndex: Int) {
_view?.selectTab(selectedTabIndex);
}
@SuppressLint("ViewConstructor")
class ChannelView
(fragment: ChannelFragment, inflater: LayoutInflater) : LinearLayout(inflater.context) {
private val _fragment: ChannelFragment = fragment
class ChannelView : LinearLayout {
private val _fragment: ChannelFragment;
private var _textChannel: TextView
private var _textChannelSub: TextView
private var _creatorThumbnail: CreatorThumbnail
private var _imageBanner: AppCompatImageView
private var _textChannel: TextView;
private var _textChannelSub: TextView;
private var _creatorThumbnail: CreatorThumbnail;
private var _imageBanner: AppCompatImageView;
private var _tabs: TabLayout
private var _viewPager: ViewPager2
private var _tabs: TabLayout;
private var _viewPager: ViewPager2;
private var _tabLayoutMediator: TabLayoutMediator;
private var _buttonSubscribe: SubscribeButton;
private var _buttonSubscriptionSettings: ImageButton;
// private var _adapter: ChannelViewPagerAdapter;
private var _tabLayoutMediator: TabLayoutMediator
private var _buttonSubscribe: SubscribeButton
private var _buttonSubscriptionSettings: ImageButton
private var _overlayContainer: FrameLayout;
private var _overlay_loading: LinearLayout;
private var _overlay_loading_spinner: ImageView;
private var _overlayContainer: FrameLayout
private var _overlayLoading: LinearLayout
private var _overlayLoadingSpinner: ImageView
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
private var _slideUpOverlay: SlideUpMenuOverlay? = null
private var _isLoading: Boolean = false
private var _selectedTabIndex: Int = -1
private var _isLoading: Boolean = false;
private var _selectedTabIndex: Int = -1;
var channel: IPlatformChannel? = null
private set
private var _url: String? = null
private set;
private var _url: String? = null;
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels);
//recalculate(position, positionOffset);
}
}
private val _taskLoadPolycentricProfile: TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>
private val _taskGetChannel: TaskHandler<String, IPlatformChannel>
private val _taskLoadPolycentricProfile: TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>;
private val _taskGetChannel: TaskHandler<String, IPlatformChannel>;
init {
inflater.inflate(R.layout.fragment_channel, this)
_taskLoadPolycentricProfile =
TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>({ fragment.lifecycleScope },
{ id ->
return@TaskHandler PolycentricCache.instance.getProfileAsync(id)
}).success { setPolycentricProfile(it, animate = true) }.exception<Throwable> {
Logger.w(TAG, "Failed to load polycentric profile.", it)
}
_taskGetChannel = TaskHandler<String, IPlatformChannel>({ fragment.lifecycleScope },
{ url -> StatePlatform.instance.getChannelLive(url) }).success { showChannel(it); }
constructor(fragment: ChannelFragment, inflater: LayoutInflater) : super(inflater.context) {
_fragment = fragment;
inflater.inflate(R.layout.fragment_channel, this);
_taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>({fragment.lifecycleScope}, { id ->
return@TaskHandler PolycentricCache.instance.getProfileAsync(id);
})
.success { it -> setPolycentricProfile(it, animate = true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load polycentric profile.", it);
};
_taskGetChannel = TaskHandler<String, IPlatformChannel>({fragment.lifecycleScope}, { url -> StatePlatform.instance.getChannelLive(url) })
.success { showChannel(it); }
.exception<NoPlatformClientException> {
UIDialogs.showDialog(
context,
UIDialogs.showDialog(context,
R.drawable.ic_sources,
context.getString(R.string.no_source_enabled_to_support_this_channel) + "\n(${_url})",
null,
null,
context.getString(R.string.no_source_enabled_to_support_this_channel) + "\n(${_url})", null, null,
0,
UIDialogs.Action("Back", {
fragment.close(true)
fragment.close(true);
}, UIDialogs.ActionStyle.PRIMARY)
)
}.exception<Throwable> {
Logger.e(TAG, "Failed to load channel.", it)
UIDialogs.showGeneralRetryErrorDialog(
context, it.message ?: "", it, { loadChannel() }, null, fragment
)
);
}
val tabs: TabLayout = findViewById(R.id.tabs)
val viewPager: ViewPager2 = findViewById(R.id.view_pager)
_textChannel = findViewById(R.id.text_channel_name)
_textChannelSub = findViewById(R.id.text_metadata)
_creatorThumbnail = findViewById(R.id.creator_thumbnail)
_imageBanner = findViewById(R.id.image_channel_banner)
_buttonSubscribe = findViewById(R.id.button_subscribe)
_buttonSubscriptionSettings = findViewById(R.id.button_sub_settings)
_overlayLoading = findViewById(R.id.channel_loading_overlay)
_overlayLoadingSpinner = findViewById(R.id.channel_loader)
_overlayContainer = findViewById(R.id.overlay_container)
.exception<Throwable> {
Logger.e(TAG, "Failed to load channel.", it);
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadChannel() });
}
val tabs: TabLayout = findViewById(R.id.tabs);
val viewPager: ViewPager2 = findViewById(R.id.view_pager);
_textChannel = findViewById(R.id.text_channel_name);
_textChannelSub = findViewById(R.id.text_metadata);
_creatorThumbnail = findViewById(R.id.creator_thumbnail);
_imageBanner = findViewById(R.id.image_channel_banner);
_buttonSubscribe = findViewById(R.id.button_subscribe);
_buttonSubscriptionSettings = findViewById(R.id.button_sub_settings);
_overlay_loading = findViewById(R.id.channel_loading_overlay);
_overlay_loading_spinner = findViewById(R.id.channel_loader);
_overlayContainer = findViewById(R.id.overlay_container);
_buttonSubscribe.onSubscribed.subscribe {
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer)
_buttonSubscriptionSettings.visibility =
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
}
_buttonSubscribe.onUnSubscribed.subscribe {
_buttonSubscriptionSettings.visibility =
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
}
_buttonSubscriptionSettings.setOnClickListener {
val url = channel?.url ?: _url ?: return@setOnClickListener
val sub =
StateSubscriptions.instance.getSubscription(url) ?: return@setOnClickListener
UISlideOverlays.showSubscriptionOptionsOverlay(sub, _overlayContainer)
}
val url = channel?.url ?: _url ?: return@setOnClickListener;
val sub = StateSubscriptions.instance.getSubscription(url) ?: return@setOnClickListener;
UISlideOverlays.showSubscriptionOptionsOverlay(sub, _overlayContainer);
};
//TODO: Determine if this is really the only solution (isSaveEnabled=false)
viewPager.isSaveEnabled = false
viewPager.registerOnPageChangeCallback(_onPageChangeCallback)
val adapter = ChannelViewPagerAdapter(fragment.childFragmentManager, fragment.lifecycle)
viewPager.isSaveEnabled = false;
viewPager.registerOnPageChangeCallback(_onPageChangeCallback);
val adapter = ChannelViewPagerAdapter(fragment.childFragmentManager, fragment.lifecycle);
adapter.onChannelClicked.subscribe { c -> fragment.navigate<ChannelFragment>(c) }
adapter.onContentClicked.subscribe { v, _ ->
when (v) {
is IPlatformVideo -> {
StatePlayer.instance.clearQueue()
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail()
}
is IPlatformPlaylist -> {
fragment.navigate<RemotePlaylistFragment>(v)
}
is IPlatformPost -> {
fragment.navigate<PostDetailFragment>(v)
}
if(v is IPlatformVideo) {
StatePlayer.instance.clearQueue();
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail();
} else if (v is IPlatformPlaylist) {
fragment.navigate<PlaylistFragment>(v);
} else if (v is IPlatformPost) {
fragment.navigate<PostDetailFragment>(v);
}
}
adapter.onAddToClicked.subscribe { content ->
adapter.onAddToClicked.subscribe {content ->
_overlayContainer.let {
if (content is IPlatformVideo) _slideUpOverlay =
UISlideOverlays.showVideoOptionsOverlay(content, it)
if(content is IPlatformVideo)
_slideUpOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it);
}
}
adapter.onAddToQueueClicked.subscribe { content ->
if (content is IPlatformVideo) {
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}]")
if(content is IPlatformVideo) {
StatePlayer.instance.addToQueue(content);
}
}
adapter.onUrlClicked.subscribe { url ->
fragment.navigate<BrowserFragment>(url)
fragment.navigate<BrowserFragment>(url);
}
adapter.onContentUrlClicked.subscribe { url, contentType ->
when (contentType) {
when(contentType) {
ContentType.MEDIA -> {
StatePlayer.instance.clearQueue()
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail()
}
ContentType.URL -> fragment.navigate<BrowserFragment>(url)
else -> {}
StatePlayer.instance.clearQueue();
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail();
};
ContentType.URL -> fragment.navigate<BrowserFragment>(url);
else -> {};
}
}
adapter.onLongPress.subscribe { content ->
_overlayContainer.let {
if (content is IPlatformVideo) _slideUpOverlay =
UISlideOverlays.showVideoOptionsOverlay(content, it)
if(content is IPlatformVideo)
_slideUpOverlay = UISlideOverlays.showVideoOptionsOverlay(content, it);
}
}
viewPager.adapter = adapter
val tabLayoutMediator = TabLayoutMediator(
tabs, viewPager, (viewPager.adapter as ChannelViewPagerAdapter)::getTabNames
)
tabLayoutMediator.attach()
viewPager.adapter = adapter;
val tabLayoutMediator = TabLayoutMediator(tabs, viewPager) { tab, position ->
tab.text = when (position) {
0 -> "VIDEOS"
1 -> "CHANNELS"
//2 -> "STORE"
2 -> "SUPPORT"
3 -> "ABOUT"
else -> "Unknown $position"
};
};
tabLayoutMediator.attach();
_tabLayoutMediator = tabLayoutMediator;
_tabs = tabs;
_viewPager = viewPager;
_tabLayoutMediator = tabLayoutMediator
_tabs = tabs
_viewPager = viewPager
if (_selectedTabIndex != -1) {
selectTab(_selectedTabIndex)
selectTab(_selectedTabIndex);
}
setLoading(true)
}
fun selectTab(tab: ChannelTab) {
(_viewPager.adapter as ChannelViewPagerAdapter).getTabPosition(tab)
setLoading(true);
}
fun cleanup() {
_taskLoadPolycentricProfile.cancel()
_taskGetChannel.cancel()
_tabLayoutMediator.detach()
_viewPager.unregisterOnPageChangeCallback(_onPageChangeCallback)
hideSlideUpOverlay()
(_overlayLoadingSpinner.drawable as Animatable?)?.stop()
_taskLoadPolycentricProfile.cancel();
_taskGetChannel.cancel();
_tabLayoutMediator.detach();
_viewPager.unregisterOnPageChangeCallback(_onPageChangeCallback);
hideSlideUpOverlay();
(_overlay_loading_spinner.drawable as Animatable?)?.stop();
}
fun onShown(parameter: Any?, isBack: Boolean) {
hideSlideUpOverlay()
_taskLoadPolycentricProfile.cancel()
_selectedTabIndex = -1
hideSlideUpOverlay();
_taskLoadPolycentricProfile.cancel();
_selectedTabIndex = -1;
if (!isBack || _url == null) {
_imageBanner.setImageDrawable(null)
if (!isBack) {
_imageBanner.setImageDrawable(null);
when (parameter) {
is String -> {
_buttonSubscribe.setSubscribeChannel(parameter)
_buttonSubscriptionSettings.visibility =
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
setPolycentricProfileOr(parameter) {
_textChannel.text = ""
_textChannelSub.text = ""
_creatorThumbnail.setThumbnail(null, true)
Glide.with(_imageBanner).clear(_imageBanner)
}
if (parameter is String) {
_buttonSubscribe.setSubscribeChannel(parameter);
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
setPolycentricProfileOr(parameter) {
_textChannel.text = "";
_textChannelSub.text = "";
_creatorThumbnail.setThumbnail(null, true);
Glide.with(_imageBanner)
.clear(_imageBanner);
};
_url = parameter
loadChannel()
}
_url = parameter;
loadChannel();
} else if (parameter is SerializedChannel) {
showChannel(parameter);
_url = parameter.url;
loadChannel();
} else if (parameter is IPlatformChannel)
showChannel(parameter);
else if (parameter is PlatformAuthorLink) {
setPolycentricProfileOr(parameter.url) {
_textChannel.text = parameter.name;
_textChannelSub.text = "";
_creatorThumbnail.setThumbnail(parameter.thumbnail, true);
Glide.with(_imageBanner)
.clear(_imageBanner);
is SerializedChannel -> {
showChannel(parameter)
_url = parameter.url
loadChannel()
}
loadPolycentricProfile(parameter.id, parameter.url)
};
is IPlatformChannel -> showChannel(parameter)
is PlatformAuthorLink -> {
setPolycentricProfileOr(parameter.url) {
_textChannel.text = parameter.name
_textChannelSub.text = ""
_creatorThumbnail.setThumbnail(parameter.thumbnail, true)
Glide.with(_imageBanner).clear(_imageBanner)
_url = parameter.url;
loadChannel();
} else if (parameter is Subscription) {
setPolycentricProfileOr(parameter.channel.url) {
_textChannel.text = parameter.channel.name;
_textChannelSub.text = "";
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true);
Glide.with(_imageBanner)
.clear(_imageBanner);
loadPolycentricProfile(parameter.id, parameter.url)
}
loadPolycentricProfile(parameter.channel.id, parameter.channel.url)
};
_url = parameter.url
loadChannel()
}
is Subscription -> {
setPolycentricProfileOr(parameter.channel.url) {
_textChannel.text = parameter.channel.name
_textChannelSub.text = ""
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true)
Glide.with(_imageBanner).clear(_imageBanner)
loadPolycentricProfile(parameter.channel.id, parameter.channel.url)
}
_url = parameter.channel.url
loadChannel()
}
_url = parameter.channel.url;
loadChannel();
}
} else {
loadChannel()
loadChannel();
}
}
private fun selectTab(selectedTabIndex: Int) {
_selectedTabIndex = selectedTabIndex
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
fun selectTab(selectedTabIndex: Int) {
_selectedTabIndex = selectedTabIndex;
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex));
}
private fun loadPolycentricProfile(id: PlatformID, url: String) {
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true)
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true);
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = true)
if (cachedPolycentricProfile.expired) {
_taskLoadPolycentricProfile.run(id)
_taskLoadPolycentricProfile.run(id);
}
} else {
_taskLoadPolycentricProfile.run(id)
_taskLoadPolycentricProfile.run(id);
}
}
private fun setLoading(isLoading: Boolean) {
if (_isLoading == isLoading) {
return
return;
}
_isLoading = isLoading
if (isLoading) {
_overlayLoading.visibility = View.VISIBLE
(_overlayLoadingSpinner.drawable as Animatable?)?.start()
} else {
(_overlayLoadingSpinner.drawable as Animatable?)?.stop()
_overlayLoading.visibility = View.GONE
_isLoading = isLoading;
if(isLoading){
_overlay_loading.visibility = View.VISIBLE;
(_overlay_loading_spinner.drawable as Animatable?)?.start();
}
else {
(_overlay_loading_spinner.drawable as Animatable?)?.stop();
_overlay_loading.visibility = View.GONE;
}
}
fun onBackPressed(): Boolean {
if (_slideUpOverlay != null) {
hideSlideUpOverlay()
return true
hideSlideUpOverlay();
return true;
}
return false
return false;
}
private fun hideSlideUpOverlay() {
_slideUpOverlay?.hide(false)
_slideUpOverlay = null
_slideUpOverlay?.hide(false);
_slideUpOverlay = null;
}
private fun loadChannel() {
val url = _url
val url = _url;
if (url != null) {
setLoading(true)
_taskGetChannel.run(url)
setLoading(true);
_taskGetChannel.run(url);
}
}
private fun showChannel(channel: IPlatformChannel) {
setLoading(false)
setLoading(false);
_fragment.topBar?.onShown(channel)
_fragment.topBar?.onShown(channel);
val buttons = arrayListOf(Pair(R.drawable.ic_playlist_add) {
UIDialogs.showConfirmationDialog(context,
context.getString(R.string.do_you_want_to_convert_channel_channelname_to_a_playlist)
.replace("{channelName}", channel.name),
{
UIDialogs.showDialogProgress(context) {
_fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
StatePlaylists.instance.createPlaylistFromChannel(channel) { page ->
_fragment.lifecycleScope.launch(Dispatchers.Main) {
it.setText("${channel.name}\n" + context.getString(R.string.page) + " $page")
}
UIDialogs.showConfirmationDialog(context, context.getString(R.string.do_you_want_to_convert_channel_channelname_to_a_playlist).replace("{channelName}", channel.name), {
UIDialogs.showDialogProgress(context) {
_fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
StatePlaylists.instance.createPlaylistFromChannel(channel) { page ->
_fragment.lifecycleScope.launch(Dispatchers.Main) {
it.setText("${channel.name}\n" + context.getString(R.string.page) + " $page");
}
} catch (ex: Exception) {
Logger.e(TAG, "Error", ex)
UIDialogs.showGeneralErrorDialog(
context,
context.getString(R.string.failed_to_convert_channel),
ex
)
}
withContext(Dispatchers.Main) {
it.hide()
}
};
}
}
})
})
catch(ex: Exception) {
Logger.e(TAG, "Error", ex);
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_convert_channel), ex);
}
withContext(Dispatchers.Main) {
it.hide();
}
};
};
});
});
_fragment.lifecycleScope.launch(Dispatchers.IO) {
val plugin = StatePlatform.instance.getChannelClientOrNull(channel.url)
val plugin = StatePlatform.instance.getChannelClientOrNull(channel.url);
withContext(Dispatchers.Main) {
if (plugin != null && plugin.capabilities.hasSearchChannelContents) {
buttons.add(Pair(R.drawable.ic_search) {
_fragment.navigate<SuggestionsFragment>(
SuggestionsFragmentData(
"", SearchType.VIDEO, channel.url
)
)
})
_fragment.navigate<SuggestionsFragment>(SuggestionsFragmentData("", SearchType.VIDEO, channel.url));
});
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons)
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons);
}
}
}
_buttonSubscribe.setSubscribeChannel(channel)
_buttonSubscriptionSettings.visibility =
if (_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE
_textChannel.text = channel.name
_textChannelSub.text =
if (channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + context.getString(
R.string.subscribers
).lowercase() else ""
_buttonSubscribe.setSubscribeChannel(channel);
_buttonSubscriptionSettings.visibility = if(_buttonSubscribe.isSubscribed) View.VISIBLE else View.GONE;
_textChannel.text = channel.name;
_textChannelSub.text = if(channel.subscribers > 0) "${channel.subscribers.toHumanNumber()} " + context.getString(R.string.subscribers).lowercase() else "";
val supportsPlaylists =
StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists
val playlistPosition = 1
if (supportsPlaylists && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
ChannelTab.PLAYLISTS.ordinal.toLong()
)
) {
// keep the current tab selected
if (_viewPager.currentItem >= playlistPosition) {
_viewPager.setCurrentItem(_viewPager.currentItem + 1, false)
}
//TODO: Find a better way to access the adapter fragments..
(_viewPager.adapter as ChannelViewPagerAdapter).insert(
playlistPosition,
ChannelTab.PLAYLISTS
)
}
if (!supportsPlaylists && (_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
ChannelTab.PLAYLISTS.ordinal.toLong()
)
) {
// keep the current tab selected
if (_viewPager.currentItem >= playlistPosition) {
_viewPager.setCurrentItem(_viewPager.currentItem - 1, false)
}
(_viewPager.adapter as ChannelViewPagerAdapter).remove(playlistPosition)
(_viewPager.adapter as ChannelViewPagerAdapter?)?.let {
it.getFragment<ChannelContentsFragment>().setChannel(channel);
it.getFragment<ChannelAboutFragment>().setChannel(channel);
it.getFragment<ChannelListFragment>().setChannel(channel);
it.getFragment<ChannelMonetizationFragment>().setChannel(channel);
//TODO: Call on other tabs as needed
}
// sets the channel for each tab
for (fragment in _fragment.childFragmentManager.fragments) {
(fragment as IChannelTabFragment).setChannel(channel)
}
(_viewPager.adapter as ChannelViewPagerAdapter).channel = channel
_viewPager.adapter!!.notifyDataSetChanged()
this.channel = channel
this.channel = channel;
setPolycentricProfileOr(channel.url) {
_textChannel.text = channel.name
_creatorThumbnail.setThumbnail(channel.thumbnail, true)
Glide.with(_imageBanner).load(channel.banner).crossfade().into(_imageBanner)
_textChannel.text = channel.name;
_creatorThumbnail.setThumbnail(channel.thumbnail, true);
Glide.with(_imageBanner)
.load(channel.banner)
.crossfade()
.into(_imageBanner);
_taskLoadPolycentricProfile.run(channel.id)
}
_taskLoadPolycentricProfile.run(channel.id);
};
}
private fun setPolycentricProfileOr(url: String, or: () -> Unit) {
setPolycentricProfile(null, animate = false)
setPolycentricProfile(null, animate = false);
val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(url) }
val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(url) };
if (cachedProfile != null) {
setPolycentricProfile(cachedProfile, animate = false)
setPolycentricProfile(cachedProfile, animate = false);
} else {
or()
or();
}
}
private fun setPolycentricProfile(
cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean
) {
val dp35 = 35.dp(resources)
val profile = cachedPolycentricProfile?.profile
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)?.let {
it.toURLInfoSystemLinkUrl(
profile.system.toProto(), it.process, profile.systemState.servers.toList()
)
}
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
val dp_35 = 35.dp(resources)
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate)
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(channel?.thumbnail, animate)
_creatorThumbnail.setHarborAvailable(
profile != null, animate, profile?.system?.toProto()
)
_creatorThumbnail.setThumbnail(channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
}
val banner = profile?.systemState?.banner?.selectHighestResolutionImage()?.let {
it.toURLInfoSystemLinkUrl(
profile.system.toProto(), it.process, profile.systemState.servers.toList()
)
}
val banner = profile?.systemState?.banner?.selectHighestResolutionImage()
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (banner != null) {
Glide.with(_imageBanner).load(banner).crossfade().into(_imageBanner)
Glide.with(_imageBanner)
.load(banner)
.crossfade()
.into(_imageBanner);
} else {
Glide.with(_imageBanner).load(channel?.banner).crossfade().into(_imageBanner)
Glide.with(_imageBanner)
.load(channel?.banner)
.crossfade()
.into(_imageBanner);
}
if (profile != null) {
_fragment.topBar?.onShown(profile)
_textChannel.text = profile.systemState.username
_fragment.topBar?.onShown(profile);
_textChannel.text = profile.systemState.username;
}
// sets the profile for each tab
for (fragment in _fragment.childFragmentManager.fragments) {
(fragment as IChannelTabFragment).setPolycentricProfile(profile)
(_viewPager.adapter as ChannelViewPagerAdapter?)?.let {
it.getFragment<ChannelAboutFragment>().setPolycentricProfile(profile);
it.getFragment<ChannelMonetizationFragment>().setPolycentricProfile(profile);
it.getFragment<ChannelListFragment>().setPolycentricProfile(profile);
it.getFragment<ChannelContentsFragment>().setPolycentricProfile(profile);
//TODO: Call on other tabs as needed
}
val insertPosition = 1
//TODO only add channels and support if its setup on the polycentric profile
if (profile != null && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
ChannelTab.SUPPORT.ordinal.toLong()
)
) {
(_viewPager.adapter as ChannelViewPagerAdapter).insert(insertPosition, ChannelTab.SUPPORT)
}
if (profile != null && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
ChannelTab.CHANNELS.ordinal.toLong()
)
) {
(_viewPager.adapter as ChannelViewPagerAdapter).insert(insertPosition, ChannelTab.CHANNELS)
}
(_viewPager.adapter as ChannelViewPagerAdapter).profile = profile
_viewPager.adapter!!.notifyDataSetChanged()
}
}
companion object {
const val TAG = "ChannelFragment"
val TAG = "ChannelFragment";
fun newInstance() = ChannelFragment().apply { }
}
}
}
@@ -1,9 +1,7 @@
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
@@ -120,7 +118,6 @@ class CommentsFragment : MainFragment() {
holder.onDelete.subscribe(::onDelete);
holder.onRepliesClick.subscribe(::onRepliesClick);
holder.onClick.subscribe(::onClick);
holder.onAuthorClick.subscribe(::onAuthorClick);
return@InsertedViewAdapterWithLoader holder;
}
);
@@ -214,19 +211,6 @@ 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;
@@ -6,32 +6,26 @@ import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.contents.ContentType
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.IPager
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
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
import com.futo.platformplayer.views.adapters.feedtypes.PreviewNestedVideoViewHolder
import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoViewHolder
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.withTimestamp
import kotlin.math.floor
abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent, IPlatformContent, IPager<IPlatformContent>, ContentPreviewViewHolder> where TFragment : MainFragment {
@@ -87,12 +81,6 @@ 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)
@@ -147,7 +135,6 @@ 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);
}
@@ -187,7 +174,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
fragment.navigate<VideoDetailFragment>(content).maximizeVideoDetail();
}
} else if (content is IPlatformPlaylist) {
fragment.navigate<RemotePlaylistFragment>(content);
fragment.navigate<PlaylistFragment>(content);
} else if (content is IPlatformPost) {
fragment.navigate<PostDetailFragment>(content);
}
@@ -198,7 +185,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
StatePlayer.instance.clearQueue();
fragment.navigate<VideoDetailFragment>(url).maximizeVideoDetail();
};
ContentType.PLAYLIST -> fragment.navigate<RemotePlaylistFragment>(url);
ContentType.PLAYLIST -> fragment.navigate<PlaylistFragment>(url);
ContentType.URL -> fragment.navigate<BrowserFragment>(url);
else -> {};
}
@@ -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() }, null, fragment);
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
}
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, _channelUrl != null);
val filtersOverlay = UISlideOverlays.showFiltersOverlay(lifecycleScope, it, _enabledClientIds!!, filterValuesCopy);
filtersOverlay.onOK.subscribe { enabledClientIds, changed ->
if (changed) {
setFilterValues(filtersOverlay.commonCapabilities, filterValuesCopy);
@@ -156,7 +156,7 @@ class ContentSearchResultsFragment : MainFragment() {
onSearch.subscribe(this) {
if(it.isHttpUrl()) {
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
navigate<RemotePlaylistFragment>(it);
navigate<PlaylistFragment>(it);
else if(StatePlatform.instance.hasEnabledChannelClient(it))
navigate<ChannelFragment>(it);
else
@@ -170,11 +170,7 @@ class ContentSearchResultsFragment : MainFragment() {
fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
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 commonCapabilities = StatePlatform.instance.getCommonSearchCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
val sorts = commonCapabilities?.sorts ?: listOf();
if (sorts.size > 1) {
withContext(Dispatchers.Main) {
@@ -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() }, null, fragment);
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
}
}
@@ -12,10 +12,8 @@ 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
@@ -145,7 +143,6 @@ 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!!) };
@@ -153,35 +150,23 @@ class DownloadsFragment : MainFragment() {
_listActiveDownloadsContainer.visibility = GONE;
else {
_listActiveDownloadsContainer.visibility = VISIBLE;
_listActiveDownloadsMeta.text = "(${activeDownloads.size} videos)";
_listActiveDownloadsMeta.text = "(${activeDownloads.size})";
_listActiveDownloads.removeAllViews();
for(view in activeDownloads.take(4).map { ActiveDownloadItem(context, it, _frag.lifecycleScope) })
for(view in activeDownloads.map { ActiveDownloadItem(context, it, _frag.lifecycleScope) })
_listActiveDownloads.addView(view);
}
if(playlists.isEmpty() && watchLaterDownload == null)
if(playlists.isEmpty())
_listPlaylistsContainer.visibility = GONE;
else {
_listPlaylistsContainer.visibility = VISIBLE;
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()})";
_listPlaylistsMeta.text = "(${playlists.size} ${context.getString(R.string.playlists).lowercase()}, ${playlists.sumOf { it.playlist.videos.size }} ${context.getString(R.string.videos).lowercase()})";
_listPlaylists.removeAllViews();
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) }) {
for(view in playlists.map { PlaylistDownloadItem(context, it) }) {
view.setOnClickListener {
if(view.obj is Playlist) {
_frag.navigate<PlaylistFragment>(view.obj);
}
_frag.navigate<PlaylistFragment>(view.playlist.playlist);
};
_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() });
};
@@ -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);
});
};
}
@@ -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);
@@ -12,21 +12,16 @@ import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.fragment.mainactivity.topbar.ImportTopBarFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.adapters.viewholders.ImportPlaylistsViewHolder
import com.futo.platformplayer.views.adapters.viewholders.SelectablePlaylist
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ImportPlaylistsFragment : MainFragment() {
override val isMainView : Boolean = true;
@@ -72,7 +67,7 @@ class ImportPlaylistsFragment : MainFragment() {
private val _items: ArrayList<SelectablePlaylist> = arrayListOf();
private var _currentLoadIndex = 0;
private var _taskLoadPlaylist: TaskHandler<String, IPlatformPlaylistDetails?>;
private var _taskLoadPlaylist: TaskHandler<String, Playlist?>;
constructor(fragment: ImportPlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) {
_fragment = fragment;
@@ -107,7 +102,7 @@ class ImportPlaylistsFragment : MainFragment() {
setLoading(false);
_taskLoadPlaylist = TaskHandler<String, IPlatformPlaylistDetails?>({fragment.lifecycleScope}, { link -> StatePlatform.instance.getPlaylist(link); })
_taskLoadPlaylist = TaskHandler<String, Playlist?>({fragment.lifecycleScope}, { link -> StatePlatform.instance.getPlaylist(link).toPlaylist(); })
.success {
if (it != null) {
_items.add(SelectablePlaylist(it));
@@ -118,7 +113,7 @@ class ImportPlaylistsFragment : MainFragment() {
}.exceptionWithParameter<Throwable> { ex, para ->
//setLoading(false);
Logger.w(ChannelFragment.TAG, "Failed to load results.", ex);
UIDialogs.appToast(context.getString(R.string.failed_to_fetch) + "\n${para}\n" + ex.message, false)
UIDialogs.toast(context, context.getString(R.string.failed_to_fetch) + "\n${para}", false)
//UIDialogs.showDataRetryDialog(layoutInflater, { load(); });
loadNext();
};
@@ -152,32 +147,12 @@ class ImportPlaylistsFragment : MainFragment() {
it.title = context.getString(R.string.import_playlists);
it.onImport.subscribe(this) {
val playlistsToImport = _items.filter { i -> i.selected }.toList();
UIDialogs.showDialogProgress(context) {
it.setText("Importing playlists..");
it.setProgress(0f);
_fragment.lifecycleScope.launch(Dispatchers.IO) {
for ((i, playlistToImport) in playlistsToImport.withIndex()) {
withContext(Dispatchers.Main) {
it.setText("Importing playlists..\n[${playlistToImport.playlist.name}]");
}
try {
StatePlaylists.instance.createOrUpdatePlaylist(playlistToImport.playlist.toPlaylist());
}
catch(ex: Throwable) {
UIDialogs.appToast("Failed to import [${playlistToImport.playlist.name}]\n" + ex.message);
}
withContext(Dispatchers.Main) {
it.setProgress(i.toDouble() / playlistsToImport.size);
}
}
withContext(Dispatchers.Main) {
UIDialogs.toast("${playlistsToImport.size} " + context.getString(R.string.playlists_imported));
_fragment.closeSegment();
it.dismiss();
}
}
for (playlistToImport in playlistsToImport) {
StatePlaylists.instance.createOrUpdatePlaylist(playlistToImport.playlist);
}
UIDialogs.toast("${playlistsToImport.size} " + context.getString(R.string.playlists_imported));
_fragment.closeSegment();
};
}
}
@@ -1,11 +1,14 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.annotation.SuppressLint
import android.graphics.drawable.Animatable
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.app.ShareCompat
import androidx.core.view.setPadding
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
@@ -14,6 +17,7 @@ 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.downloads.VideoDownload
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.StateApp
@@ -26,6 +30,7 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class PlaylistFragment : MainFragment() {
override val isMainView : Boolean = true;
@@ -65,6 +70,7 @@ class PlaylistFragment : MainFragment() {
private val _fragment: PlaylistFragment;
private var _playlist: Playlist? = null;
private var _remotePlaylist: IPlatformPlaylistDetails? = null;
private var _editPlaylistNameInput: SlideUpMenuTextInput? = null;
private var _editPlaylistOverlay: SlideUpMenuOverlay? = null;
private var _url: String? = null;
@@ -130,16 +136,17 @@ class PlaylistFragment : MainFragment() {
return@TaskHandler StatePlatform.instance.getPlaylist(it);
})
.success {
setName(it.name);
//TODO: Implement support for pagination
setVideos(it.toPlaylist().videos, false);
setVideoCount(it.videoCount);
setLoading(false);
_remotePlaylist = it;
setName(it.name);
setVideos(it.contents.getResults(), false);
setVideoCount(it.videoCount);
//TODO: Implement support for pagination
}
.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, null, fragment);
UIDialogs.showGeneralRetryErrorDialog(c, context.getString(R.string.failed_to_load_playlist), it, ::fetchPlaylist);
};
}
@@ -148,6 +155,7 @@ class PlaylistFragment : MainFragment() {
if (parameter is Playlist?) {
_playlist = parameter;
_remotePlaylist = null;
_url = null;
if(parameter != null) {
@@ -167,6 +175,7 @@ class PlaylistFragment : MainFragment() {
//TODO: Do I have to remove the showConvertPlaylistButton(); button here?
} else if (parameter is IPlatformPlaylist) {
_playlist = null;
_remotePlaylist = null;
_url = parameter.url;
setVideoCount(parameter.videoCount);
@@ -176,8 +185,10 @@ class PlaylistFragment : MainFragment() {
setButtonEditVisible(false);
fetchPlaylist();
showConvertPlaylistButton();
} else if (parameter is String) {
_playlist = null;
_remotePlaylist = null;
_url = parameter;
setName(null);
@@ -187,20 +198,17 @@ class PlaylistFragment : MainFragment() {
setButtonEditVisible(false);
fetchPlaylist();
showConvertPlaylistButton();
}
_playlist?.let {
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this::download);
}
updateDownloadState();
}
fun onResume() {
StateDownloads.instance.onDownloadsChanged.subscribe(this) {
_fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
_playlist?.let {
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this@PlaylistView::download);
}
updateDownloadState();
} catch (e: Throwable) {
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
}
@@ -209,9 +217,7 @@ class PlaylistFragment : MainFragment() {
StateDownloads.instance.onDownloadedChanged.subscribe(this) {
_fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
_playlist?.let {
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this@PlaylistView::download);
}
updateDownloadState();
} catch (e: Throwable) {
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
}
@@ -219,17 +225,39 @@ class PlaylistFragment : MainFragment() {
};
}
private fun download() {
_playlist?.let {
UISlideOverlays.showDownloadPlaylistOverlay(it, overlayContainer);
}
}
fun onPause() {
StateDownloads.instance.onDownloadsChanged.remove(this);
StateDownloads.instance.onDownloadedChanged.remove(this);
}
private fun showConvertPlaylistButton() {
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
val remotePlaylist = _remotePlaylist;
if (remotePlaylist == null) {
UIDialogs.toast(context.getString(R.string.please_wait_for_playlist_to_finish_loading));
return@Pair;
}
setLoading(true);
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try {
StatePlaylists.instance.playlistStore.save(remotePlaylist.toPlaylist());
withContext(Dispatchers.Main) {
setLoading(false);
UIDialogs.toast(context.getString(R.string.playlist_copied_as_local_playlist));
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
setLoading(false);
}
throw e;
}
}
}));
}
private fun fetchPlaylist() {
Logger.i(TAG, "fetchPlaylist")
@@ -240,6 +268,43 @@ 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; }
@@ -250,15 +315,21 @@ class PlaylistFragment : MainFragment() {
override fun onPlayAllClick() {
val playlist = _playlist;
val remotePlaylist = _remotePlaylist;
if (playlist != null) {
StatePlayer.instance.setPlaylist(playlist, focus = true);
} else if (remotePlaylist != null) {
StatePlayer.instance.setPlaylist(remotePlaylist, focus = true, shuffle = false);
}
}
override fun onShuffleClick() {
val playlist = _playlist;
val remotePlaylist = _remotePlaylist;
if (playlist != null) {
StatePlayer.instance.setPlaylist(playlist, focus = true, shuffle = true);
} else if (remotePlaylist != null) {
StatePlayer.instance.setPlaylist(remotePlaylist, focus = true, shuffle = true);
}
}
@@ -274,12 +345,19 @@ class PlaylistFragment : MainFragment() {
}
override fun onVideoClicked(video: IPlatformVideo) {
val playlist = _playlist;
val remotePlaylist = _remotePlaylist;
if (playlist != null) {
val index = playlist.videos.indexOf(video);
if (index == -1)
return;
StatePlayer.instance.setPlaylist(playlist, index, true);
} else if (remotePlaylist != null) {
val index = remotePlaylist.contents.getResults().indexOf(video);
if (index == -1)
return;
StatePlayer.instance.setPlaylist(remotePlaylist, index, true);
}
}
}
@@ -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() }, null, fragment);
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
}
}
@@ -35,13 +35,11 @@ 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
import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.toHumanNumber
import com.futo.platformplayer.views.adapters.ChannelTab
import com.futo.platformplayer.views.adapters.feedtypes.PreviewPostView
import com.futo.platformplayer.views.comments.AddCommentView
import com.futo.platformplayer.views.others.CreatorThumbnail
@@ -163,7 +161,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, null, _fragment);
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost);
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
@@ -213,8 +211,6 @@ class PostDetailFragment : MainFragment {
_repliesOverlay = findViewById(R.id.replies_overlay);
_textContent.setPlatformPlayerLinkMovementMethod(context);
_buttonSubscribe.onSubscribed.subscribe {
//TODO: add overlay to layout
//UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
@@ -265,7 +261,7 @@ class PostDetailFragment : MainFragment {
_buttonSupport.setOnClickListener {
val author = _post?.author ?: _postOverview?.author;
author?.let { _fragment.navigate<ChannelFragment>(it).selectTab(ChannelTab.SUPPORT); };
author?.let { _fragment.navigate<ChannelFragment>(it).selectTab(2); };
};
_buttonStore.setOnClickListener {
@@ -477,7 +473,6 @@ class PostDetailFragment : MainFragment {
}
updateCommentType(true);
setLoading(false);
}
fun setPostOverview(value: IPlatformPost) {
@@ -1,367 +0,0 @@
package com.futo.platformplayer.fragment.mainactivity.main
import android.annotation.SuppressLint
import android.graphics.drawable.Animatable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.app.ShareCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.models.JSPager
import com.futo.platformplayer.api.media.structures.IAsyncPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.MultiPager
import com.futo.platformplayer.api.media.structures.ReusablePager
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFragment
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.VideoListEditorViewHolder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class RemotePlaylistFragment : MainFragment() {
override val isMainView : Boolean = true;
override val isTab: Boolean = true;
override val hasBottomBar: Boolean get() = true;
private var _view: RemotePlaylistView? = null;
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
_view?.onShown(parameter);
}
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = RemotePlaylistView(this, inflater);
_view = view;
return view;
}
override fun onDestroyMainView() {
super.onDestroyMainView();
_view = null;
}
@SuppressLint("ViewConstructor")
class RemotePlaylistView : LinearLayout {
private val _fragment: RemotePlaylistFragment;
private var _remotePlaylist: IPlatformPlaylistDetails? = null;
private var _remotePlaylistPagerWindow: IPager<IPlatformVideo>? = null;
private var _url: String? = null;
private val _videos: ArrayList<IPlatformVideo> = arrayListOf();
private val _taskLoadPlaylist: TaskHandler<String, IPlatformPlaylistDetails>;
private var _nextPageHandler: TaskHandler<IPager<IPlatformVideo>, List<IPlatformVideo>>;
private var _imagePlaylistThumbnail: ImageView;
private var _textName: TextView;
private var _textMetadata: TextView;
private var _loaderOverlay: FrameLayout;
private var _imageLoader: ImageView;
private var _overlayContainer: FrameLayout;
private var _buttonShare: ImageButton;
private var _recyclerPlaylist: RecyclerView;
private var _llmPlaylist: LinearLayoutManager;
private val _adapterVideos: InsertedViewAdapterWithLoader<VideoListEditorViewHolder>;
private val _scrollListener: RecyclerView.OnScrollListener
constructor(fragment: RemotePlaylistFragment, inflater: LayoutInflater) : super(inflater.context) {
inflater.inflate(R.layout.fragment_remote_playlist, this);
_fragment = fragment;
_textName = findViewById(R.id.text_name);
_textMetadata = findViewById(R.id.text_metadata);
_imagePlaylistThumbnail = findViewById(R.id.image_playlist_thumbnail);
_loaderOverlay = findViewById(R.id.layout_loading_overlay);
_imageLoader = findViewById(R.id.image_loader);
_recyclerPlaylist = findViewById(R.id.recycler_playlist);
_llmPlaylist = LinearLayoutManager(context);
_adapterVideos = InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(),
childCountGetter = { _videos.size },
childViewHolderBinder = { viewHolder, position -> viewHolder.bind(_videos[position], false); },
childViewHolderFactory = { viewGroup, _ ->
val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.list_playlist, viewGroup, false);
val holder = VideoListEditorViewHolder(view, null);
holder.onClick.subscribe {
showConvertConfirmationModal();
};
return@InsertedViewAdapterWithLoader holder;
}
);
_recyclerPlaylist.adapter = _adapterVideos;
_recyclerPlaylist.layoutManager = _llmPlaylist;
_overlayContainer = findViewById(R.id.overlay_container);
val buttonPlayAll = findViewById<LinearLayout>(R.id.button_play_all);
val buttonShuffle = findViewById<LinearLayout>(R.id.button_shuffle);
_buttonShare = findViewById(R.id.button_share);
_buttonShare.setOnClickListener {
val remotePlaylist = _remotePlaylist ?: return@setOnClickListener;
_fragment.startActivity(ShareCompat.IntentBuilder(context)
.setType("text/plain")
.setText(remotePlaylist.shareUrl)
.intent);
};
buttonPlayAll.setOnClickListener {
showConvertConfirmationModal();
};
buttonShuffle.setOnClickListener {
showConvertConfirmationModal();
};
_taskLoadPlaylist = TaskHandler<String, IPlatformPlaylistDetails>(
StateApp.instance.scopeGetter,
{
return@TaskHandler StatePlatform.instance.getPlaylist(it);
})
.success {
_remotePlaylist = it;
val c = it.contents;
_remotePlaylistPagerWindow = if (c is ReusablePager) c.getWindow() else c;
setName(it.name);
setVideos(_remotePlaylistPagerWindow!!.getResults());
setVideoCount(it.videoCount);
setLoading(false);
}
.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, null, fragment);
};
_nextPageHandler = TaskHandler<IPager<IPlatformVideo>, List<IPlatformVideo>>({fragment.lifecycleScope}, {
if (it is IAsyncPager<*>)
it.nextPageAsync();
else
it.nextPage();
processPagerExceptions(it);
return@TaskHandler it.getResults();
}).success {
_adapterVideos.setLoading(false);
addVideos(it);
//TODO: ensureEnoughContentVisible()
}.exception<Throwable> {
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);
};
_scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val visibleItemCount = _recyclerPlaylist.childCount
val firstVisibleItem = _llmPlaylist.findFirstVisibleItemPosition()
val visibleThreshold = 15
if (!_adapterVideos.isLoading && firstVisibleItem + visibleItemCount + visibleThreshold >= _videos.size) {
loadNextPage()
}
}
}
_recyclerPlaylist.addOnScrollListener(_scrollListener)
}
private fun loadNextPage() {
val pager: IPager<IPlatformVideo> = _remotePlaylistPagerWindow ?: return;
val hasMorePages = pager.hasMorePages();
Logger.i(TAG, "loadNextPage() hasMorePages=$hasMorePages, page size=${pager.getResults().size}");
if (pager.hasMorePages()) {
_adapterVideos.setLoading(true);
_nextPageHandler.run(pager);
}
}
private fun processPagerExceptions(pager: IPager<*>) {
if(pager is MultiPager<*> && pager.allowFailure) {
val ex = pager.getResultExceptions();
for(kv in ex) {
val jsVideoPager: JSPager<*>? = if(kv.key is MultiPager<*>)
(kv.key as MultiPager<*>).findPager { it is JSPager<*> } as JSPager<*>?;
else if(kv.key is JSPager<*>)
kv.key as JSPager<*>;
else null;
context?.let {
_fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
if(jsVideoPager != null)
UIDialogs.toast(it, context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", jsVideoPager.getPluginConfig().name).replace("{message}", kv.value.message ?: ""), false);
else
UIDialogs.toast(it, kv.value.message ?: "", false);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to show toast.", e)
}
}
}
}
}
}
fun onShown(parameter: Any?) {
_taskLoadPlaylist.cancel();
_nextPageHandler.cancel();
if (parameter is IPlatformPlaylist) {
_remotePlaylist = null;
_url = parameter.url;
setVideoCount(parameter.videoCount);
setName(parameter.name);
setVideos(null);
fetchPlaylist();
showConvertPlaylistButton();
} else if (parameter is String) {
_remotePlaylist = null;
_url = parameter;
setName(null);
setVideos(null);
setVideoCount(-1);
fetchPlaylist();
showConvertPlaylistButton();
}
}
private fun showConvertConfirmationModal() {
val remotePlaylist = _remotePlaylist;
if (remotePlaylist == null) {
UIDialogs.toast(context.getString(R.string.please_wait_for_playlist_to_finish_loading));
return;
}
val c = context ?: return;
UIDialogs.showConfirmationDialog(c, "Conversion to local playlist is required for this action", {
setLoading(true);
UIDialogs.showDialogProgress(context) {
it.setText("Converting playlist..");
it.setProgress(0f);
_fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
val playlist = remotePlaylist.toPlaylist() { progress ->
_fragment.lifecycleScope.launch(Dispatchers.Main) {
it.setProgress(progress.toDouble() / remotePlaylist.videoCount);
}
};
StatePlaylists.instance.playlistStore.save(playlist);
withContext(Dispatchers.Main) {
UIDialogs.toast("Playlist converted");
it.dismiss();
_fragment.navigate<PlaylistFragment>(playlist);
}
}
catch(ex: Throwable) {
UIDialogs.appToast("Failed to convert playlist.\n" + ex.message);
}
}
}
});
}
private fun showConvertPlaylistButton() {
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(arrayListOf(Pair(R.drawable.ic_copy) {
showConvertConfirmationModal();
}));
}
private fun fetchPlaylist() {
Logger.i(TAG, "fetchPlaylist")
val url = _url;
if (!url.isNullOrBlank()) {
setLoading(true);
_taskLoadPlaylist.run(url);
}
}
private fun setName(name: String?) {
_textName.text = name ?: "";
}
private fun setVideoCount(videoCount: Int = -1) {
_textMetadata.text = if (videoCount == -1) "" else "${videoCount} " + context.getString(R.string.videos);
}
private fun setVideos(videos: List<IPlatformVideo>?) {
if (!videos.isNullOrEmpty()) {
val video = videos.first();
_imagePlaylistThumbnail.let {
Glide.with(it)
.load(video.thumbnails.getHQThumbnail())
.placeholder(R.drawable.placeholder_video_thumbnail)
.crossfade()
.into(it);
};
} else {
_textMetadata.text = "0 " + context.getString(R.string.videos);
Glide.with(_imagePlaylistThumbnail)
.load(R.drawable.placeholder_video_thumbnail)
.into(_imagePlaylistThumbnail)
}
synchronized(_videos) {
_videos.clear();
_videos.addAll(videos ?: listOf());
_adapterVideos.notifyDataSetChanged();
}
}
private fun addVideos(videos: List<IPlatformVideo>) {
synchronized(_videos) {
val index = _videos.size;
_videos.addAll(videos);
_adapterVideos.notifyItemRangeInserted(_adapterVideos.childToParentPosition(index), videos.size);
}
}
private fun setLoading(isLoading: Boolean) {
if (isLoading){
(_imageLoader.drawable as Animatable?)?.start()
_loaderOverlay.visibility = View.VISIBLE;
}
else {
_loaderOverlay.visibility = View.GONE;
(_imageLoader.drawable as Animatable?)?.stop()
}
}
}
companion object {
private const val TAG = "RemotePlaylistFragment";
fun newInstance() = RemotePlaylistFragment().apply {}
}
}
@@ -12,7 +12,6 @@ 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
@@ -101,11 +100,6 @@ class SourceDetailFragment : MainFragment() {
loadConfig(parameter);
updateSourceViews();
}
else if(parameter is UpdatePluginAction) {
loadConfig(parameter.config);
updateSourceViews();
checkForUpdatesSource();
}
setLoading(false);
}
@@ -113,20 +107,17 @@ class SourceDetailFragment : MainFragment() {
fun onHide() {
val id = _config?.id ?: return;
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;
reloadSource(id);
UIDialogs.toast(context.getString(R.string.plugin_settings_saved), false);
}
if(shouldReload)
reloadSource(id);
if(_settingsAppChanged) {
_settingsAppForm.setObjectValues();
StatePlugins.instance.savePlugin(id);
}
}
@@ -146,25 +137,9 @@ 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 { field, value ->
_settingsAppForm.onChanged.subscribe { _, _ ->
_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)
@@ -572,8 +547,4 @@ class SourceDetailFragment : MainFragment() {
const val TAG = "SourceDetailFragment";
fun newInstance() = SourceDetailFragment().apply {}
}
class UpdatePluginAction(val config: SourcePluginConfig) {
}
}
@@ -25,7 +25,6 @@ import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.stores.FragmentedStorage
@@ -198,7 +197,6 @@ class SubscriptionsFeedFragment : MainFragment() {
val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST);
var allowLive: Boolean = true;
var allowPlanned: Boolean = false;
var allowWatched: Boolean = true;
override fun encode(): String {
return Json.encodeToString(this);
}
@@ -262,7 +260,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) }, null, fragment);
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults(true) });
else {
finishRefreshLayoutLoader();
setLoading(false);
@@ -306,8 +304,7 @@ class SubscriptionsFeedFragment : MainFragment() {
SubscriptionBar.Toggle(context.getString(R.string.videos), _filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), it); },
SubscriptionBar.Toggle(context.getString(R.string.posts), _filterSettings.allowContentTypes.contains(ContentType.POST)) { toggleFilterContentType(ContentType.POST, it); },
SubscriptionBar.Toggle(context.getString(R.string.live), _filterSettings.allowLive) { _filterSettings.allowLive = it; _filterSettings.save(); loadResults(false); },
SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); },
SubscriptionBar.Toggle(context.getString(R.string.watched), _filterSettings.allowWatched) { _filterSettings.allowWatched = it; _filterSettings.save(); loadResults(false); }
SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); }
);
}
@@ -339,9 +336,6 @@ class SubscriptionsFeedFragment : MainFragment() {
return results.filter {
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
if(it is IPlatformVideo && it.duration > 0 && !_filterSettings.allowWatched && StateHistory.instance.isHistoryWatched(it.url, it.duration))
return@filter false;
//TODO: Check against a sub cache
if(filterGroup != null && !filterGroup.urls.contains(it.author.url))
return@filter false;
@@ -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() }, null, this);
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadSuggestions() });
};
constructor(): super() {
@@ -15,7 +15,6 @@ import com.futo.platformplayer.api.media.models.Thumbnail
import com.futo.platformplayer.api.media.models.Thumbnails
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
import com.futo.platformplayer.api.media.models.ratings.IRating
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
@@ -145,8 +144,10 @@ class TutorialFragment : MainFragment() {
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
return EmptyPager()
}
override fun getPlaybackTracker(): IPlaybackTracker? = null;
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
override fun getPlaybackTracker(): IPlaybackTracker? {
return null
}
}
companion object {
@@ -23,7 +23,6 @@ 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
@@ -125,7 +124,6 @@ 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
@@ -246,7 +244,6 @@ 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;
@@ -352,8 +349,7 @@ 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_browser = findViewById(R.id.videodetail_container_webview)
_container_content_support = findViewById(R.id.videodetail_container_support)
_textComments = findViewById(R.id.text_comments);
_addCommentView = findViewById(R.id.add_comment_view);
@@ -402,10 +398,6 @@ class VideoDetailView : ConstraintLayout {
}
}
};
_monetization.onUrlTap.subscribe {
fragment.navigate<BrowserFragment>(it);
onMinimize.emit();
}
_player.attachPlayer();
@@ -628,7 +620,6 @@ 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);
@@ -649,20 +640,6 @@ 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 = "";
@@ -690,7 +667,7 @@ class VideoDetailView : ConstraintLayout {
_lastAudioSource = null;
_lastSubtitleSource = null;
video = null;
cleanupPlaybackTracker();
_playbackTracker = null;
Logger.i(TAG, "Keep screen on unset onClose")
fragment.activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
};
@@ -1033,7 +1010,7 @@ class VideoDetailView : ConstraintLayout {
_searchVideo = null;
video = null;
cleanupPlaybackTracker();
_playbackTracker = null;
_url = url;
_videoResumePositionMilliseconds = resumeSeconds * 1000;
_rating.visibility = View.GONE;
@@ -1058,16 +1035,11 @@ class VideoDetailView : ConstraintLayout {
switchContentView(_container_content_main);
}
fun setVideoOverview(video: IPlatformVideo, fetch: Boolean = true, resumeSeconds: Long = 0, bypassSameVideoCheck: Boolean = false) {
fun setVideoOverview(video: IPlatformVideo, fetch: Boolean = true, resumeSeconds: Long = 0) {
Logger.i(TAG, "setVideoOverview")
if(!bypassSameVideoCheck && this.video?.url == video.url)
if(this.video?.url == video.url)
return;
//Loop workaround
if(bypassSameVideoCheck && this.video?.url == video.url && StatePlayer.instance.loopVideo) {
_player.seekTo(0);
return;
}
val cachedVideo = StateDownloads.instance.getCachedVideo(video.id);
if(cachedVideo != null) {
@@ -1076,7 +1048,7 @@ class VideoDetailView : ConstraintLayout {
}
this.video = null;
cleanupPlaybackTracker();
this._playbackTracker = null;
_searchVideo = video;
_videoResumePositionMilliseconds = resumeSeconds * 1000;
setLastPositionMilliseconds(_videoResumePositionMilliseconds, false);
@@ -1211,7 +1183,7 @@ class VideoDetailView : ConstraintLayout {
}
this.videoLocal = videoLocal;
this.video = video;
cleanupPlaybackTracker();
this._playbackTracker = null;
if(video is JSVideoDetails) {
val me = this;
@@ -1527,22 +1499,6 @@ class VideoDetailView : ConstraintLayout {
}
}
fun cleanupPlaybackTracker(){
val tracker = _playbackTracker;
if(tracker != null) {
_playbackTracker = null;
fragment.lifecycleScope.launch(Dispatchers.IO) {
Logger.i(TAG, "Cleaning up old playback tracker");
try {
tracker.onConcluded();
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to cleanup playback tracker", ex);
}
}
}
}
//Source Loads
private fun loadCurrentVideo(resumePositionMs: Long = 0) {
_didStop = false;
@@ -1707,7 +1663,7 @@ class VideoDetailView : ConstraintLayout {
Logger.i(TAG, "prevVideo")
val next = StatePlayer.instance.prevQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
if(next != null) {
setVideoOverview(next, true, 0, true);
setVideoOverview(next);
}
}
@@ -1717,7 +1673,7 @@ class VideoDetailView : ConstraintLayout {
if(next == null && forceLoop)
next = StatePlayer.instance.restartQueue();
if(next != null) {
setVideoOverview(next, true, 0, true);
setVideoOverview(next);
return true;
}
else
@@ -2037,7 +1993,7 @@ class VideoDetailView : ConstraintLayout {
private fun fetchVideo() {
Logger.i(TAG, "fetchVideo")
video = null;
cleanupPlaybackTracker();
_playbackTracker = null;
val url = _url;
if (url != null && url.isNotBlank()) {
@@ -2252,11 +2208,11 @@ class VideoDetailView : ConstraintLayout {
videoSourceHeight = 9;
}
val aspectRatio = videoSourceWidth.toDouble() / videoSourceHeight;
if(aspectRatio > 2.38) {
if(aspectRatio > 3) {
videoSourceWidth = 16;
videoSourceHeight = 9;
}
else if(aspectRatio < 0.43) {
else if(aspectRatio < 0.3) {
videoSourceHeight = 16;
videoSourceWidth = 9;
}
@@ -2497,7 +2453,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, null, fragment);
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, ::fetchVideo);
} 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)
@@ -2533,7 +2489,7 @@ class VideoDetailView : ConstraintLayout {
_retryJob = null;
_liveTryJob?.cancel();
_liveTryJob = null;
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, ::fetchVideo, null, fragment);
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, ::fetchVideo);
}
}
.exception<Throwable> {
@@ -2545,7 +2501,7 @@ class VideoDetailView : ConstraintLayout {
_retryJob = null;
_liveTryJob?.cancel();
_liveTryJob = null;
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo, null, fragment);
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo);
}
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
@@ -2606,7 +2562,7 @@ class VideoDetailView : ConstraintLayout {
}
else
withContext(Dispatchers.Main) {
setVideoDetails(videoDetail, false);
setVideoDetails(videoDetail, true);
_liveTryJob = null;
}
}
@@ -1,7 +1,6 @@
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
@@ -9,17 +8,10 @@ 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 {
@@ -93,44 +85,6 @@ 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 ?: "";
@@ -5,17 +5,10 @@ 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;
@@ -35,11 +28,6 @@ class WatchLaterFragment : MainFragment() {
return view;
}
override fun onResume() {
super.onResume()
_view?.onResume();
}
override fun onDestroyMainView() {
super.onDestroyMainView();
_view = null;
@@ -57,34 +45,6 @@ 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() {
@@ -116,7 +76,6 @@ class WatchLaterFragment : MainFragment() {
}
companion object {
val TAG = "WatchLaterFragment";
fun newInstance() = WatchLaterFragment().apply {}
}
}
@@ -2,17 +2,11 @@ package com.futo.platformplayer.helpers
class FileHelper {
companion object {
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
val allowedCharacters = HashSet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-.".toCharArray().toList());
fun String.sanitizeFileName(): String {
return this.filter { allowedCharacters.contains(it) };
}
}
}
@@ -13,7 +13,6 @@ import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
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.IHLSManifestAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
@@ -45,7 +44,7 @@ class VideoHelper {
}
fun isDownloadable(source: IVideoSource) = source is IVideoUrlSource || source is IHLSManifestSource;
fun isDownloadable(source: IAudioSource) = (source is IAudioUrlSource || source is IHLSManifestAudioSource) && source !is IAudioUrlWidevineSource
fun isDownloadable(source: IAudioSource) = source is IAudioUrlSource || source is IHLSManifestAudioSource;
fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers);
fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? {
@@ -43,8 +43,7 @@ class FileLogConsumer : ILogConsumer, Closeable {
}
while (_linesToWrite.isNotEmpty()) {
val todo = _linesToWrite.remove()
_writer?.appendLine(todo);
_writer?.appendLine(_linesToWrite.remove());
}
_writer?.flush();
@@ -86,7 +85,7 @@ class FileLogConsumer : ILogConsumer, Closeable {
_running = false;
_writer?.close();
_writer = null;
//_logThread?.join();
_logThread?.join();
_logThread = null;
}
@@ -30,7 +30,7 @@ class HistoryVideo {
}
companion object {
fun fromReconString(str: String, resolve: ((url: String)->SerializedPlatformVideo?)? = null): HistoryVideo {
fun fromReconString(str: String, resolve: ((url: String)->SerializedPlatformVideo)? = null): HistoryVideo {
var index = str.indexOf("|||");
if(index < 0) throw IllegalArgumentException("Invalid history string: " + str);
val url = str.substring(0, index);
@@ -1,11 +0,0 @@
package com.futo.platformplayer.models
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import kotlinx.serialization.Serializable
@Serializable
class ImportCache(
var videos: List<SerializedPlatformVideo>? = null,
var channels: List<SerializedChannel>? = null
);
@@ -40,9 +40,6 @@ 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;
@@ -129,7 +126,6 @@ class Subscription {
else if(lastVideo.year > 3000)
lastVideo = OffsetDateTime.MIN;
lastVideoUpdate = OffsetDateTime.now();
lastPeekVideo = OffsetDateTime.MIN;
}
ResultCapabilities.TYPE_MIXED -> {
uploadInterval = interval;
@@ -138,7 +134,6 @@ class Subscription {
else if(lastVideo.year > 3000)
lastVideo = OffsetDateTime.MIN;
lastVideoUpdate = OffsetDateTime.now();
lastPeekVideo = OffsetDateTime.MIN;
}
ResultCapabilities.TYPE_SUBSCRIPTIONS -> {
uploadInterval = interval;
@@ -316,6 +316,7 @@ 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;

Some files were not shown because too many files have changed in this diff Show More