mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 13:02:39 +02:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 414b6e24d2 | |||
| 9499afd815 | |||
| e7aca5cd25 | |||
| 80a6a8ac9f | |||
| c3428a695f | |||
| 1a9665b5c6 | |||
| ebb4693425 | |||
| 4f09f48ace | |||
| a0d6ff912b | |||
| a345da0feb | |||
| fc5a8d9531 | |||
| 7353edb058 | |||
| 2a7c0a5c79 | |||
| 4cf3aabe89 | |||
| ef284ba51d | |||
| 5edd389e84 | |||
| 309332ac9c | |||
| 035d19f581 | |||
| 72bb43f934 | |||
| 447ed6bf21 | |||
| db1bcfcc6b | |||
| 1ccae84933 | |||
| 152b9b23cd | |||
| a3070d8d08 | |||
| aceab7b476 | |||
| 5f1c0209a8 | |||
| 819e81b7a6 | |||
| 8193234c2f | |||
| 6263a31f41 | |||
| 481a0cda99 | |||
| b39b89e908 | |||
| ce0f98055f | |||
| 3dddf68766 | |||
| 88d687f26e | |||
| d44df42727 | |||
| 88c8dbcb7c | |||
| b4fddbe26a | |||
| ab6d7669d7 |
@@ -64,3 +64,9 @@
|
||||
[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
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# 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
@@ -0,0 +1,43 @@
|
||||
# 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 Licensor’s 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.
|
||||
@@ -0,0 +1,15 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_287_2206)">
|
||||
<path d="M22.0557 38.25L43.1117 6H1L22.0557 38.25Z" fill="url(#paint0_linear_287_2206)"/>
|
||||
<path d="M6 28.2444C6.85811 27.3291 8.98625 25.2353 10.6338 24.1827C12.2814 23.13 14.257 20.1209 15.0388 18.7479C17.4224 15.2392 22.7618 7.91286 25.0501 6.67716C25.462 6.35678 26.0608 5.85718 26.3087 5.64745C27.1668 3.7405 30.0844 0.498738 34.8898 2.78706C35.3017 2.64974 36.32 2.61542 36.7777 2.61542C36.4153 2.86334 35.6564 3.58795 35.5191 4.50328C35.153 7.02039 33.7647 8.48874 33.1164 8.90825C32.6587 11.8259 32.0294 14.4002 30.6564 15.3155L31.915 17.5466C33.8029 19.5489 37.7159 23.8737 38.2649 25.1552C36.4344 24.5603 35.2521 23.992 34.8898 23.7822L38.2649 28.416C36.2818 28.2635 31.8235 26.9744 29.8556 23.0385C30.6336 25.1438 31.4001 27.7677 31.6862 28.8165C30.6183 27.9393 28.3224 25.3955 27.6816 22.2376C27.8647 25.304 27.8342 27.4816 27.7961 28.1872C27.2812 27.7105 26.0913 26.2307 25.4505 24.1255V27.6723C24.6821 26.604 23.1363 24.0104 22.9967 22.0533C23.1255 24.2716 23.047 25.3115 22.9906 25.5556L20.0731 22.8097C19.2912 23.2292 17.1898 24.1827 15.0388 24.6403C13.5743 25.876 11.797 28.969 11.0915 30.3611V28.5877L9.14643 30.5327L9.83291 28.4733L8.57433 29.5602C8.28828 29.7318 7.62468 30.0751 7.25857 30.0751C7.39585 29.7547 7.65904 29.4076 7.77345 29.2741L6.11441 29.9034C6.3051 29.3504 6.90388 28.13 7.77345 27.6723C6.58351 28.13 6.09536 28.2444 6 28.2444Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_287_2206" x1="22.0557" y1="38.25" x2="22.0557" y2="-4.75" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#01D6E6"/>
|
||||
<stop offset="1" stop-color="#0182E7"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_287_2206">
|
||||
<rect width="44" height="44" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -262,6 +262,17 @@ function getDevLogs(lastIndex, cb) {
|
||||
.then(x=>x.json())
|
||||
.then(y=> cb && cb(y));
|
||||
}
|
||||
function getDevHttpExchanges(cb) {
|
||||
fetch("/plugin/getDevHttpExchanges", {
|
||||
timeout: 1000
|
||||
})
|
||||
.then(x=>x.json())
|
||||
.then(y=> cb && cb(y));
|
||||
}
|
||||
function setDevHttpProxy(url, port) {
|
||||
return fetch("/dev/setDevProxy?url=" + encodeURIComponent(url) + "&port=" + port)
|
||||
.then(x=>x.json());
|
||||
}
|
||||
function sendFakeDevLog(devId, msg) {
|
||||
return syncGET("/plugin/fakeDevLog?devId=" + devId + "&msg=" + msg, {});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
<!--<link href="./dependencies/vuetify.min.css" rel="stylesheet">-->
|
||||
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.7.1/dist/vuetify.min.css" rel="stylesheet">
|
||||
|
||||
<title>DevPortal</title>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.svg">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
|
||||
|
||||
<style>
|
||||
@@ -150,7 +153,7 @@
|
||||
.pastPluginUrl {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 500px;
|
||||
width: 700px;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
@@ -160,13 +163,122 @@
|
||||
box-shadow: 0px 1px 2px #131313;
|
||||
font-weight: lighter;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.pastPluginUrl .deleteButton {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
height: 100%;
|
||||
width: 30px;
|
||||
top: 0px;
|
||||
padding-top: 2px;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
transform: scaleX(1.5);
|
||||
}
|
||||
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
#cloakLoader {
|
||||
display: block;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: black;
|
||||
color: white;
|
||||
padding-top: 50px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.httpContainer {
|
||||
position: relative;
|
||||
}
|
||||
.httpLine {
|
||||
}
|
||||
.httpLine .request {
|
||||
height: 50px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.httpLine .request .status {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
width: 40px;
|
||||
top: 10px;
|
||||
padding: 5px;
|
||||
background-color: #333;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.httpLine .request .status.error {
|
||||
background-color: #880000;
|
||||
}
|
||||
.httpLine .request .status.success {
|
||||
background-color: #008800;
|
||||
}
|
||||
.httpLine .request .status.warn {
|
||||
background-color: #803500;
|
||||
}
|
||||
.httpLine .request .method {
|
||||
position: absolute;
|
||||
left: 55px;
|
||||
top: 10px;
|
||||
padding: 5px;
|
||||
background-color: #333;
|
||||
border-radius: 5px;
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
.httpLine .request .url {
|
||||
position: absolute;
|
||||
left: 110px;
|
||||
top: 10px;
|
||||
padding: 5px;
|
||||
background-color: #333;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.httpLine .response {
|
||||
background-color: #111;
|
||||
margin-left: 55px;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
}
|
||||
.httpLine .response .body{
|
||||
white-space: pre-wrap;
|
||||
font-family: monospace;
|
||||
background-color: black;
|
||||
padding: 10px;
|
||||
}
|
||||
.httpLine .response .headers {
|
||||
margin: 10px;
|
||||
}
|
||||
.httpLine .response .headers .key {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
color: #FFF;
|
||||
}
|
||||
.httpLine .response .headers .value {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
color: #AAA;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<v-app>
|
||||
<v-main>
|
||||
<div v-cloak id="cloakLoader" v-if="!page">
|
||||
<h2>Loading..</h2>
|
||||
First load may take longer
|
||||
</div>
|
||||
<v-main v-cloak>
|
||||
<div id="topMenu">
|
||||
<div style="height: 100%; display: inline-block; padding-left: 10px; padding-right: 20px;">
|
||||
<img src="./dependencies/FutoMainLogo.svg"
|
||||
@@ -250,10 +362,13 @@
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="pastPluginUrls" style="margin-top: 60px;">
|
||||
<div v-if="pastPluginUrls" style="margin-top: 60px; margin-left: 25px;">
|
||||
<h2 style="font-weight: lighter; text-align: center;">Past Plugins</h2>
|
||||
<div class="pastPluginUrl" v-for="pastPluginUrl in pastPluginUrls" @click="this.Plugin.newPluginUrl = pastPluginUrl; loadPlugin(pastPluginUrl)">
|
||||
{{pastPluginUrl}}
|
||||
<div class="deleteButton" @click="(ev)=>{ev.stopPropagation(); deletePastPlugin(pastPluginUrl)}">
|
||||
X
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -505,7 +620,62 @@
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn>Clear</v-btn>
|
||||
<v-btn @click="Integration.logs = []">Clear</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-card style="margin: 20px;" v-if="Plugin.currentPlugin && Integration.httpExchanges">
|
||||
<v-card-title>
|
||||
Http Logs
|
||||
</v-card-title>
|
||||
</v-card-header>
|
||||
<v-card-text>
|
||||
<div style="position: absolute; top: 0px; right: 15px;">
|
||||
<v-checkbox v-model="Integration.showHttpRequests" label="Show Http Requests"></v-checkbox>
|
||||
</div>
|
||||
<div class="httpContainer" v-if="Integration.showHttpRequests">
|
||||
<div class="httpLine" v-for="exchange of Integration.httpExchanges">
|
||||
<div class="request" @click="toggleHttpExchange(exchange)">
|
||||
<div :class="[{ success: exchange.response.status < 300, warn: exchange.response.status >= 300 && exchange.response.status < 400, error: exchange.response.status >= 400 }, 'status']">
|
||||
{{exchange.response.status}}
|
||||
</div>
|
||||
<div class="method">
|
||||
{{exchange.request.method}}
|
||||
</div>
|
||||
<div class="url">
|
||||
{{exchange.request.url}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="response" v-if="exchange.response.show">
|
||||
<h2>Request Headers</h2>
|
||||
<div class="headers">
|
||||
<div class="header" v-for="(headerValue, header) in exchange.request.headers">
|
||||
<div class="key">
|
||||
{{header}}
|
||||
</div>
|
||||
<div class="value">
|
||||
{{headerValue}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Response</h2>
|
||||
<div class="headers">
|
||||
<div class="header" v-for="(headerValue, header) in exchange.response.headers">
|
||||
<div class="key">
|
||||
{{header}}
|
||||
</div>
|
||||
<div class="value">
|
||||
{{headerValue}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">{{exchange.response.body}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="Integration.showHttpRequests" @click="Integration.httpExchanges = []">Clear</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
@@ -562,7 +732,9 @@
|
||||
lastLogIndex: -1,
|
||||
lastLogDevID: "",
|
||||
logs: [],
|
||||
lastInjectTime: ""
|
||||
httpExchanges: [],
|
||||
lastInjectTime: "",
|
||||
showHttpRequests: false
|
||||
},
|
||||
Plugin: {
|
||||
loadUsingTag: false,
|
||||
@@ -646,6 +818,16 @@
|
||||
});
|
||||
}
|
||||
});
|
||||
if(this.Integration.showHttpRequests) {
|
||||
getDevHttpExchanges((exchanges)=>{
|
||||
Vue.nextTick(()=>{
|
||||
for(i = 0; i < exchanges.length; i++) {
|
||||
exchanges[i].response.show = false;
|
||||
this.Integration.httpExchanges.unshift(exchanges[i]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
catch(ex) {
|
||||
console.error("Failed update", ex);
|
||||
@@ -687,6 +869,12 @@
|
||||
this.reloadPlugin();
|
||||
});
|
||||
},
|
||||
deletePastPlugin(url) {
|
||||
let currentPastPlugins = this.pastPluginUrls;
|
||||
currentPastPlugins = currentPastPlugins.filter(x=>x.toLowerCase() != url.toLowerCase());
|
||||
this.pastPluginUrls = currentPastPlugins;
|
||||
localStorage.setItem("pastPlugins", JSON.stringify(currentPastPlugins));
|
||||
},
|
||||
loginTestPlugin() {
|
||||
pluginLoginTestPlugin();
|
||||
setTimeout(()=>{
|
||||
@@ -922,6 +1110,9 @@
|
||||
},
|
||||
showTestResults(results) {
|
||||
|
||||
},
|
||||
toggleHttpExchange(exchange) {
|
||||
exchange.response.show = !exchange.response.show;
|
||||
},
|
||||
copyClipboard(cpy) {
|
||||
if(navigator.clipboard)
|
||||
|
||||
+1
-1
@@ -127,7 +127,7 @@ declare class PlatformVideoDetails extends PlatformVideo {
|
||||
}
|
||||
|
||||
declare interface PlatformPostDef extends PlatformContentDef {
|
||||
thumbnails: string[],
|
||||
thumbnails: Thumbnails[],
|
||||
images: string[],
|
||||
description: string
|
||||
}
|
||||
|
||||
@@ -357,6 +357,15 @@ class AudioUrlSource {
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
class AudioUrlWidevineSource extends AudioUrlSource {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
this.plugin_type = "AudioUrlWidevineSource";
|
||||
|
||||
this.bearerToken = obj.bearerToken;
|
||||
this.licenseUri = obj.licenseUri;
|
||||
}
|
||||
}
|
||||
class AudioUrlRangeSource extends AudioUrlSource {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
|
||||
@@ -50,12 +50,9 @@ fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
||||
|
||||
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
|
||||
if (!systemState.servers.contains(PolycentricCache.STAGING_SERVER)) {
|
||||
removeServer(PolycentricCache.STAGING_SERVER)
|
||||
}
|
||||
|
||||
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
|
||||
removeServer(PolycentricCache.SERVER)
|
||||
Logger.w("Backfill", "Polycentric prod server not added, adding it.")
|
||||
addServer(PolycentricCache.SERVER)
|
||||
}
|
||||
|
||||
val exceptions = fullyBackfillServers()
|
||||
|
||||
@@ -33,6 +33,7 @@ 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
|
||||
@@ -493,6 +494,17 @@ 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)
|
||||
@@ -503,6 +515,8 @@ class SettingsDev : FragmentedStorageFileJson() {
|
||||
var channelCacheStartupCount = StateCache.instance.channelCacheStartupCount;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//region BOILERPLATE
|
||||
override fun encode(): String {
|
||||
return Json.encodeToString(this);
|
||||
|
||||
@@ -18,6 +18,7 @@ import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
|
||||
@@ -31,12 +32,17 @@ import com.futo.platformplayer.dialogs.ConnectedCastingDialog
|
||||
import com.futo.platformplayer.dialogs.ImportDialog
|
||||
import com.futo.platformplayer.dialogs.ImportOptionsDialog
|
||||
import com.futo.platformplayer.dialogs.MigrateDialog
|
||||
import com.futo.platformplayer.dialogs.PluginUpdateDialog
|
||||
import com.futo.platformplayer.dialogs.ProgressDialog
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import com.futo.platformplayer.views.ToastView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -184,6 +190,14 @@ class UIDialogs {
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
fun showPluginUpdateDialog(context: Context, oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig) {
|
||||
val dialog = PluginUpdateDialog(context, oldConfig, newConfig);
|
||||
registerDialogOpened(dialog);
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
|
||||
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
|
||||
val builder = AlertDialog.Builder(context);
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
||||
@@ -269,22 +283,48 @@ class UIDialogs {
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
}
|
||||
fun showGeneralRetryErrorDialog(context: Context, msg: String, ex: Throwable? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null) {
|
||||
fun showGeneralRetryErrorDialog(context: Context, msg: String, ex: Throwable? = null, retryAction: (() -> Unit)? = null, closeAction: (() -> Unit)? = null, mainFragment: MainFragment? = null) {
|
||||
val pluginConfig = if(ex is PluginException) ex.config else null;
|
||||
val pluginInfo = if(ex is PluginException)
|
||||
"\nPlugin [${ex.config.name}]" else "";
|
||||
showDialog(context,
|
||||
R.drawable.ic_error_pred,
|
||||
"${msg}${pluginInfo}",
|
||||
(if(ex != null ) "${ex.message}" else ""),
|
||||
if(ex is PluginException) ex.code else null,
|
||||
0,
|
||||
UIDialogs.Action(context.getString(R.string.retry), {
|
||||
retryAction?.invoke();
|
||||
}, UIDialogs.ActionStyle.PRIMARY),
|
||||
UIDialogs.Action(context.getString(R.string.close), {
|
||||
closeAction?.invoke()
|
||||
}, UIDialogs.ActionStyle.NONE)
|
||||
);
|
||||
|
||||
var exMsg = if(ex != null ) "${ex.message}" else "";
|
||||
if(pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
|
||||
exMsg += "\n\nAn update is available"
|
||||
|
||||
if(mainFragment != null && pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
|
||||
showDialog(context,
|
||||
R.drawable.ic_error_pred,
|
||||
"${msg}${pluginInfo}",
|
||||
exMsg,
|
||||
if(ex is PluginException) ex.code else null,
|
||||
1,
|
||||
UIDialogs.Action(context.getString(R.string.update), {
|
||||
mainFragment.navigate<SourceDetailFragment>(SourceDetailFragment.UpdatePluginAction(pluginConfig));
|
||||
if(mainFragment is VideoDetailFragment)
|
||||
mainFragment.minimizeVideoDetail();
|
||||
}, UIDialogs.ActionStyle.ACCENT),
|
||||
UIDialogs.Action(context.getString(R.string.close), {
|
||||
closeAction?.invoke()
|
||||
}, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action(context.getString(R.string.retry), {
|
||||
retryAction?.invoke();
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
else
|
||||
showDialog(context,
|
||||
R.drawable.ic_error_pred,
|
||||
"${msg}${pluginInfo}",
|
||||
exMsg,
|
||||
if(ex is PluginException) ex.code else null,
|
||||
0,
|
||||
UIDialogs.Action(context.getString(R.string.close), {
|
||||
closeAction?.invoke()
|
||||
}, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action(context.getString(R.string.retry), {
|
||||
retryAction?.invoke();
|
||||
}, UIDialogs.ActionStyle.PRIMARY)
|
||||
);
|
||||
}
|
||||
|
||||
fun showSingleButtonDialog(context: Context, icon: Int, text: String, buttonText: String, action: (() -> Unit)) {
|
||||
|
||||
@@ -224,7 +224,7 @@ class AddSourceActivity : AppCompatActivity() {
|
||||
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
|
||||
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
||||
if(it) {
|
||||
StatePlatform.instance.clearUpdateAvailable(config)
|
||||
StatePlugins.instance.clearUpdateAvailable(config)
|
||||
if(isNew)
|
||||
lifecycleScope.launch {
|
||||
StatePlatform.instance.enableClient(listOf(config.id));
|
||||
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
@@ -19,7 +20,12 @@ 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.*
|
||||
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.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.common.BitMatrix
|
||||
@@ -64,11 +70,8 @@ class PolycentricBackupActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
_buttonShare.onClick.subscribe {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain";
|
||||
putExtra(Intent.EXTRA_TEXT, _exportBundle);
|
||||
}
|
||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_text)));
|
||||
val shareIntent = Intent(Intent.ACTION_VIEW, Uri.parse(_exportBundle))
|
||||
startActivity(Intent.createChooser(shareIntent, "Share ID"));
|
||||
};
|
||||
|
||||
_buttonCopy.onClick.subscribe {
|
||||
|
||||
+2
-1
@@ -12,6 +12,7 @@ import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
@@ -77,7 +78,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||
}
|
||||
|
||||
processHandle.addServer("https://srv1-stg.polycentric.io");
|
||||
processHandle.addServer(PolycentricCache.SERVER);
|
||||
processHandle.setUsername(username);
|
||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||
} catch (e: Throwable) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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
|
||||
@@ -13,6 +15,11 @@ 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 {
|
||||
@@ -25,8 +32,29 @@ 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));
|
||||
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
@@ -60,6 +61,8 @@ class CachedPlatformClient : IPlatformClient {
|
||||
filters: Map<String, List<String>>?
|
||||
): IPager<IPlatformContent> = _client.getChannelContents(channelUrl);
|
||||
|
||||
override fun getChannelPlaylists(channelUrl: String): IPager<IPlatformPlaylist> = _client.getChannelPlaylists(channelUrl);
|
||||
|
||||
override fun getPeekChannelTypes(): List<String> = _client.getPeekChannelTypes();
|
||||
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = _client.peekChannelContents(channelUrl, type);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
@@ -93,6 +94,11 @@ interface IPlatformClient {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -18,7 +18,8 @@ data class PlatformClientCapabilities(
|
||||
val hasGetLiveEvents: Boolean = false,
|
||||
val hasGetLiveChatWindow: Boolean = false,
|
||||
val hasGetContentChapters: Boolean = false,
|
||||
val hasPeekChannelContents: Boolean = false
|
||||
val hasPeekChannelContents: Boolean = false,
|
||||
val hasGetChannelPlaylists: Boolean = false
|
||||
) {
|
||||
|
||||
}
|
||||
+1
@@ -3,4 +3,5 @@ package com.futo.platformplayer.api.media.models.live
|
||||
interface ILiveChatWindowDescriptor {
|
||||
val url: String;
|
||||
val removeElements: List<String>;
|
||||
val removeElementsInterval: List<String>;
|
||||
}
|
||||
+1
@@ -7,4 +7,5 @@ interface IPlaybackTracker {
|
||||
|
||||
fun onInit(seconds: Double);
|
||||
fun onProgress(seconds: Double, isPlaying: Boolean);
|
||||
fun onConcluded();
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
interface IAudioUrlWidevineSource : IAudioUrlSource {
|
||||
val bearerToken: String
|
||||
val licenseUri: String
|
||||
}
|
||||
@@ -8,6 +8,8 @@ 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 {
|
||||
@@ -115,7 +117,7 @@ class DevJSClient : JSClient {
|
||||
|
||||
//Video
|
||||
override fun isContentDetailsUrl(url: String): Boolean {
|
||||
return StateDeveloper.instance.handleDevCall(devID, "isVideoDetailsUrl"){
|
||||
return StateDeveloper.instance.handleDevCall(devID, "isVideoDetailsUrl(${Json.encodeToString(url)})"){
|
||||
super.isContentDetailsUrl(url);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ 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
|
||||
@@ -39,6 +40,7 @@ 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
|
||||
@@ -46,6 +48,7 @@ import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -57,6 +60,7 @@ 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
|
||||
@@ -229,7 +233,8 @@ open class JSClient : IPlatformClient {
|
||||
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
|
||||
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
|
||||
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
||||
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false
|
||||
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false,
|
||||
hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -400,6 +405,16 @@ open class JSClient : IPlatformClient {
|
||||
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)
|
||||
@@ -421,6 +436,7 @@ open class JSClient : IPlatformClient {
|
||||
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")
|
||||
|
||||
+45
-1
@@ -46,7 +46,8 @@ class SourcePluginConfig(
|
||||
var enableInHome: Boolean = true,
|
||||
var supportedClaimTypes: List<Int> = listOf(),
|
||||
var primaryClaimFieldType: Int? = null,
|
||||
var developerSubmitUrl: String? = null
|
||||
var developerSubmitUrl: String? = null,
|
||||
var allowAllHttpHeaderAccess: Boolean = false,
|
||||
) : IV8PluginConfig {
|
||||
|
||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||
@@ -80,6 +81,44 @@ class SourcePluginConfig(
|
||||
return _allowUrlsLowerVal!!;
|
||||
};
|
||||
|
||||
fun isLowRiskUpdate(oldScript: String, newConfig: SourcePluginConfig, newScript: String): Boolean{
|
||||
//New allow header access
|
||||
if(!allowAllHttpHeaderAccess && newConfig.allowAllHttpHeaderAccess)
|
||||
return false;
|
||||
|
||||
//All urls should already be allowed
|
||||
for(url in newConfig.allowUrls) {
|
||||
if(!allowUrls.contains(url))
|
||||
return false;
|
||||
}
|
||||
//All packages should already be allowed
|
||||
for(pack in newConfig.packages) {
|
||||
if(!packages.contains(pack))
|
||||
return false;
|
||||
}
|
||||
//Developer Submit Url should be same or empty
|
||||
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
|
||||
return false;
|
||||
|
||||
//Should have a public key
|
||||
if(scriptPublicKey.isNullOrEmpty() || scriptSignature.isNullOrEmpty())
|
||||
return false;
|
||||
|
||||
//Should be same public key
|
||||
if(scriptPublicKey != newConfig.scriptPublicKey)
|
||||
return false;
|
||||
|
||||
//Old signature should be valid
|
||||
if(!validate(oldScript))
|
||||
return false;
|
||||
|
||||
//New signature should be valid
|
||||
if(!newConfig.validate(newScript))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fun getWarnings(scriptToCheck: String? = null) : List<Pair<String,String>> {
|
||||
val list = mutableListOf<Pair<String,String>>();
|
||||
|
||||
@@ -108,6 +147,11 @@ class SourcePluginConfig(
|
||||
list.add(Pair(
|
||||
"Unrestricted Web Access",
|
||||
"This plugin requires access to all URLs, this may include malicious URLs."));
|
||||
if(allowAllHttpHeaderAccess)
|
||||
list.add(Pair(
|
||||
"Unrestricted Http Header access",
|
||||
"Allows this plugin to access all headers (including cookies and authorization headers) for unauthenticated requests."
|
||||
))
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
+3
-1
@@ -91,8 +91,10 @@ class SourcePluginDescriptor {
|
||||
@Serializable
|
||||
class AppPluginSettings {
|
||||
|
||||
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, 0)
|
||||
@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();
|
||||
|
||||
+27
-1
@@ -2,14 +2,22 @@ package com.futo.platformplayer.api.media.platforms.js.internal
|
||||
|
||||
import android.net.Uri
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequest
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
|
||||
import com.futo.platformplayer.developer.DeveloperEndpoints
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.matchesDomain
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
import com.google.common.net.MediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okio.GzipSource
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.util.UUID
|
||||
|
||||
class JSHttpClient : ManagedHttpClient {
|
||||
@@ -28,7 +36,15 @@ class JSHttpClient : ManagedHttpClient {
|
||||
private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
|
||||
private var _otherCookieMap: HashMap<String, HashMap<String, String>>;
|
||||
|
||||
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super() {
|
||||
constructor(jsClient: JSClient?, auth: SourceAuth? = null, captcha: SourceCaptchaData? = null, config: SourcePluginConfig? = null) : super(
|
||||
//Temporary ugly solution for DevPortal proxy support
|
||||
(if(jsClient?.config?.id == StateDeveloper.DEV_ID && StateDeveloper.instance.devProxy != null)
|
||||
OkHttpClient.Builder().proxy(Proxy(Proxy.Type.HTTP,
|
||||
InetSocketAddress(StateDeveloper.instance.devProxy!!.url, StateDeveloper.instance.devProxy!!.port)
|
||||
))
|
||||
else
|
||||
OkHttpClient.Builder())
|
||||
) {
|
||||
_jsClient = jsClient;
|
||||
_jsConfig = config;
|
||||
_auth = auth;
|
||||
@@ -201,6 +217,16 @@ class JSHttpClient : ManagedHttpClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(_jsClient is DevJSClient) {
|
||||
//val peekBody = resp.peekBody(1000 * 1000).string();
|
||||
StateDeveloper.instance.addDevHttpExchange(
|
||||
StateDeveloper.DevHttpExchange(
|
||||
StateDeveloper.DevHttpRequest(resp.request.method, resp.request.url.toString(), mapOf(*resp.request.headers.map { Pair(it.first, it.second) }.toTypedArray()), ""),
|
||||
StateDeveloper.DevHttpRequest("RESP", resp.request.url.toString(), mapOf(*resp.headers.map { Pair(it.first, it.second) }.toTypedArray()), "", resp.code)
|
||||
));
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ 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
|
||||
@@ -37,7 +38,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();
|
||||
links = HashMap(_channel.getOrDefault<Map<String, String>>(config, "links", contextName, mapOf()) ?: mapOf());
|
||||
}
|
||||
|
||||
override fun getContents(client: IPlatformClient): IPager<IPlatformContent> {
|
||||
|
||||
+2
-1
@@ -14,12 +14,13 @@ 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();
|
||||
}
|
||||
}
|
||||
+13
@@ -17,6 +17,8 @@ class JSPlaybackTracker: IPlaybackTracker {
|
||||
|
||||
private var _lastRequest: Long = Long.MIN_VALUE;
|
||||
|
||||
private val _hasOnConcluded: Boolean;
|
||||
|
||||
override var nextRequest: Int = 1000
|
||||
private set;
|
||||
|
||||
@@ -26,6 +28,7 @@ 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;
|
||||
@@ -59,6 +62,16 @@ 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);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
|
||||
override val bearerToken: String
|
||||
override val licenseUri: String
|
||||
|
||||
@Suppress("ConvertSecondaryConstructorToPrimary")
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
|
||||
val contextName = "JSAudioUrlWidevineSource"
|
||||
val config = plugin.config
|
||||
bearerToken = _obj.getOrThrow(config, "bearerToken", contextName)
|
||||
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val url = getAudioUrl()
|
||||
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration, bearerToken=$bearerToken, licenseUri=$licenseUri)"
|
||||
}
|
||||
}
|
||||
+2
@@ -66,6 +66,7 @@ abstract class JSSource {
|
||||
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
|
||||
const val TYPE_DASH = "DashSource";
|
||||
const val TYPE_HLS = "HLSSource";
|
||||
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
|
||||
|
||||
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
|
||||
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource {
|
||||
@@ -88,6 +89,7 @@ abstract class JSSource {
|
||||
return when(type) {
|
||||
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
|
||||
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
|
||||
TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(plugin, obj);
|
||||
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
|
||||
else -> throw NotImplementedError("Unknown type ${type}");
|
||||
}
|
||||
|
||||
@@ -116,14 +116,10 @@ class DeveloperEndpoints(private val context: Context) {
|
||||
}
|
||||
|
||||
//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) {
|
||||
@@ -448,6 +444,25 @@ class DeveloperEndpoints(private val context: Context) {
|
||||
context.respondCode(500, ex::class.simpleName + ":" + ex.message, "text/plain")
|
||||
}
|
||||
}
|
||||
@HttpGET("/dev/setDevProxy")
|
||||
fun devSetDevProxy(context: HttpContext) {
|
||||
try {
|
||||
val url = context.query.getOrDefault("url", "");
|
||||
val port = context.query.getOrDefault("port", "");
|
||||
if(url.isNullOrEmpty() || port.isNullOrEmpty() || port.toIntOrNull() == null)
|
||||
{
|
||||
StateDeveloper.instance.devProxy = null;
|
||||
context.respondCode(400);
|
||||
return;
|
||||
}
|
||||
StateDeveloper.instance.devProxy = StateDeveloper.DevProxySettings(url, port.toInt());
|
||||
context.respondCode(200, "true", "application/json");
|
||||
}
|
||||
catch(ex: Exception) {
|
||||
Logger.e("DeveloperEndpoints", ex.message, ex);
|
||||
context.respondCode(500, ex::class.simpleName + ":" + ex.message, "text/plain")
|
||||
}
|
||||
}
|
||||
|
||||
@HttpGET("/plugin/getDevLogs")
|
||||
fun pluginGetDevLogs(context: HttpContext) {
|
||||
@@ -459,6 +474,15 @@ class DeveloperEndpoints(private val context: Context) {
|
||||
context.respondCode(500, ex.message ?: "", "text/plain")
|
||||
}
|
||||
}
|
||||
@HttpGET("/plugin/getDevHttpExchanges")
|
||||
fun pluginGetDevExchanges(context: HttpContext) {
|
||||
try {
|
||||
context.respondJson(200, StateDeveloper.instance.getHttpExchangesAndClear());
|
||||
}
|
||||
catch(ex: Exception) {
|
||||
context.respondCode(500, ex.message ?: "", "text/plain")
|
||||
}
|
||||
}
|
||||
@HttpGET("/plugin/fakeDevLog")
|
||||
fun pluginFakeDevLog(context: HttpContext) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
package com.futo.platformplayer.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.media.MediaCas.PluginDescriptor
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.AddSourceActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
||||
import com.futo.platformplayer.assume
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.ImportCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class PluginUpdateDialog : AlertDialog {
|
||||
companion object {
|
||||
private val TAG = "PluginUpdateDialog";
|
||||
}
|
||||
private val _context: Context;
|
||||
|
||||
private lateinit var _buttonCancel1: Button;
|
||||
private lateinit var _buttonCancel2: Button;
|
||||
private lateinit var _buttonUpdate: LinearLayout;
|
||||
|
||||
private lateinit var _buttonOk: LinearLayout;
|
||||
private lateinit var _buttonInstall: LinearLayout;
|
||||
|
||||
private lateinit var _textPlugin: TextView;
|
||||
private lateinit var _textProgres: TextView;
|
||||
private lateinit var _textError: TextView;
|
||||
private lateinit var _textResult: TextView;
|
||||
|
||||
private lateinit var _uiChoiceTop: FrameLayout;
|
||||
private lateinit var _uiProgressTop: FrameLayout;
|
||||
private lateinit var _uiRiskTop: FrameLayout;
|
||||
|
||||
private lateinit var _uiChoiceBot: LinearLayout;
|
||||
private lateinit var _uiResultBot: LinearLayout;
|
||||
private lateinit var _uiRiskBot: LinearLayout;
|
||||
private lateinit var _uiProgressBot: LinearLayout;
|
||||
|
||||
private lateinit var _iconPlugin: ImageView;
|
||||
private lateinit var _updateSpinner: ImageView;
|
||||
|
||||
private var _isUpdating = false;
|
||||
|
||||
private val _oldConfig: SourcePluginConfig;
|
||||
private val _newConfig: SourcePluginConfig;
|
||||
|
||||
|
||||
constructor(context: Context, oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig): super(context) {
|
||||
_context = context;
|
||||
_oldConfig = oldConfig;
|
||||
_newConfig = newConfig;
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(LayoutInflater.from(context).inflate(R.layout.dialog_plugin_update, null));
|
||||
|
||||
_buttonCancel1 = findViewById(R.id.button_cancel_1);
|
||||
_buttonCancel2 = findViewById(R.id.button_cancel_2);
|
||||
_buttonUpdate = findViewById(R.id.button_update);
|
||||
|
||||
_buttonOk = findViewById(R.id.button_ok);
|
||||
_buttonInstall = findViewById(R.id.button_install);
|
||||
|
||||
_textPlugin = findViewById(R.id.text_plugin);
|
||||
_textProgres = findViewById(R.id.text_progress);
|
||||
_textError = findViewById(R.id.text_error);
|
||||
_textResult = findViewById(R.id.text_result);
|
||||
|
||||
_uiChoiceTop = findViewById(R.id.dialog_ui_choice_top);
|
||||
_uiProgressTop = findViewById(R.id.dialog_ui_progress_top);
|
||||
_uiRiskTop = findViewById(R.id.dialog_ui_risk_top);
|
||||
|
||||
_uiChoiceBot = findViewById(R.id.dialog_ui_bottom_choice);
|
||||
_uiResultBot = findViewById(R.id.dialog_ui_bottom_result);
|
||||
_uiRiskBot = findViewById(R.id.dialog_ui_bottom_risk);
|
||||
_uiProgressBot = findViewById(R.id.dialog_ui_bottom_progress);
|
||||
|
||||
_updateSpinner = findViewById(R.id.update_spinner);
|
||||
_iconPlugin = findViewById(R.id.icon_plugin);
|
||||
|
||||
_buttonCancel1.setOnClickListener {
|
||||
dismiss();
|
||||
};
|
||||
_buttonCancel2.setOnClickListener {
|
||||
dismiss();
|
||||
};
|
||||
_buttonUpdate.setOnClickListener {
|
||||
if (_isUpdating)
|
||||
return@setOnClickListener;
|
||||
_isUpdating = true;
|
||||
update();
|
||||
};
|
||||
|
||||
Glide.with(_iconPlugin)
|
||||
.load(_oldConfig.absoluteIconUrl)
|
||||
.fallback(R.drawable.ic_sources)
|
||||
.into(_iconPlugin);
|
||||
_textPlugin.text = _oldConfig.name;
|
||||
|
||||
val descriptor = StatePlugins.instance.getPlugin(_oldConfig.id);
|
||||
if(descriptor != null) {
|
||||
if(descriptor.appSettings.automaticUpdate) {
|
||||
if (_isUpdating)
|
||||
return;
|
||||
_isUpdating = true;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
super.dismiss();
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
_uiChoiceTop.visibility = View.GONE;
|
||||
_uiRiskTop.visibility = View.GONE;
|
||||
_uiChoiceBot.visibility = View.GONE;
|
||||
_uiResultBot.visibility = View.GONE;
|
||||
_uiRiskBot.visibility = View.GONE;
|
||||
_uiProgressTop.visibility = View.VISIBLE;
|
||||
_uiProgressBot.visibility = View.VISIBLE;
|
||||
|
||||
setCancelable(false);
|
||||
setCanceledOnTouchOutside(false);
|
||||
|
||||
Logger.i(TAG, "Keep screen on set import")
|
||||
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
|
||||
_updateSpinner.drawable?.assume<Animatable>()?.start();
|
||||
|
||||
val scope = StateApp.instance.scopeOrNull;
|
||||
scope?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val client = ManagedHttpClient();
|
||||
val script = StatePlugins.instance.getScript(_oldConfig.id) ?: "";
|
||||
|
||||
val newScript = client.get(_newConfig.absoluteScriptUrl)?.body?.string();
|
||||
if(newScript.isNullOrEmpty())
|
||||
throw IllegalStateException("No script found");
|
||||
|
||||
if(_oldConfig.isLowRiskUpdate(script, _newConfig, newScript)){
|
||||
|
||||
StatePlugins.instance.installPluginBackground(context, StateApp.instance.scope, _newConfig, newScript,
|
||||
{ text: String, progress: Double ->
|
||||
_textProgres.setText(text);
|
||||
},
|
||||
{ ex ->
|
||||
if(ex == null) {
|
||||
StatePlugins.instance.clearUpdateAvailable(_newConfig);
|
||||
_iconPlugin.setImageResource(R.drawable.ic_check);
|
||||
_textError.visibility = View.GONE;
|
||||
_textResult.visibility = View.VISIBLE;
|
||||
}
|
||||
else {
|
||||
_iconPlugin.setImageResource(R.drawable.ic_error_pred);
|
||||
_textError.text = ex.message + "\n\nYou can retry inside the sources tab";
|
||||
_textError.visibility = View.VISIBLE;
|
||||
_textResult.visibility = View.GONE;
|
||||
}
|
||||
try {
|
||||
_buttonOk.setOnClickListener {
|
||||
dismiss();
|
||||
}
|
||||
_uiProgressTop.visibility = View.GONE;
|
||||
_uiProgressBot.visibility = View.GONE;
|
||||
_uiChoiceTop.visibility = View.VISIBLE;
|
||||
_uiResultBot.visibility = View.VISIBLE;
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to update UI.", e)
|
||||
} finally {
|
||||
Logger.i(TAG, "Keep screen on unset update")
|
||||
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
else {
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
_buttonInstall.setOnClickListener {
|
||||
dismiss();
|
||||
|
||||
val intent = Intent(_context, AddSourceActivity::class.java).apply {
|
||||
data = Uri.parse(_newConfig.sourceUrl)
|
||||
};
|
||||
|
||||
_context.startActivity(intent);
|
||||
}
|
||||
|
||||
_uiProgressTop.visibility = View.GONE;
|
||||
_uiProgressBot.visibility = View.GONE;
|
||||
_uiRiskTop.visibility = View.VISIBLE;
|
||||
_uiRiskBot.visibility = View.VISIBLE;
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to update UI.", e)
|
||||
} finally {
|
||||
Logger.i(TAG, "Keep screen on unset update")
|
||||
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to update.", e);
|
||||
withContext(Dispatchers.Main) {
|
||||
_buttonOk.setOnClickListener {
|
||||
dismiss();
|
||||
}
|
||||
_iconPlugin.setImageResource(R.drawable.ic_error_pred);
|
||||
_textResult.visibility = View.GONE;
|
||||
_uiProgressTop.visibility = View.GONE;
|
||||
_uiProgressBot.visibility = View.GONE;
|
||||
_uiChoiceTop.visibility = View.VISIBLE;
|
||||
_uiResultBot.visibility = View.VISIBLE;
|
||||
_textError.visibility = View.VISIBLE;
|
||||
_textError.text = e.message + "\n\nYou can retry inside the sources tab"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
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
|
||||
@@ -35,6 +37,19 @@ 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) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.caoccao.javet.interop.V8Runtime
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
@@ -190,6 +191,8 @@ class PackageHttp: V8Package {
|
||||
@Transient
|
||||
private val _client: ManagedHttpClient;
|
||||
|
||||
val parentConfig: IV8PluginConfig get() = _package._config;
|
||||
|
||||
@Transient
|
||||
private val _defaultHeaders = mutableMapOf<String, String>();
|
||||
@Transient
|
||||
@@ -208,28 +211,24 @@ class PackageHttp: V8Package {
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun setDefaultHeaders(defaultHeaders: Map<String, String>): PackageHttpClient {
|
||||
fun setDefaultHeaders(defaultHeaders: Map<String, String>) {
|
||||
for(pair in defaultHeaders)
|
||||
_defaultHeaders[pair.key] = pair.value;
|
||||
return this;
|
||||
}
|
||||
@V8Function
|
||||
fun setDoApplyCookies(apply: Boolean): PackageHttpClient {
|
||||
fun setDoApplyCookies(apply: Boolean) {
|
||||
if(_client is JSHttpClient)
|
||||
_client.doApplyCookies = apply;
|
||||
return this;
|
||||
}
|
||||
@V8Function
|
||||
fun setDoUpdateCookies(update: Boolean): PackageHttpClient {
|
||||
fun setDoUpdateCookies(update: Boolean) {
|
||||
if(_client is JSHttpClient)
|
||||
_client.doUpdateCookies = update;
|
||||
return this;
|
||||
}
|
||||
@V8Function
|
||||
fun setDoAllowNewCookies(allow: Boolean): PackageHttpClient {
|
||||
fun setDoAllowNewCookies(allow: Boolean) {
|
||||
if(_client is JSHttpClient)
|
||||
_client.doAllowNewCookies = allow;
|
||||
return this;
|
||||
}
|
||||
|
||||
@V8Function
|
||||
@@ -242,7 +241,8 @@ class PackageHttp: V8Package {
|
||||
val resp = client.requestMethod(method, url, headers);
|
||||
val responseBody = resp.body?.string();
|
||||
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -256,7 +256,8 @@ class PackageHttp: V8Package {
|
||||
val resp = client.requestMethod(method, url, body, headers);
|
||||
val responseBody = resp.body?.string();
|
||||
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -271,7 +272,8 @@ class PackageHttp: V8Package {
|
||||
val resp = client.get(url, headers);
|
||||
val responseBody = resp.body?.string();
|
||||
//logResponse("GET", url, resp.code, resp.headers, responseBody);
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -285,7 +287,8 @@ class PackageHttp: V8Package {
|
||||
val resp = client.post(url, body, headers);
|
||||
val responseBody = resp.body?.string();
|
||||
//logResponse("POST", url, resp.code, resp.headers, responseBody);
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -305,18 +308,31 @@ class PackageHttp: V8Package {
|
||||
}
|
||||
}
|
||||
|
||||
private fun sanitizeResponseHeaders(headers: Map<String, List<String>>?): Map<String, List<String>> {
|
||||
private fun sanitizeResponseHeaders(headers: Map<String, List<String>>?, onlyWhitelisted: Boolean = false): Map<String, List<String>> {
|
||||
val result = mutableMapOf<String, List<String>>()
|
||||
headers?.forEach { (header, values) ->
|
||||
val lowerCaseHeader = header.lowercase()
|
||||
if (WHITELISTED_RESPONSE_HEADERS.contains(lowerCaseHeader)) {
|
||||
result[lowerCaseHeader] = values
|
||||
if(onlyWhitelisted)
|
||||
headers?.forEach { (header, values) ->
|
||||
val lowerCaseHeader = header.lowercase()
|
||||
if (WHITELISTED_RESPONSE_HEADERS.contains(lowerCaseHeader)) {
|
||||
result[lowerCaseHeader] = values
|
||||
}
|
||||
}
|
||||
else {
|
||||
headers?.forEach { (header, values) ->
|
||||
val lowerCaseHeader = header.lowercase()
|
||||
if(lowerCaseHeader == "set-cookie") {
|
||||
result[lowerCaseHeader] = values.filter{
|
||||
!it.lowercase().contains("httponly")
|
||||
};
|
||||
}
|
||||
else
|
||||
result[lowerCaseHeader] = values;
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/*private fun logRequest(method: String, url: String, headers: Map<String, String> = HashMap(), body: String?) {
|
||||
private fun logRequest(method: String, url: String, headers: Map<String, String> = HashMap(), body: String?) {
|
||||
Logger.v(TAG) {
|
||||
val stringBuilder = StringBuilder();
|
||||
stringBuilder.appendLine("HTTP request (useAuth = )");
|
||||
@@ -333,7 +349,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) {
|
||||
@@ -413,7 +429,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 {
|
||||
@@ -422,8 +438,14 @@ class PackageHttp: V8Package {
|
||||
override fun open() {
|
||||
Logger.i(TAG, "Websocket opened: " + _url);
|
||||
_isOpen = true;
|
||||
if(hasOpen)
|
||||
_listeners?.invokeVoid("open", arrayOf<Any>());
|
||||
if(hasOpen) {
|
||||
try {
|
||||
_listeners?.invokeVoid("open", arrayOf<Any>());
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] open failed: " + ex.message, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun message(msg: String) {
|
||||
if(hasMessage) {
|
||||
@@ -435,18 +457,37 @@ class PackageHttp: V8Package {
|
||||
}
|
||||
override fun closing(code: Int, reason: String) {
|
||||
if(hasClosing)
|
||||
_listeners?.invokeVoid("closing", code, reason);
|
||||
{
|
||||
try {
|
||||
_listeners?.invokeVoid("closing", code, reason);
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closing failed: " + ex.message, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun closed(code: Int, reason: String) {
|
||||
_isOpen = false;
|
||||
if(hasClosed)
|
||||
_listeners?.invokeVoid("closed", code, reason);
|
||||
if(hasClosed) {
|
||||
try {
|
||||
_listeners?.invokeVoid("closed", code, reason);
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun failure(exception: Throwable) {
|
||||
_isOpen = false;
|
||||
Logger.e(TAG, "Websocket failure: ${exception.message} (${_url})", exception);
|
||||
if(hasFailure)
|
||||
_listeners?.invokeVoid("failure", exception.message);
|
||||
if(hasFailure) {
|
||||
try {
|
||||
_listeners?.invokeVoid("failure", exception.message);
|
||||
}
|
||||
catch(ex: Throwable){
|
||||
Logger.e(TAG, "Socket for [${_packageClient.parentConfig.name}] closed failed: " + ex.message, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -456,6 +497,16 @@ 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(
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
|
||||
|
||||
}
|
||||
|
||||
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
|
||||
override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
|
||||
_lastPolycentricProfile = polycentricProfile;
|
||||
|
||||
if (polycentricProfile == null) {
|
||||
|
||||
+1
-1
@@ -309,7 +309,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||
_adapterResults?.setLoading(loading);
|
||||
}
|
||||
|
||||
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
|
||||
override 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");
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ class ChannelListFragment : Fragment, IChannelTabFragment {
|
||||
}
|
||||
}
|
||||
|
||||
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
|
||||
override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
|
||||
_taskLoadChannel.cancel();
|
||||
_lastPolycentricProfile = polycentricProfile;
|
||||
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
||||
_lastChannel = channel;
|
||||
}
|
||||
|
||||
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
|
||||
override fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
|
||||
_lastPolycentricProfile = polycentricProfile
|
||||
if (polycentricProfile != null) {
|
||||
_supportView?.setPolycentricProfile(polycentricProfile)
|
||||
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -1,7 +1,11 @@
|
||||
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 setChannel(channel: IPlatformChannel)
|
||||
fun setPolycentricProfile(polycentricProfile: PolycentricProfile?) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+370
-292
@@ -15,8 +15,9 @@ 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,26 +28,32 @@ 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.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.dp
|
||||
import com.futo.platformplayer.fragment.channel.tab.IChannelTabFragment
|
||||
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.*
|
||||
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.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -55,459 +62,530 @@ 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(selectedTabIndex: Int) {
|
||||
_view?.selectTab(selectedTabIndex);
|
||||
fun selectTab(tab: ChannelTab) {
|
||||
_view?.selectTab(tab)
|
||||
}
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
class ChannelView : LinearLayout {
|
||||
private val _fragment: ChannelFragment;
|
||||
class ChannelView
|
||||
(fragment: ChannelFragment, inflater: LayoutInflater) : LinearLayout(inflater.context) {
|
||||
private val _fragment: ChannelFragment = fragment
|
||||
|
||||
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 _tabLayoutMediator: TabLayoutMediator;
|
||||
private var _buttonSubscribe: SubscribeButton;
|
||||
private var _buttonSubscriptionSettings: ImageButton;
|
||||
private var _tabs: TabLayout
|
||||
private var _viewPager: ViewPager2
|
||||
|
||||
private var _overlayContainer: FrameLayout;
|
||||
private var _overlay_loading: LinearLayout;
|
||||
private var _overlay_loading_spinner: ImageView;
|
||||
// private var _adapter: ChannelViewPagerAdapter;
|
||||
private var _tabLayoutMediator: TabLayoutMediator
|
||||
private var _buttonSubscribe: SubscribeButton
|
||||
private var _buttonSubscriptionSettings: ImageButton
|
||||
|
||||
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
|
||||
private var _overlayContainer: FrameLayout
|
||||
private var _overlayLoading: LinearLayout
|
||||
private var _overlayLoadingSpinner: ImageView
|
||||
|
||||
private var _isLoading: Boolean = false;
|
||||
private var _selectedTabIndex: Int = -1;
|
||||
private var _slideUpOverlay: SlideUpMenuOverlay? = null
|
||||
|
||||
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() {
|
||||
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
|
||||
super.onPageScrolled(position, positionOffset, positionOffsetPixels);
|
||||
//recalculate(position, positionOffset);
|
||||
}
|
||||
}
|
||||
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
|
||||
|
||||
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>
|
||||
|
||||
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); }
|
||||
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); }
|
||||
.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
|
||||
)
|
||||
}
|
||||
.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);
|
||||
|
||||
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)
|
||||
_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, _ ->
|
||||
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);
|
||||
when (v) {
|
||||
is IPlatformVideo -> {
|
||||
StatePlayer.instance.clearQueue()
|
||||
fragment.navigate<VideoDetailFragment>(v).maximizeVideoDetail()
|
||||
}
|
||||
|
||||
is IPlatformPlaylist -> {
|
||||
fragment.navigate<PlaylistFragment>(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);
|
||||
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) {
|
||||
StatePlaylists.instance.addToWatchLater(
|
||||
SerializedPlatformVideo.fromVideo(
|
||||
content
|
||||
)
|
||||
)
|
||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||
}
|
||||
}
|
||||
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) { 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;
|
||||
viewPager.adapter = adapter
|
||||
val tabLayoutMediator = TabLayoutMediator(
|
||||
tabs, viewPager, (viewPager.adapter as ChannelViewPagerAdapter)::getTabNames
|
||||
)
|
||||
tabLayoutMediator.attach()
|
||||
|
||||
_tabLayoutMediator = tabLayoutMediator
|
||||
_tabs = tabs
|
||||
_viewPager = viewPager
|
||||
if (_selectedTabIndex != -1) {
|
||||
selectTab(_selectedTabIndex);
|
||||
selectTab(_selectedTabIndex)
|
||||
}
|
||||
setLoading(true)
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
fun selectTab(tab: ChannelTab) {
|
||||
(_viewPager.adapter as ChannelViewPagerAdapter).getTabPosition(tab)
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
_taskLoadPolycentricProfile.cancel();
|
||||
_taskGetChannel.cancel();
|
||||
_tabLayoutMediator.detach();
|
||||
_viewPager.unregisterOnPageChangeCallback(_onPageChangeCallback);
|
||||
hideSlideUpOverlay();
|
||||
(_overlay_loading_spinner.drawable as Animatable?)?.stop();
|
||||
_taskLoadPolycentricProfile.cancel()
|
||||
_taskGetChannel.cancel()
|
||||
_tabLayoutMediator.detach()
|
||||
_viewPager.unregisterOnPageChangeCallback(_onPageChangeCallback)
|
||||
hideSlideUpOverlay()
|
||||
(_overlayLoadingSpinner.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);
|
||||
_imageBanner.setImageDrawable(null)
|
||||
|
||||
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);
|
||||
};
|
||||
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)
|
||||
}
|
||||
|
||||
_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);
|
||||
_url = parameter
|
||||
loadChannel()
|
||||
}
|
||||
|
||||
loadPolycentricProfile(parameter.id, parameter.url)
|
||||
};
|
||||
is SerializedChannel -> {
|
||||
showChannel(parameter)
|
||||
_url = parameter.url
|
||||
loadChannel()
|
||||
}
|
||||
|
||||
_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);
|
||||
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)
|
||||
|
||||
loadPolycentricProfile(parameter.channel.id, parameter.channel.url)
|
||||
};
|
||||
loadPolycentricProfile(parameter.id, parameter.url)
|
||||
}
|
||||
|
||||
_url = parameter.channel.url;
|
||||
loadChannel();
|
||||
_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()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loadChannel();
|
||||
loadChannel()
|
||||
}
|
||||
}
|
||||
|
||||
fun selectTab(selectedTabIndex: Int) {
|
||||
_selectedTabIndex = selectedTabIndex;
|
||||
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex));
|
||||
private 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){
|
||||
_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;
|
||||
_isLoading = isLoading
|
||||
if (isLoading) {
|
||||
_overlayLoading.visibility = View.VISIBLE
|
||||
(_overlayLoadingSpinner.drawable as Animatable?)?.start()
|
||||
} else {
|
||||
(_overlayLoadingSpinner.drawable as Animatable?)?.stop()
|
||||
_overlayLoading.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);
|
||||
}
|
||||
} 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();
|
||||
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 ""
|
||||
|
||||
//TODO: Find a better way to access the adapter fragments..
|
||||
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)
|
||||
}
|
||||
|
||||
(_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
|
||||
(_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)
|
||||
}
|
||||
|
||||
this.channel = channel;
|
||||
// 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
|
||||
|
||||
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 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);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
||||
val banner = profile?.systemState?.banner?.selectHighestResolutionImage()
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
if (avatar != null) {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate)
|
||||
} else {
|
||||
_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()
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
(_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
|
||||
// sets the profile for each tab
|
||||
for (fragment in _fragment.childFragmentManager.fragments) {
|
||||
(fragment as IChannelTabFragment).setPolycentricProfile(profile)
|
||||
}
|
||||
|
||||
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 {
|
||||
val TAG = "ChannelFragment";
|
||||
const val TAG = "ChannelFragment"
|
||||
fun newInstance() = ChannelFragment().apply { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||
.success { loadedResult(it); }.exception<ScriptCaptchaRequiredException> { }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load results.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }, null, fragment);
|
||||
}
|
||||
|
||||
setPreviewsEnabled(Settings.instance.search.previewFeedItems);
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ class CreatorSearchResultsFragment : MainFragment() {
|
||||
.exception<ScriptCaptchaRequiredException> { }
|
||||
.exception<Throwable> {
|
||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }, null, fragment);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
Logger.w(TAG, "Failed to load next page.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
||||
loadNextPage();
|
||||
});
|
||||
}, null, fragment);
|
||||
//UIDialogs.showDataRetryDialog(layoutInflater, it.message, { loadNextPage() });
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -174,7 +174,7 @@ class HistoryFragment : MainFragment() {
|
||||
Logger.w(TAG, "Failed to load next page.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
||||
loadNextPage();
|
||||
});
|
||||
}, null, fragment);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -126,10 +126,10 @@ class HomeFragment : MainFragment() {
|
||||
Logger.w(TAG, "Failed to load channel.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_get_home), it, {
|
||||
loadResults()
|
||||
}) {
|
||||
}, {
|
||||
finishRefreshLayoutLoader();
|
||||
setLoading(false);
|
||||
};
|
||||
}, fragment);
|
||||
};
|
||||
|
||||
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||
|
||||
+1
-1
@@ -146,7 +146,7 @@ class PlaylistFragment : MainFragment() {
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load playlist.", it);
|
||||
val c = context ?: return@exception;
|
||||
UIDialogs.showGeneralRetryErrorDialog(c, context.getString(R.string.failed_to_load_playlist), it, ::fetchPlaylist);
|
||||
UIDialogs.showGeneralRetryErrorDialog(c, context.getString(R.string.failed_to_load_playlist), it, ::fetchPlaylist, null, fragment);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ class PlaylistSearchResultsFragment : MainFragment() {
|
||||
.success { loadedResult(it); }
|
||||
.exception<Throwable> {
|
||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }, null, fragment);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -41,6 +41,7 @@ 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
|
||||
@@ -162,7 +163,7 @@ class PostDetailFragment : MainFragment {
|
||||
.success { setPostDetails(it) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(ChannelFragment.TAG, context.getString(R.string.failed_to_load_post), it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost, null, _fragment);
|
||||
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
||||
|
||||
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
||||
@@ -264,7 +265,7 @@ class PostDetailFragment : MainFragment {
|
||||
|
||||
_buttonSupport.setOnClickListener {
|
||||
val author = _post?.author ?: _postOverview?.author;
|
||||
author?.let { _fragment.navigate<ChannelFragment>(it).selectTab(2); };
|
||||
author?.let { _fragment.navigate<ChannelFragment>(it).selectTab(ChannelTab.SUPPORT); };
|
||||
};
|
||||
|
||||
_buttonStore.setOnClickListener {
|
||||
|
||||
+9
@@ -101,6 +101,11 @@ class SourceDetailFragment : MainFragment() {
|
||||
loadConfig(parameter);
|
||||
updateSourceViews();
|
||||
}
|
||||
else if(parameter is UpdatePluginAction) {
|
||||
loadConfig(parameter.config);
|
||||
updateSourceViews();
|
||||
checkForUpdatesSource();
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -567,4 +572,8 @@ class SourceDetailFragment : MainFragment() {
|
||||
const val TAG = "SourceDetailFragment";
|
||||
fun newInstance() = SourceDetailFragment().apply {}
|
||||
}
|
||||
|
||||
class UpdatePluginAction(val config: SourcePluginConfig) {
|
||||
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -262,7 +262,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
.exception<Throwable> {
|
||||
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
||||
if(it !is CancellationException)
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults(true) });
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults(true) }, null, fragment);
|
||||
else {
|
||||
finishRefreshLayoutLoader();
|
||||
setLoading(false);
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ class SuggestionsFragment : MainFragment {
|
||||
.success { suggestions -> updateSuggestions(suggestions, false) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(ChannelFragment.TAG, "Failed to load suggestions.", it);
|
||||
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadSuggestions() });
|
||||
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadSuggestions() }, null, this);
|
||||
};
|
||||
|
||||
constructor(): super() {
|
||||
|
||||
+24
-8
@@ -690,7 +690,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_lastAudioSource = null;
|
||||
_lastSubtitleSource = null;
|
||||
video = null;
|
||||
_playbackTracker = null;
|
||||
cleanupPlaybackTracker();
|
||||
Logger.i(TAG, "Keep screen on unset onClose")
|
||||
fragment.activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
};
|
||||
@@ -1033,7 +1033,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
_searchVideo = null;
|
||||
video = null;
|
||||
_playbackTracker = null;
|
||||
cleanupPlaybackTracker();
|
||||
_url = url;
|
||||
_videoResumePositionMilliseconds = resumeSeconds * 1000;
|
||||
_rating.visibility = View.GONE;
|
||||
@@ -1071,7 +1071,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
|
||||
this.video = null;
|
||||
this._playbackTracker = null;
|
||||
cleanupPlaybackTracker();
|
||||
_searchVideo = video;
|
||||
_videoResumePositionMilliseconds = resumeSeconds * 1000;
|
||||
setLastPositionMilliseconds(_videoResumePositionMilliseconds, false);
|
||||
@@ -1206,7 +1206,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
this.videoLocal = videoLocal;
|
||||
this.video = video;
|
||||
this._playbackTracker = null;
|
||||
cleanupPlaybackTracker();
|
||||
|
||||
if(video is JSVideoDetails) {
|
||||
val me = this;
|
||||
@@ -1522,6 +1522,22 @@ 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;
|
||||
@@ -2016,7 +2032,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
private fun fetchVideo() {
|
||||
Logger.i(TAG, "fetchVideo")
|
||||
video = null;
|
||||
_playbackTracker = null;
|
||||
cleanupPlaybackTracker();
|
||||
|
||||
val url = _url;
|
||||
if (url != null && url.isNotBlank()) {
|
||||
@@ -2476,7 +2492,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
Logger.w(TAG, "exception<ScriptImplementationException>", it)
|
||||
|
||||
if (!nextVideo()) {
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, ::fetchVideo);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, ::fetchVideo, null, fragment);
|
||||
} else {
|
||||
StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_INVALIDVIDEO", context.getString(R.string.invalid_video), context.getString(
|
||||
R.string.there_was_an_invalid_video_in_your_queue_videoname_by_authorname_playback_was_skipped).replace("{videoName}", video?.name ?: "").replace("{authorName}", video?.author?.name ?: ""), AnnouncementType.SESSION)
|
||||
@@ -2512,7 +2528,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_retryJob = null;
|
||||
_liveTryJob?.cancel();
|
||||
_liveTryJob = null;
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, ::fetchVideo);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, ::fetchVideo, null, fragment);
|
||||
}
|
||||
}
|
||||
.exception<Throwable> {
|
||||
@@ -2524,7 +2540,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_retryJob = null;
|
||||
_liveTryJob?.cancel();
|
||||
_liveTryJob = null;
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo);
|
||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo, null, fragment);
|
||||
}
|
||||
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ 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
|
||||
@@ -44,7 +45,7 @@ class VideoHelper {
|
||||
}
|
||||
|
||||
fun isDownloadable(source: IVideoSource) = source is IVideoUrlSource || source is IHLSManifestSource;
|
||||
fun isDownloadable(source: IAudioSource) = source is IAudioUrlSource || source is IHLSManifestAudioSource;
|
||||
fun isDownloadable(source: IAudioSource) = (source is IAudioUrlSource || source is IHLSManifestAudioSource) && source !is IAudioUrlWidevineSource
|
||||
|
||||
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? {
|
||||
|
||||
@@ -316,7 +316,6 @@ class PolycentricCache {
|
||||
.build();
|
||||
|
||||
private const val TAG = "PolycentricCache"
|
||||
const val STAGING_SERVER = "https://srv1-stg.polycentric.io"
|
||||
const val SERVER = "https://srv1-prod.polycentric.io"
|
||||
private var _instance: PolycentricCache? = null;
|
||||
private val CACHE_EXPIRATION_SECONDS = 60 * 5;
|
||||
|
||||
@@ -571,18 +571,22 @@ class StateApp {
|
||||
StateAnnouncement.instance.deleteAnnouncement("plugin-update")
|
||||
|
||||
scopeOrNull?.launch(Dispatchers.IO) {
|
||||
val updateAvailable = StatePlatform.instance.checkForUpdates()
|
||||
val updateAvailable = StatePlugins.instance.checkForUpdates()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (updateAvailable.isNotEmpty()) {
|
||||
UIDialogs.appToast(
|
||||
ToastView.Toast(updateAvailable
|
||||
.map { " - " + it.name }
|
||||
.map { " - " + it.first.name }
|
||||
.joinToString("\n"),
|
||||
true,
|
||||
null,
|
||||
"Plugin updates available"
|
||||
));
|
||||
|
||||
for(update in updateAvailable)
|
||||
if(StatePlatform.instance.isClientEnabled(update.first.id))
|
||||
UIDialogs.showPluginUpdateDialog(context, update.first, update.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ class StateDeveloper {
|
||||
|
||||
private var _devLogsIndex: Int = 0;
|
||||
private val _devLogs: MutableList<DevLog> = mutableListOf();
|
||||
private val _devHttpExchanges: MutableList<DevHttpExchange> = mutableListOf();
|
||||
|
||||
var devProxy: DevProxySettings? = null;
|
||||
|
||||
fun initializeDev(id: String) {
|
||||
currentDevID = id;
|
||||
@@ -94,6 +97,21 @@ class StateDeveloper {
|
||||
}
|
||||
}
|
||||
|
||||
fun addDevHttpExchange(exchange: DevHttpExchange) {
|
||||
synchronized(_devHttpExchanges) {
|
||||
if(_devHttpExchanges.size > 15)
|
||||
_devHttpExchanges.removeAt(0);
|
||||
_devHttpExchanges.add(exchange);
|
||||
}
|
||||
}
|
||||
fun getHttpExchangesAndClear(): List<DevHttpExchange> {
|
||||
synchronized(_devHttpExchanges) {
|
||||
val data = _devHttpExchanges.toList();
|
||||
_devHttpExchanges.clear();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
fun setDevClientSettings(settings: HashMap<String, String?>) {
|
||||
val client = StatePlatform.instance.getDevClient();
|
||||
client?.let {
|
||||
@@ -138,4 +156,12 @@ class StateDeveloper {
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class DevLog(val id: Int, val devId: String, val type: String, val log: String);
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class DevHttpRequest(val method: String, val url: String, val headers: Map<String, String>, val body: String, val status: Int = 0);
|
||||
@kotlinx.serialization.Serializable
|
||||
data class DevHttpExchange(val request: DevHttpRequest, val response: DevHttpRequest);
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class DevProxySettings(val url: String, val port: Int)
|
||||
}
|
||||
@@ -456,6 +456,18 @@ class StateDownloads {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val currentDownloads = _downloaded.getItems().map { it.url }.toHashSet();
|
||||
val exporting = _exporting.findItems { !currentDownloads.contains(it.videoLocal.url) };
|
||||
for (export in exporting)
|
||||
_exporting.delete(export);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to delete dangling export:", ex);
|
||||
UIDialogs.toast("Failed to delete dangling export:\n" + ex);
|
||||
}
|
||||
|
||||
return Pair(totalDeletedCount, totalDeleted);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.futo.platformplayer.api.media.models.contents.PlatformContentPlacehol
|
||||
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.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
@@ -80,7 +81,6 @@ class StatePlatform {
|
||||
private val _clientsLock = Object();
|
||||
private val _availableClients : ArrayList<IPlatformClient> = ArrayList();
|
||||
private val _enabledClients : ArrayList<IPlatformClient> = ArrayList();
|
||||
private var _updatesAvailableMap: HashSet<String> = hashSetOf();
|
||||
|
||||
//ClientPools are used to isolate plugin usage of certain components from others
|
||||
//This prevents for example a background task like subscriptions from blocking a user from opening a video
|
||||
@@ -800,6 +800,11 @@ class StatePlatform {
|
||||
return client.getChannelContents(channelUrl, type, ordering) ;
|
||||
}
|
||||
|
||||
fun getChannelPlaylists(channelUrl: String): IPager<IPlatformPlaylist> {
|
||||
val client = getChannelClient(channelUrl);
|
||||
return client.getChannelPlaylists(channelUrl);
|
||||
}
|
||||
|
||||
fun peekChannelContents(baseClient: IPlatformClient, channelUrl: String, type: String?): List<IPlatformContent> {
|
||||
val client = _channelClientPool.getClientPooled(baseClient, Settings.instance.subscriptions.getSubscriptionsConcurrency());
|
||||
return client.peekChannelContents(channelUrl, type) ;
|
||||
@@ -925,66 +930,7 @@ class StatePlatform {
|
||||
}
|
||||
}
|
||||
|
||||
fun hasUpdateAvailable(c: SourcePluginConfig): Boolean {
|
||||
val updatesAvailableMap = _updatesAvailableMap
|
||||
synchronized(updatesAvailableMap) {
|
||||
return updatesAvailableMap.contains(c.id)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkForUpdates(): List<SourcePluginConfig> = withContext(Dispatchers.IO) {
|
||||
var configs = mutableListOf<SourcePluginConfig>()
|
||||
val updatesAvailableFor = hashSetOf<String>()
|
||||
for (availableClient in getAvailableClients().filter { it is JSClient && it.descriptor.appSettings.checkForUpdates }) {
|
||||
if (availableClient !is JSClient) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (checkForUpdates(availableClient.config)) {
|
||||
configs.add(availableClient.config);
|
||||
updatesAvailableFor.add(availableClient.config.id)
|
||||
}
|
||||
}
|
||||
|
||||
_updatesAvailableMap = updatesAvailableFor
|
||||
return@withContext configs;
|
||||
}
|
||||
|
||||
fun clearUpdateAvailable(c: SourcePluginConfig) {
|
||||
val updatesAvailableMap = _updatesAvailableMap
|
||||
synchronized(updatesAvailableMap) {
|
||||
updatesAvailableMap.remove(c.id)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkForUpdates(c: SourcePluginConfig): Boolean = withContext(Dispatchers.IO) {
|
||||
val sourceUrl = c.sourceUrl ?: return@withContext false;
|
||||
|
||||
Logger.i(TAG, "Check for source updates '${c.name}'.");
|
||||
try {
|
||||
val client = ManagedHttpClient();
|
||||
val response = client.get(sourceUrl);
|
||||
Logger.i(TAG, "Downloading source config '$sourceUrl'.");
|
||||
|
||||
if (!response.isOk || response.body == null) {
|
||||
return@withContext false;
|
||||
}
|
||||
|
||||
val configJson = response.body.string();
|
||||
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
||||
|
||||
val config = SourcePluginConfig.fromJson(configJson);
|
||||
if (config.version <= c.version) {
|
||||
return@withContext false;
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Update is available (config.version=${config.version}, source.config.version=${c.version}).");
|
||||
return@withContext true;
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to check for updates.", e);
|
||||
return@withContext false;
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var _instance : StatePlatform? = null;
|
||||
|
||||
@@ -43,6 +43,7 @@ class StatePlugins {
|
||||
private var _embeddedSourcesDefault: List<String>? = null
|
||||
private var _sourcesUnderConstruction: Map<String, ImageVariable>? = null
|
||||
|
||||
private var _updatesAvailableMap: HashSet<String> = hashSetOf();
|
||||
|
||||
fun getPluginIconOrNull(id: String): ImageVariable? {
|
||||
if(iconsDir.hasIcon(id))
|
||||
@@ -55,6 +56,70 @@ class StatePlugins {
|
||||
.load();
|
||||
}
|
||||
|
||||
|
||||
suspend fun checkForUpdates(): List<Pair<SourcePluginConfig, SourcePluginConfig>> = withContext(Dispatchers.IO) {
|
||||
var configs = mutableListOf<Pair<SourcePluginConfig, SourcePluginConfig>>()
|
||||
val updatesAvailableFor = hashSetOf<String>()
|
||||
for (availableClient in StatePlatform.instance.getAvailableClients().filter { it is JSClient && it.descriptor.appSettings.checkForUpdates }) {
|
||||
if (availableClient !is JSClient) {
|
||||
continue
|
||||
}
|
||||
|
||||
val newConfig = checkForUpdates(availableClient.config);
|
||||
if (newConfig != null) {
|
||||
configs.add(Pair(availableClient.config, newConfig));
|
||||
updatesAvailableFor.add(availableClient.config.id)
|
||||
}
|
||||
}
|
||||
|
||||
_updatesAvailableMap = updatesAvailableFor
|
||||
return@withContext configs;
|
||||
}
|
||||
private suspend fun checkForUpdates(c: SourcePluginConfig): SourcePluginConfig? = withContext(Dispatchers.IO) {
|
||||
val sourceUrl = c.sourceUrl ?: return@withContext null;
|
||||
|
||||
Logger.i(TAG, "Check for source updates '${c.name}'.");
|
||||
try {
|
||||
val client = ManagedHttpClient();
|
||||
val response = client.get(sourceUrl);
|
||||
Logger.i(TAG, "Downloading source config '$sourceUrl'.");
|
||||
|
||||
if (!response.isOk || response.body == null) {
|
||||
return@withContext null;
|
||||
}
|
||||
|
||||
val configJson = response.body.string();
|
||||
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
||||
|
||||
val config = SourcePluginConfig.fromJson(configJson);
|
||||
if (config.version <= c.version) {
|
||||
return@withContext null;
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Update is available (config.version=${config.version}, source.config.version=${c.version}).");
|
||||
return@withContext config;
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to check for updates.", e);
|
||||
return@withContext null;
|
||||
}
|
||||
}
|
||||
fun hasUpdateAvailable(c: SourcePluginConfig): Boolean {
|
||||
val updatesAvailableMap = _updatesAvailableMap
|
||||
synchronized(updatesAvailableMap) {
|
||||
return updatesAvailableMap.contains(c.id)
|
||||
}
|
||||
}
|
||||
fun clearUpdateAvailable(c: SourcePluginConfig) {
|
||||
val updatesAvailableMap = _updatesAvailableMap
|
||||
synchronized(updatesAvailableMap) {
|
||||
updatesAvailableMap.remove(c.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
fun loginPlugin(context: Context, id: String, afterLogin: ()->Unit): Boolean {
|
||||
val descriptor = getPlugin(id) ?: return false;
|
||||
val config = descriptor.config;
|
||||
@@ -353,6 +418,49 @@ class StatePlugins {
|
||||
else verifyCanInstall();
|
||||
}
|
||||
|
||||
fun installPluginBackground(context: Context, scope: CoroutineScope, config: SourcePluginConfig, script: String, onProgress: (text: String, progress: Double)->Unit, onConcluded: (ex: Throwable?)->Unit) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val client = ManagedHttpClient();
|
||||
try {
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress.invoke("Validating script", 0.25);
|
||||
}
|
||||
|
||||
val tempDescriptor = SourcePluginDescriptor(config);
|
||||
val plugin = JSClient(context, tempDescriptor, null, script);
|
||||
plugin.validate();
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress.invoke("Downloading Icon", 0.5);
|
||||
}
|
||||
|
||||
val icon = config.absoluteIconUrl?.let { absIconUrl ->
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress.invoke("Saving plugin", 0.75);
|
||||
}
|
||||
val iconResp = client.get(absIconUrl);
|
||||
if(iconResp.isOk)
|
||||
return@let iconResp.body?.byteStream()?.use { it.readBytes() };
|
||||
return@let null;
|
||||
}
|
||||
val installEx = StatePlugins.instance.createPlugin(config, script, icon, true);
|
||||
if(installEx != null)
|
||||
throw installEx;
|
||||
StatePlatform.instance.updateAvailableClients(context);
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress.invoke("Finished", 1.0)
|
||||
onConcluded.invoke(null);
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Logger.e(TAG, ex.message ?: "null", ex);
|
||||
withContext(Dispatchers.Main) {
|
||||
onConcluded.invoke(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getPlugin(id: String): SourcePluginDescriptor? {
|
||||
if(id == StateDeveloper.DEV_ID)
|
||||
throw IllegalStateException("Attempted to retrieve a persistent developer plugin, this is not allowed");
|
||||
|
||||
+101
-49
@@ -5,69 +5,121 @@ import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
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.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.fragment.channel.tab.*
|
||||
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.channel.tab.ChannelPlaylistsFragment
|
||||
import com.futo.platformplayer.fragment.channel.tab.IChannelTabFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
|
||||
class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||
private val _cache: Array<Fragment?> = arrayOfNulls(4);
|
||||
|
||||
val onContentUrlClicked = Event2<String, ContentType>();
|
||||
val onUrlClicked = Event1<String>();
|
||||
val onContentClicked = Event2<IPlatformContent, Long>();
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||
val onAddToClicked = Event1<IPlatformContent>();
|
||||
val onAddToQueueClicked = Event1<IPlatformContent>();
|
||||
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
|
||||
val onLongPress = Event1<IPlatformContent>();
|
||||
enum class ChannelTab {
|
||||
VIDEOS, CHANNELS, PLAYLISTS, SUPPORT, ABOUT
|
||||
}
|
||||
|
||||
class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
||||
FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||
private val _supportedFragments = mutableMapOf(
|
||||
ChannelTab.VIDEOS.ordinal to ChannelTab.VIDEOS, ChannelTab.ABOUT.ordinal to ChannelTab.ABOUT
|
||||
)
|
||||
private val _tabs = arrayListOf(ChannelTab.VIDEOS, ChannelTab.ABOUT)
|
||||
|
||||
var profile: PolycentricProfile? = null
|
||||
var channel: IPlatformChannel? = null
|
||||
|
||||
val onContentUrlClicked = Event2<String, ContentType>()
|
||||
val onUrlClicked = Event1<String>()
|
||||
val onContentClicked = Event2<IPlatformContent, Long>()
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>()
|
||||
val onAddToClicked = Event1<IPlatformContent>()
|
||||
val onAddToQueueClicked = Event1<IPlatformContent>()
|
||||
val onAddToWatchLaterClicked = Event1<IPlatformContent>()
|
||||
val onLongPress = Event1<IPlatformContent>()
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return _tabs[position].ordinal.toLong()
|
||||
}
|
||||
|
||||
override fun containsItem(itemId: Long): Boolean {
|
||||
return _supportedFragments.containsKey(itemId.toInt())
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return _cache.size;
|
||||
return _supportedFragments.size
|
||||
}
|
||||
inline fun <reified T:IChannelTabFragment> getFragment(): T {
|
||||
|
||||
//TODO: I have a feeling this can somehow be synced with createFragment so only 1 mapping exists (without a Map<>)
|
||||
if(T::class == ChannelContentsFragment::class)
|
||||
return createFragment(0) as T;
|
||||
else if(T::class == ChannelListFragment::class)
|
||||
return createFragment(1) as T;
|
||||
//else if(T::class == ChannelStoreFragment::class)
|
||||
// return createFragment(2) as T;
|
||||
else if(T::class == ChannelMonetizationFragment::class)
|
||||
return createFragment(2) as T;
|
||||
else if(T::class == ChannelAboutFragment::class)
|
||||
return createFragment(3) as T;
|
||||
else
|
||||
throw NotImplementedError("Implement other types");
|
||||
fun getTabPosition(tab: ChannelTab): Int {
|
||||
return _tabs.indexOf(tab)
|
||||
}
|
||||
|
||||
fun getTabNames(tab: TabLayout.Tab, position: Int) {
|
||||
tab.text = _tabs[position].name
|
||||
}
|
||||
|
||||
fun insert(position: Int, tab: ChannelTab) {
|
||||
_supportedFragments[tab.ordinal] = tab
|
||||
_tabs.add(position, tab)
|
||||
notifyItemInserted(position)
|
||||
}
|
||||
|
||||
fun remove(position: Int) {
|
||||
_supportedFragments.remove(_tabs[position].ordinal)
|
||||
_tabs.removeAt(position)
|
||||
notifyItemRemoved(position)
|
||||
}
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
val cachedFragment = _cache[position];
|
||||
if (cachedFragment != null) {
|
||||
return cachedFragment;
|
||||
val fragment: Fragment
|
||||
when (_tabs[position]) {
|
||||
ChannelTab.VIDEOS -> {
|
||||
fragment = ChannelContentsFragment.newInstance().apply {
|
||||
onContentClicked.subscribe(this@ChannelViewPagerAdapter.onContentClicked::emit)
|
||||
onContentUrlClicked.subscribe(this@ChannelViewPagerAdapter.onContentUrlClicked::emit)
|
||||
onUrlClicked.subscribe(this@ChannelViewPagerAdapter.onUrlClicked::emit)
|
||||
onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit)
|
||||
onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit)
|
||||
onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit)
|
||||
onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit)
|
||||
onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit)
|
||||
}
|
||||
}
|
||||
|
||||
ChannelTab.CHANNELS -> {
|
||||
fragment = ChannelListFragment.newInstance()
|
||||
.apply { onClickChannel.subscribe(onChannelClicked::emit) }
|
||||
}
|
||||
|
||||
ChannelTab.PLAYLISTS -> {
|
||||
fragment = ChannelPlaylistsFragment.newInstance().apply {
|
||||
onContentClicked.subscribe(this@ChannelViewPagerAdapter.onContentClicked::emit)
|
||||
onContentUrlClicked.subscribe(this@ChannelViewPagerAdapter.onContentUrlClicked::emit)
|
||||
onUrlClicked.subscribe(this@ChannelViewPagerAdapter.onUrlClicked::emit)
|
||||
onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit)
|
||||
onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit)
|
||||
onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit)
|
||||
onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit)
|
||||
onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit)
|
||||
}
|
||||
}
|
||||
|
||||
ChannelTab.SUPPORT -> {
|
||||
fragment = ChannelMonetizationFragment.newInstance()
|
||||
}
|
||||
|
||||
ChannelTab.ABOUT -> {
|
||||
fragment = ChannelAboutFragment.newInstance()
|
||||
}
|
||||
}
|
||||
channel?.let { (fragment as IChannelTabFragment).setChannel(it) }
|
||||
profile?.let { (fragment as IChannelTabFragment).setPolycentricProfile(it) }
|
||||
|
||||
val fragment = when (position) {
|
||||
0 -> ChannelContentsFragment.newInstance().apply {
|
||||
onContentClicked.subscribe(this@ChannelViewPagerAdapter.onContentClicked::emit);
|
||||
onContentUrlClicked.subscribe(this@ChannelViewPagerAdapter.onContentUrlClicked::emit);
|
||||
onUrlClicked.subscribe(this@ChannelViewPagerAdapter.onUrlClicked::emit);
|
||||
onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit);
|
||||
onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit);
|
||||
onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit);
|
||||
onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit);
|
||||
onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit);
|
||||
};
|
||||
1 -> ChannelListFragment.newInstance().apply { onClickChannel.subscribe(onChannelClicked::emit) };
|
||||
//2 -> ChannelStoreFragment.newInstance();
|
||||
2 -> ChannelMonetizationFragment.newInstance();
|
||||
3 -> ChannelAboutFragment.newInstance();
|
||||
else -> throw IllegalStateException("Invalid tab position $position")
|
||||
};
|
||||
|
||||
_cache[position]= fragment;
|
||||
return fragment;
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
|
||||
class DisabledSourceView : LinearLayout {
|
||||
private val _root: LinearLayout;
|
||||
@@ -37,7 +38,7 @@ class DisabledSourceView : LinearLayout {
|
||||
|
||||
_textSource.text = client.name;
|
||||
|
||||
if (client is JSClient && StatePlatform.instance.hasUpdateAvailable(client.config)) {
|
||||
if (client is JSClient && StatePlugins.instance.hasUpdateAvailable(client.config)) {
|
||||
_textSourceSubtitle.text = context.getString(R.string.update_available_exclamation)
|
||||
_textSourceSubtitle.setTextColor(context.getColor(R.color.light_blue_400))
|
||||
_textSourceSubtitle.typeface = resources.getFont(R.font.inter_regular)
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
|
||||
class EnabledSourceViewHolder : ViewHolder {
|
||||
private val _imageSource: ImageView;
|
||||
@@ -61,7 +62,7 @@ class EnabledSourceViewHolder : ViewHolder {
|
||||
|
||||
_textSource.text = client.name
|
||||
|
||||
if (client is JSClient && StatePlatform.instance.hasUpdateAvailable(client.config)) {
|
||||
if (client is JSClient && StatePlugins.instance.hasUpdateAvailable(client.config)) {
|
||||
_textSourceSubtitle.text = itemView.context.getString(R.string.update_available_exclamation)
|
||||
_textSourceSubtitle.setTextColor(itemView.context.getColor(R.color.light_blue_400))
|
||||
_textSourceSubtitle.typeface = itemView.resources.getFont(R.font.inter_regular)
|
||||
|
||||
@@ -106,8 +106,15 @@ class LiveChatOverlay : LinearLayout {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url);
|
||||
_window?.let {
|
||||
var toRemoveJS = "";
|
||||
for(req in it.removeElements)
|
||||
view?.evaluateJavascript("document.querySelectorAll(" + _argJsonSerializer.encodeToString(req) + ").forEach(x=>x.remove());") {};
|
||||
toRemoveJS += "document.querySelectorAll(" + _argJsonSerializer.encodeToString(req) + ").forEach(x=>x.remove());\n";
|
||||
view?.evaluateJavascript(toRemoveJS) {};
|
||||
var toRemoveJSInterval = "";
|
||||
for(req in it.removeElementsInterval)
|
||||
toRemoveJSInterval += "document.querySelectorAll(" + _argJsonSerializer.encodeToString(req) + ").forEach(x=>x.remove());\n";
|
||||
//Cleanup every second as fallback
|
||||
view?.evaluateJavascript("setInterval(()=>{" + toRemoveJSInterval + "}, 1000)") {};
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.datasource.DefaultHttpDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.dash.DashMediaSource
|
||||
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider
|
||||
import androidx.media3.exoplayer.hls.HlsMediaSource
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.MergingMediaSource
|
||||
@@ -27,6 +28,7 @@ import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
@@ -389,6 +391,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
is LocalAudioSource -> swapAudioSourceLocal(audioSource);
|
||||
is JSAudioUrlRangeSource -> swapAudioSourceUrlRange(audioSource);
|
||||
is JSHLSManifestAudioSource -> swapAudioSourceHLS(audioSource);
|
||||
is IAudioUrlWidevineSource -> swapAudioSourceUrlWidevine(audioSource)
|
||||
is IAudioUrlSource -> swapAudioSourceUrl(audioSource);
|
||||
null -> _lastAudioMediaSource = null;
|
||||
else -> throw IllegalArgumentException("Unsupported video source [${audioSource.javaClass.simpleName}]");
|
||||
@@ -508,6 +511,31 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
.createMediaSource(MediaItem.fromUri(audioSource.url));
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapAudioSourceUrlWidevine(audioSource: IAudioUrlWidevineSource) {
|
||||
Logger.i(TAG, "Loading AudioSource [UrlWidevine]")
|
||||
val dataSource = if (audioSource is JSSource && audioSource.hasRequestModifier)
|
||||
audioSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)
|
||||
|
||||
val httpRequestHeaders = mapOf("Authorization" to "Bearer " + audioSource.bearerToken)
|
||||
val provider = DefaultDrmSessionManagerProvider()
|
||||
provider.setDrmHttpDataSourceFactory(dataSource)
|
||||
_lastAudioMediaSource = ProgressiveMediaSource.Factory(dataSource)
|
||||
.setDrmSessionManagerProvider(provider)
|
||||
.createMediaSource(
|
||||
MediaItem.Builder()
|
||||
.setUri(audioSource.getAudioUrl()).setDrmConfiguration(
|
||||
MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
|
||||
.setLicenseUri(audioSource.licenseUri)
|
||||
.setMultiSession(true)
|
||||
.setLicenseRequestHeaders(httpRequestHeaders)
|
||||
.build()
|
||||
).build()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//Prefered source selection
|
||||
fun getPreferredVideoSource(video: IPlatformVideoDetails, targetPixels: Int = -1): IVideoSource? {
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:background="@color/gray_1d">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:paddingTop="30dp"
|
||||
android:paddingBottom="30dp">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/dialog_ui_choice_top"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible">
|
||||
<ImageView
|
||||
android:id="@+id/icon_plugin"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
app:srcCompat="@drawable/ic_sources" />
|
||||
|
||||
</FrameLayout>
|
||||
<FrameLayout
|
||||
android:id="@+id/dialog_ui_risk_top"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone">
|
||||
<ImageView
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
app:srcCompat="@drawable/ic_warning_yellow" />
|
||||
|
||||
</FrameLayout>
|
||||
<FrameLayout
|
||||
android:id="@+id/dialog_ui_progress_top"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/update_spinner"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
app:srcCompat="@drawable/ic_update_animated"
|
||||
android:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Plugin Update"
|
||||
android:textSize="15sp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_extra_light"
|
||||
android:layout_marginTop="15dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
<TextView
|
||||
android:id="@+id/text_plugin"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Some Plugin Name"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/dialog_ui_bottom_choice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text="A new update is available.\nWould you like to update this plugin?"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text="Updates may be critical to functionality"
|
||||
android:textSize="13sp"
|
||||
android:textColor="@color/pastel_red"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="28dp">
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_cancel_1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/cancel"
|
||||
android:textSize="14dp"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:background="@color/transparent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_update"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_button_primary"
|
||||
android:layout_marginEnd="28dp"
|
||||
android:clickable="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Update"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingStart="28dp"
|
||||
android:paddingEnd="28dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/dialog_ui_bottom_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/text_progress"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text="This plugin has modified its permissions"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/dialog_ui_bottom_risk"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text="This plugin has modified its permissions"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text="Make sure you read the installation screen"
|
||||
android:textSize="13sp"
|
||||
android:textColor="@color/pastel_red"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="28dp">
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_cancel_2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/cancel"
|
||||
android:textSize="14dp"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:background="@color/transparent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_install"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_button_primary"
|
||||
android:layout_marginEnd="28dp"
|
||||
android:clickable="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Reinstall"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingStart="28dp"
|
||||
android:paddingEnd="28dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/dialog_ui_bottom_result"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:id="@+id/text_error"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text=""
|
||||
android:textSize="13sp"
|
||||
android:textColor="@color/pastel_red"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_result"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:text="Succesfully updated plugin."
|
||||
android:textSize="13sp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="28dp">
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_ok"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_button_primary"
|
||||
android:layout_marginEnd="28dp"
|
||||
android:clickable="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Ok"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingStart="28dp"
|
||||
android:paddingEnd="28dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -347,6 +347,7 @@
|
||||
<string name="get_answers_to_common_questions">Get answers to common questions</string>
|
||||
<string name="give_feedback_on_the_application">Give feedback on the application</string>
|
||||
<string name="info">Info</string>
|
||||
<string name="networking">Networking</string>
|
||||
<string name="gesture_controls">Gesture controls</string>
|
||||
<string name="volume_slider">Volume slider</string>
|
||||
<string name="volume_slider_descr">Enable slide gesture to change volume</string>
|
||||
@@ -461,6 +462,8 @@
|
||||
<string name="deletes_all_ongoing_downloads">Deletes all ongoing downloads</string>
|
||||
<string name="deletes_all_unresolved_source_files">Deletes all unresolved source files</string>
|
||||
<string name="developer_mode">Developer Mode</string>
|
||||
<string name="allow_all_certificates">Allow All Certificates</string>
|
||||
<string name="allow_all_certificates_warning">This risks exposing all your Grayjay network traffic.</string>
|
||||
<string name="development_server">Development Server</string>
|
||||
<string name="experimental">Experimental</string>
|
||||
<string name="cache">Cache</string>
|
||||
@@ -489,6 +492,8 @@
|
||||
<string name="visibility">Visibility</string>
|
||||
<string name="check_for_updates_setting">Check for updates</string>
|
||||
<string name="check_for_updates_setting_description">If a plugin should be checked for updates on startup</string>
|
||||
<string name="automatic_update_setting">Automatic Update</string>
|
||||
<string name="automatic_update_setting_description">Update automatically on boot if no permissions changed and plugin is enabled</string>
|
||||
<string name="allow_developer_submit">Allow Developer Submissions</string>
|
||||
<string name="allow_developer_submit_description">Allows the developer to send data to their server, be careful as this might include sensitive data.</string>
|
||||
<string name="allow_developer_submit_warning">Make sure you trust the developer. They may gain access to sensitive data. Only enable this when you are trying to help the developer fix a bug.</string>
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
<data android:host="patreon.com" />
|
||||
<data android:host="soundcloud.com" />
|
||||
<data android:host="twitch.tv" />
|
||||
<data android:host="bilibili.com" />
|
||||
<data android:host="bilibili.tv" />
|
||||
<data android:host="spotify.com" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
@@ -51,6 +54,9 @@
|
||||
<data android:host="patreon.com" />
|
||||
<data android:host="soundcloud.com" />
|
||||
<data android:host="twitch.tv" />
|
||||
<data android:host="bilibili.com" />
|
||||
<data android:host="bilibili.tv" />
|
||||
<data android:host="spotify.com" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
Submodule app/src/stable/assets/sources/bilibili updated: 3cc6d553cf...b518be4dd5
Submodule app/src/stable/assets/sources/patreon updated: cee1fda4e8...5b1919934d
Submodule
+1
Submodule app/src/stable/assets/sources/spotify added at 843cf2dc4b
Submodule app/src/stable/assets/sources/twitch updated: 8d978dd7bd...b4696e4e2e
Submodule app/src/stable/assets/sources/youtube updated: cac2740844...c23302da76
@@ -9,7 +9,8 @@
|
||||
"4a78c2ff-c20f-43ac-8f75-34515df1d320": "sources/kick/KickConfig.json",
|
||||
"aac9e9f0-24b5-11ee-be56-0242ac120002": "sources/patreon/PatreonConfig.json",
|
||||
"9d703ff5-c556-4962-a990-4f000829cb87": "sources/nebula/NebulaConfig.json",
|
||||
"cf8ea74d-ad9b-489e-a083-539b6aa8648c": "sources/bilibili/build/BiliBiliConfig.json"
|
||||
"cf8ea74d-ad9b-489e-a083-539b6aa8648c": "sources/bilibili/build/BiliBiliConfig.json",
|
||||
"4e365633-6d3f-4267-8941-fdc36631d813": "sources/spotify/build/SpotifyConfig.json"
|
||||
},
|
||||
"SOURCES_EMBEDDED_DEFAULT": [
|
||||
"35ae969a-a7db-11ed-afa1-0242ac120002"
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
<data android:host="patreon.com" />
|
||||
<data android:host="soundcloud.com" />
|
||||
<data android:host="twitch.tv" />
|
||||
<data android:host="bilibili.com" />
|
||||
<data android:host="bilibili.tv" />
|
||||
<data android:host="spotify.com" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
@@ -51,6 +54,9 @@
|
||||
<data android:host="patreon.com" />
|
||||
<data android:host="soundcloud.com" />
|
||||
<data android:host="twitch.tv" />
|
||||
<data android:host="bilibili.com" />
|
||||
<data android:host="bilibili.tv" />
|
||||
<data android:host="spotify.com" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
Submodule app/src/unstable/assets/sources/bilibili updated: 3cc6d553cf...b518be4dd5
Submodule app/src/unstable/assets/sources/patreon updated: cee1fda4e8...5b1919934d
Submodule
+1
Submodule app/src/unstable/assets/sources/spotify added at 843cf2dc4b
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "Testing",
|
||||
"description": "Just for testing.",
|
||||
"author": "FUTO",
|
||||
"authorUrl": "https://futo.org",
|
||||
|
||||
"platformUrl": "https://odysee.com",
|
||||
"sourceUrl": "https://plugins.grayjay.app/Test/TestConfig.json",
|
||||
"repositoryUrl": "https://futo.org",
|
||||
"scriptUrl": "./TestScript.js",
|
||||
"version": 31,
|
||||
|
||||
"iconUrl": "./odysee.png",
|
||||
"id": "1c05bfc3-08b9-42d0-93d3-6d52e0fd34d8",
|
||||
|
||||
"scriptSignature": "",
|
||||
"scriptPublicKey": "",
|
||||
"packages": ["Http"],
|
||||
|
||||
"allowEval": false,
|
||||
"allowUrls": [],
|
||||
|
||||
"supportedClaimTypes": []
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
var config = {};
|
||||
|
||||
//Source Methods
|
||||
source.enable = function(conf){
|
||||
config = conf ?? {};
|
||||
//log(config);
|
||||
}
|
||||
source.getHome = function() {
|
||||
return new ContentPager([
|
||||
source.getContentDetails("whatever")
|
||||
]);
|
||||
};
|
||||
|
||||
//Video
|
||||
source.isContentDetailsUrl = function(url) {
|
||||
return REGEX_DETAILS_URL.test(url)
|
||||
};
|
||||
source.getContentDetails = function(url) {
|
||||
return new PlatformVideoDetails({
|
||||
id: new PlatformID("Test", "Something", config.id),
|
||||
name: "Test Video",
|
||||
thumbnails: new Thumbnails([]),
|
||||
author: new PlatformAuthorLink(new PlatformID("Test", "TestID", config.id),
|
||||
"TestAuthor",
|
||||
"None",
|
||||
""),
|
||||
datetime: parseInt(new Date().getTime() / 1000),
|
||||
duration: 0,
|
||||
viewCount: 0,
|
||||
url: "",
|
||||
isLive: false,
|
||||
description: "",
|
||||
rating: new RatingLikes(0),
|
||||
video: new VideoSourceDescriptor([
|
||||
new HLSSource({
|
||||
name: "HLS",
|
||||
url: "",
|
||||
duration: 0,
|
||||
priority: true
|
||||
})
|
||||
])
|
||||
});
|
||||
};
|
||||
|
||||
log("LOADED");
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB |
Submodule app/src/unstable/assets/sources/twitch updated: 8d978dd7bd...b4696e4e2e
Submodule app/src/unstable/assets/sources/youtube updated: cac2740844...c23302da76
@@ -9,7 +9,8 @@
|
||||
"4a78c2ff-c20f-43ac-8f75-34515df1d320": "sources/kick/KickConfig.json",
|
||||
"aac9e9f0-24b5-11ee-be56-0242ac120002": "sources/patreon/PatreonConfig.json",
|
||||
"9d703ff5-c556-4962-a990-4f000829cb87": "sources/nebula/NebulaConfig.json",
|
||||
"cf8ea74d-ad9b-489e-a083-539b6aa8648c": "sources/bilibili/build/BiliBiliConfig.json"
|
||||
"cf8ea74d-ad9b-489e-a083-539b6aa8648c": "sources/bilibili/build/BiliBiliConfig.json",
|
||||
"4e365633-6d3f-4267-8941-fdc36631d813": "sources/spotify/build/SpotifyConfig.json"
|
||||
},
|
||||
"SOURCES_EMBEDDED_DEFAULT": [
|
||||
"35ae969a-a7db-11ed-afa1-0242ac120002"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config>
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
+18
-12
@@ -1,21 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Array of directories to look in
|
||||
dirs=("app/src/unstable/assets/sources" "app/src/stable/assets/sources")
|
||||
|
||||
# Loop through each directory
|
||||
sign_scripts() {
|
||||
local plugin_dir=$1
|
||||
|
||||
if [[ -d "$plugin_dir" ]]; then
|
||||
script_file=$(find "$plugin_dir" -maxdepth 2 -name '*Script.js')
|
||||
config_file=$(find "$plugin_dir" -maxdepth 2 -name '*Config.json')
|
||||
sign_script="$plugin_dir/sign.sh"
|
||||
|
||||
if [[ -f "$sign_script" && -n "$script_file" && -n "$config_file" ]]; then
|
||||
sh "$sign_script" "$script_file" "$config_file"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
for dir in "${dirs[@]}"; do
|
||||
if [[ -d "$dir" ]]; then # Check if directory exists
|
||||
for plugin in "$dir"/*; do # Loop through each plugin folder
|
||||
if [[ -d "$dir" ]]; then
|
||||
for plugin in "$dir"/*; do
|
||||
if [[ -d "$plugin" ]]; then
|
||||
script_file=$(find "$plugin" -maxdepth 1 -name '*Script.js')
|
||||
config_file=$(find "$plugin" -maxdepth 1 -name '*Config.json')
|
||||
sign_script="$plugin/sign.sh"
|
||||
|
||||
if [[ -f "$sign_script" && -n "$script_file" && -n "$config_file" ]]; then
|
||||
sh "$sign_script" "$script_file" "$config_file"
|
||||
fi
|
||||
sign_scripts "$plugin"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
done
|
||||
Reference in New Issue
Block a user