mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-29 19:13:01 +02:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f1c0209a8 | |||
| 819e81b7a6 | |||
| 8193234c2f | |||
| 6263a31f41 | |||
| 481a0cda99 | |||
| b39b89e908 | |||
| ce0f98055f | |||
| 3dddf68766 | |||
| 88d687f26e | |||
| d44df42727 | |||
| 88c8dbcb7c | |||
| b4fddbe26a | |||
| ab6d7669d7 | |||
| 3f22c7f717 | |||
| f36e9588cb | |||
| 8f99f399ee | |||
| 56166a7948 | |||
| 4edd8ee1ea | |||
| a830c918ab | |||
| 53f74c4b6e | |||
| 959c192762 | |||
| 8be7b1272b | |||
| 6b57878275 | |||
| 66c7741c38 | |||
| b370af9d91 | |||
| 40b86cb5de | |||
| 84622e22aa | |||
| 092b20041e | |||
| f6cc00f471 | |||
| be2067067b | |||
| 67a7dd9698 | |||
| 6ffc067b24 | |||
| 56e6314c11 | |||
| e590bb4a19 | |||
| 35fe7f0e7a | |||
| 45d818ac81 | |||
| 7729681829 | |||
| b12d04b27d | |||
| e6608b9a5c | |||
| 2d503dfaf6 | |||
| 08934ef8de | |||
| 62d927739a | |||
| c8db8f58e8 | |||
| 0fc966a77d | |||
| 9f6c6c8cf3 | |||
| 43a6ff138c | |||
| 269a3460e7 | |||
| 18150e9e15 | |||
| 362c7f5b2c | |||
| 2adb8ad7f9 | |||
| 6b5d4e7507 | |||
| 49c82726f0 | |||
| c8ddcda384 | |||
| b75217f789 | |||
| 8ba8e535bd | |||
| e4c574db6b | |||
| fae73293d7 | |||
| 3bd0aac4f8 | |||
| 26b822e04b | |||
| 96b9b8843c |
@@ -58,3 +58,9 @@
|
|||||||
[submodule "dep/futopay"]
|
[submodule "dep/futopay"]
|
||||||
path = dep/futopay
|
path = dep/futopay
|
||||||
url = ../futopayclientlibraries.git
|
url = ../futopayclientlibraries.git
|
||||||
|
[submodule "app/src/unstable/assets/sources/bilibili"]
|
||||||
|
path = app/src/unstable/assets/sources/bilibili
|
||||||
|
url = ../plugins/bilibili.git
|
||||||
|
[submodule "app/src/stable/assets/sources/bilibili"]
|
||||||
|
path = app/src/stable/assets/sources/bilibili
|
||||||
|
url = ../plugins/bilibili.git
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|||||||
@@ -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 |
@@ -233,6 +233,9 @@ function pluginRemoteProp(objID, propName) {
|
|||||||
function pluginRemoteCall(objID, methodName, args) {
|
function pluginRemoteCall(objID, methodName, args) {
|
||||||
return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args)));
|
return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args)));
|
||||||
}
|
}
|
||||||
|
function pluginRemoteTest(methodName, args) {
|
||||||
|
return JSON.parse(syncPOST("/plugin/remoteTest?method=" + methodName, {}, JSON.stringify(args)));
|
||||||
|
}
|
||||||
|
|
||||||
function pluginIsLoggedIn(cb, err) {
|
function pluginIsLoggedIn(cb, err) {
|
||||||
fetch("/plugin/isLoggedIn", {
|
fetch("/plugin/isLoggedIn", {
|
||||||
@@ -259,6 +262,17 @@ function getDevLogs(lastIndex, cb) {
|
|||||||
.then(x=>x.json())
|
.then(x=>x.json())
|
||||||
.then(y=> cb && cb(y));
|
.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) {
|
function sendFakeDevLog(devId, msg) {
|
||||||
return syncGET("/plugin/fakeDevLog?devId=" + devId + "&msg=" + msg, {});
|
return syncGET("/plugin/fakeDevLog?devId=" + devId + "&msg=" + msg, {});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
<!--<link href="./dependencies/vuetify.min.css" rel="stylesheet">-->
|
<!--<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">
|
<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">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -150,7 +153,7 @@
|
|||||||
.pastPluginUrl {
|
.pastPluginUrl {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
width: 500px;
|
width: 700px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@@ -160,13 +163,122 @@
|
|||||||
box-shadow: 0px 1px 2px #131313;
|
box-shadow: 0px 1px 2px #131313;
|
||||||
font-weight: lighter;
|
font-weight: lighter;
|
||||||
cursor: pointer;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<v-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 id="topMenu">
|
||||||
<div style="height: 100%; display: inline-block; padding-left: 10px; padding-right: 20px;">
|
<div style="height: 100%; display: inline-block; padding-left: 10px; padding-right: 20px;">
|
||||||
<img src="./dependencies/FutoMainLogo.svg"
|
<img src="./dependencies/FutoMainLogo.svg"
|
||||||
@@ -250,10 +362,13 @@
|
|||||||
</div>
|
</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>
|
<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)">
|
<div class="pastPluginUrl" v-for="pastPluginUrl in pastPluginUrls" @click="this.Plugin.newPluginUrl = pastPluginUrl; loadPlugin(pastPluginUrl)">
|
||||||
{{pastPluginUrl}}
|
{{pastPluginUrl}}
|
||||||
|
<div class="deleteButton" @click="(ev)=>{ev.stopPropagation(); deletePastPlugin(pastPluginUrl)}">
|
||||||
|
X
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -385,8 +500,8 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
<div style="width: 50%" v-if="Plugin.currentPlugin">
|
<div style="width: 50%" v-if="Plugin.currentPlugin">
|
||||||
<!--Get Home-->
|
<v-text-field v-model="searchTestMethods" label="Search for source methods.." style="margin-left: 35px; margin-right: 35px;"></v-text-field>
|
||||||
<v-card class="requestCard" v-for="req in Testing.requests">
|
<v-card class="requestCard" v-for="req in Testing.requests" v-show="req.title.indexOf(searchTestMethods) >= 0">
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<span v-if="req.isOptional">(Optional)</span>
|
<span v-if="req.isOptional">(Optional)</span>
|
||||||
@@ -402,6 +517,11 @@
|
|||||||
<div class="code">
|
<div class="code">
|
||||||
{{req.code}}
|
{{req.code}}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="documentation" v-if="req.docUrl" style="position: absolute; right: 15px; top: 15px;">
|
||||||
|
<a :href="req.docUrl" target="_blank">
|
||||||
|
Documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="parameter" v-for="parameter in req.parameters">
|
<div class="parameter" v-for="parameter in req.parameters">
|
||||||
<div class="name">
|
<div class="name">
|
||||||
@@ -416,6 +536,9 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn @click="testSourceRemotely(req)">
|
||||||
|
Test Android
|
||||||
|
</v-btn>
|
||||||
<v-btn @click="testSource(req)">
|
<v-btn @click="testSource(req)">
|
||||||
Test
|
Test
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@@ -497,7 +620,62 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<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-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -535,6 +713,7 @@
|
|||||||
<!--<script src="./dependencies/vue.js"></script>-->
|
<!--<script src="./dependencies/vue.js"></script>-->
|
||||||
<!--<script src="./dependencies/vuetify.js"></script>-->
|
<!--<script src="./dependencies/vuetify.js"></script>-->
|
||||||
<script src="./source_docs.js"></script>
|
<script src="./source_docs.js"></script>
|
||||||
|
<script src="./source_doc_urls.js"></script>
|
||||||
<script src="./source.js"></script>
|
<script src="./source.js"></script>
|
||||||
<script src="./dev_bridge.js"></script>
|
<script src="./dev_bridge.js"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -545,6 +724,7 @@
|
|||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
|
searchTestMethods: "",
|
||||||
page: "Plugin",
|
page: "Plugin",
|
||||||
pastPluginUrls: [],
|
pastPluginUrls: [],
|
||||||
settings: {},
|
settings: {},
|
||||||
@@ -552,7 +732,9 @@
|
|||||||
lastLogIndex: -1,
|
lastLogIndex: -1,
|
||||||
lastLogDevID: "",
|
lastLogDevID: "",
|
||||||
logs: [],
|
logs: [],
|
||||||
lastInjectTime: ""
|
httpExchanges: [],
|
||||||
|
lastInjectTime: "",
|
||||||
|
showHttpRequests: false
|
||||||
},
|
},
|
||||||
Plugin: {
|
Plugin: {
|
||||||
loadUsingTag: false,
|
loadUsingTag: false,
|
||||||
@@ -570,6 +752,9 @@
|
|||||||
Testing: {
|
Testing: {
|
||||||
requests: sourceDocs.map(x=>{
|
requests: sourceDocs.map(x=>{
|
||||||
x.parameters.forEach(y=>y.value = null);
|
x.parameters.forEach(y=>y.value = null);
|
||||||
|
|
||||||
|
if(sourceDocUrls[x.title])
|
||||||
|
x.docUrl = sourceDocUrls[x.title];
|
||||||
return x;
|
return x;
|
||||||
}),
|
}),
|
||||||
lastResult: "",
|
lastResult: "",
|
||||||
@@ -633,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) {
|
catch(ex) {
|
||||||
console.error("Failed update", ex);
|
console.error("Failed update", ex);
|
||||||
@@ -674,6 +869,12 @@
|
|||||||
this.reloadPlugin();
|
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() {
|
loginTestPlugin() {
|
||||||
pluginLoginTestPlugin();
|
pluginLoginTestPlugin();
|
||||||
setTimeout(()=>{
|
setTimeout(()=>{
|
||||||
@@ -860,8 +1061,58 @@
|
|||||||
"Error: " + ex;
|
"Error: " + ex;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
testSourceRemotely(req) {
|
||||||
|
const name = req.title;
|
||||||
|
const parameterVals = req.parameters.map(x=>{
|
||||||
|
if(x.value && x.value.startsWith && x.value.startsWith("json:"))
|
||||||
|
return JSON.parse(x.value.substring(5));
|
||||||
|
return x.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if(name == "enable") {
|
||||||
|
if(parameterVals.length > 0)
|
||||||
|
parameterVals[0] = this.Plugin.currentPlugin;
|
||||||
|
else
|
||||||
|
parameterVals.push(this.Plugin.currentPlugin);
|
||||||
|
if(parameterVals.length > 1)
|
||||||
|
parameterVals[1] = __DEV_SETTINGS;
|
||||||
|
else
|
||||||
|
parameterVals.push(__DEV_SETTINGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const func = source[name];
|
||||||
|
if(!func)
|
||||||
|
alert("Test func not found");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const remoteResult = pluginRemoteTest(name, parameterVals);
|
||||||
|
console.log("Result for " + req.title, remoteResult);
|
||||||
|
this.Testing.lastResult = "//Results [" + name + "]\n" +
|
||||||
|
JSON.stringify(remoteResult, null, 3);
|
||||||
|
this.Testing.lastResultError = "";
|
||||||
|
}
|
||||||
|
catch(ex) {
|
||||||
|
if(ex.plugin_type == "CaptchaRequiredException") {
|
||||||
|
let shouldCaptcha = confirm("Do you want to request captcha?");
|
||||||
|
if(shouldCaptcha) {
|
||||||
|
pluginCaptchaTestPlugin(ex.url, ex.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.error("Failed to run test for " + req.title, ex);
|
||||||
|
this.Testing.lastResult = ""
|
||||||
|
if(ex.message)
|
||||||
|
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
|
||||||
|
"Error: " + ex.message + "\n\n" + ex.stack;
|
||||||
|
else
|
||||||
|
this.Testing.lastResultError = "//Results [" + name + "]\n\n" +
|
||||||
|
"Error: " + ex;
|
||||||
|
}
|
||||||
|
},
|
||||||
showTestResults(results) {
|
showTestResults(results) {
|
||||||
|
|
||||||
|
},
|
||||||
|
toggleHttpExchange(exchange) {
|
||||||
|
exchange.response.show = !exchange.response.show;
|
||||||
},
|
},
|
||||||
copyClipboard(cpy) {
|
copyClipboard(cpy) {
|
||||||
if(navigator.clipboard)
|
if(navigator.clipboard)
|
||||||
|
|||||||
@@ -357,6 +357,15 @@ class AudioUrlSource {
|
|||||||
this.requestModifier = obj.requestModifier;
|
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 {
|
class AudioUrlRangeSource extends AudioUrlSource {
|
||||||
constructor(obj) {
|
constructor(obj) {
|
||||||
super(obj);
|
super(obj);
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import java.text.DecimalFormat
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
|
|
||||||
//Long
|
//Long
|
||||||
@@ -119,7 +121,8 @@ fun OffsetDateTime.getNowDiffMonths(): Long {
|
|||||||
return ChronoUnit.MONTHS.between(this, OffsetDateTime.now());
|
return ChronoUnit.MONTHS.between(this, OffsetDateTime.now());
|
||||||
}
|
}
|
||||||
fun OffsetDateTime.getNowDiffYears(): Long {
|
fun OffsetDateTime.getNowDiffYears(): Long {
|
||||||
return ChronoUnit.YEARS.between(this, OffsetDateTime.now());
|
val diff = ChronoUnit.MONTHS.between(this, OffsetDateTime.now()) / 12.0;
|
||||||
|
return diff.roundToLong();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun OffsetDateTime.getDiffDays(otherDate: OffsetDateTime): Long {
|
fun OffsetDateTime.getDiffDays(otherDate: OffsetDateTime): Long {
|
||||||
@@ -150,6 +153,7 @@ fun OffsetDateTime.toHumanNowDiffString(abs: Boolean = false) : String {
|
|||||||
if(value >= secondsInYear) {
|
if(value >= secondsInYear) {
|
||||||
value = getNowDiffYears();
|
value = getNowDiffYears();
|
||||||
if(abs) value = abs(value);
|
if(abs) value = abs(value);
|
||||||
|
value = Math.max(1, value);
|
||||||
unit = "year";
|
unit = "year";
|
||||||
}
|
}
|
||||||
else if(value >= secondsInMonth) {
|
else if(value >= secondsInMonth) {
|
||||||
|
|||||||
@@ -50,12 +50,9 @@ fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
|||||||
|
|
||||||
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
||||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
|
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)) {
|
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()
|
val exceptions = fullyBackfillServers()
|
||||||
|
|||||||
@@ -311,7 +311,10 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
|
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
|
||||||
var alwaysReloadFromCache: Boolean = false;
|
var alwaysReloadFromCache: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 15)
|
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
|
||||||
|
var peekChannelContents: Boolean = false;
|
||||||
|
|
||||||
|
@FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
|
||||||
fun clearChannelCache() {
|
fun clearChannelCache() {
|
||||||
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
|
||||||
StateCache.instance.clear();
|
StateCache.instance.clear();
|
||||||
@@ -546,6 +549,8 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@DropdownFieldOptionsId(R.array.log_levels)
|
@DropdownFieldOptionsId(R.array.log_levels)
|
||||||
var logLevel: Int = 0;
|
var logLevel: Int = 0;
|
||||||
|
|
||||||
|
fun isVerbose() = logLevel >= 4;
|
||||||
|
|
||||||
@FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1)
|
@FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1)
|
||||||
fun submitLogs() {
|
fun submitLogs() {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
@@ -823,7 +828,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var toggleFullscreen: Boolean = true;
|
var toggleFullscreen: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.system_brightness, FieldForm.TOGGLE, R.string.system_brightness_descr, 4)
|
@FormField(R.string.system_brightness, FieldForm.TOGGLE, R.string.system_brightness_descr, 4)
|
||||||
var useSystemBrightness: Boolean = true;
|
var useSystemBrightness: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.system_volume, FieldForm.TOGGLE, R.string.system_volume_descr, 5)
|
@FormField(R.string.system_volume, FieldForm.TOGGLE, R.string.system_volume_descr, 5)
|
||||||
var useSystemVolume: Boolean = true;
|
var useSystemVolume: Boolean = true;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import android.widget.Toast
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
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.casting.StateCasting
|
||||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||||
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
|
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
|
||||||
@@ -31,11 +32,17 @@ import com.futo.platformplayer.dialogs.ConnectedCastingDialog
|
|||||||
import com.futo.platformplayer.dialogs.ImportDialog
|
import com.futo.platformplayer.dialogs.ImportDialog
|
||||||
import com.futo.platformplayer.dialogs.ImportOptionsDialog
|
import com.futo.platformplayer.dialogs.ImportOptionsDialog
|
||||||
import com.futo.platformplayer.dialogs.MigrateDialog
|
import com.futo.platformplayer.dialogs.MigrateDialog
|
||||||
|
import com.futo.platformplayer.dialogs.PluginUpdateDialog
|
||||||
import com.futo.platformplayer.dialogs.ProgressDialog
|
import com.futo.platformplayer.dialogs.ProgressDialog
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
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.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateBackup
|
import com.futo.platformplayer.states.StateBackup
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import com.futo.platformplayer.views.ToastView
|
import com.futo.platformplayer.views.ToastView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -183,6 +190,14 @@ class UIDialogs {
|
|||||||
dialog.show();
|
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) {
|
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 builder = AlertDialog.Builder(context);
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
||||||
@@ -268,22 +283,48 @@ class UIDialogs {
|
|||||||
}, UIDialogs.ActionStyle.PRIMARY)
|
}, 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)
|
val pluginInfo = if(ex is PluginException)
|
||||||
"\nPlugin [${ex.config.name}]" else "";
|
"\nPlugin [${ex.config.name}]" else "";
|
||||||
showDialog(context,
|
|
||||||
R.drawable.ic_error_pred,
|
var exMsg = if(ex != null ) "${ex.message}" else "";
|
||||||
"${msg}${pluginInfo}",
|
if(pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
|
||||||
(if(ex != null ) "${ex.message}" else ""),
|
exMsg += "\n\nAn update is available"
|
||||||
if(ex is PluginException) ex.code else null,
|
|
||||||
0,
|
if(mainFragment != null && pluginConfig != null && pluginConfig is SourcePluginConfig && StatePlugins.instance.hasUpdateAvailable(pluginConfig))
|
||||||
UIDialogs.Action(context.getString(R.string.retry), {
|
showDialog(context,
|
||||||
retryAction?.invoke();
|
R.drawable.ic_error_pred,
|
||||||
}, UIDialogs.ActionStyle.PRIMARY),
|
"${msg}${pluginInfo}",
|
||||||
UIDialogs.Action(context.getString(R.string.close), {
|
exMsg,
|
||||||
closeAction?.invoke()
|
if(ex is PluginException) ex.code else null,
|
||||||
}, UIDialogs.ActionStyle.NONE)
|
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)) {
|
fun showSingleButtonDialog(context: Context, icon: Int, text: String, buttonText: String, action: (() -> Unit)) {
|
||||||
@@ -304,12 +345,16 @@ class UIDialogs {
|
|||||||
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
showDialog(context, R.drawable.ic_error, text, null, null, 0, cancelButtonAction, confirmButtonAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showUpdateAvailableDialog(context: Context, lastVersion: Int) {
|
fun showUpdateAvailableDialog(context: Context, lastVersion: Int, hideExceptionButtons: Boolean = false) {
|
||||||
val dialog = AutoUpdateDialog(context);
|
val dialog = AutoUpdateDialog(context);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
dialog.setMaxVersion(lastVersion);
|
dialog.setMaxVersion(lastVersion);
|
||||||
|
|
||||||
|
if (hideExceptionButtons) {
|
||||||
|
dialog.hideExceptionButtons()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showChangelogDialog(context: Context, lastVersion: Int) {
|
fun showChangelogDialog(context: Context, lastVersion: Int) {
|
||||||
@@ -339,8 +384,8 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, onConcluded: () -> Unit) {
|
fun showImportDialog(context: Context, store: ManagedStore<*>, name: String, reconstructions: List<String>, cache: ImportCache?, onConcluded: () -> Unit) {
|
||||||
val dialog = ImportDialog(context, store, name, reconstructions, onConcluded);
|
val dialog = ImportDialog(context, store, name, reconstructions, cache, onConcluded);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
@@ -37,11 +38,17 @@ import com.futo.platformplayer.states.StateMeta
|
|||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView
|
||||||
|
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||||
import com.futo.platformplayer.views.LoaderView
|
import com.futo.platformplayer.views.LoaderView
|
||||||
|
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuFilters
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuFilters
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuRecycler
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
||||||
import com.futo.platformplayer.views.pills.RoundButton
|
import com.futo.platformplayer.views.pills.RoundButton
|
||||||
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
||||||
@@ -87,7 +94,37 @@ class UISlideOverlays {
|
|||||||
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
||||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||||
}, false),
|
}, false),
|
||||||
|
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||||
|
SlideUpMenuGroup(container.context, "Subscription Groups",
|
||||||
|
"You can select which groups this subscription is part of.",
|
||||||
|
-1, listOf()) else null,
|
||||||
|
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||||
|
SlideUpMenuRecycler(container.context, "as") {
|
||||||
|
val groups = ArrayList<SubscriptionGroup>(StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||||
|
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
|
||||||
|
.sortedBy { !it.selected });
|
||||||
|
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? = null;
|
||||||
|
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
|
||||||
|
it.onClick.subscribe {
|
||||||
|
if(it is SubscriptionGroup.Selectable) {
|
||||||
|
val actualGroup = StateSubscriptionGroups.instance.getSubscriptionGroup(it.id)
|
||||||
|
?: return@subscribe;
|
||||||
|
groups.clear();
|
||||||
|
if(it.selected)
|
||||||
|
actualGroup.urls.remove(subscription.channel.url);
|
||||||
|
else
|
||||||
|
actualGroup.urls.add(subscription.channel.url);
|
||||||
|
|
||||||
|
StateSubscriptionGroups.instance.updateSubscriptionGroup(actualGroup);
|
||||||
|
groups.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||||
|
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
|
||||||
|
.sortedBy { !it.selected });
|
||||||
|
adapter?.notifyContentChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return@SlideUpMenuRecycler adapter;
|
||||||
|
} else null,
|
||||||
SlideUpMenuGroup(container.context, "Fetch Settings",
|
SlideUpMenuGroup(container.context, "Fetch Settings",
|
||||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||||
-1, listOf()),
|
-1, listOf()),
|
||||||
@@ -473,10 +510,15 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
|
fun showDownloadPlaylistOverlay(playlist: Playlist, container: ViewGroup) {
|
||||||
showUnknownVideoDownload(container.context.getString(R.string.video), container) { px, bitrate ->
|
showUnknownVideoDownload(container.context.getString(R.string.playlist), container) { px, bitrate ->
|
||||||
StateDownloads.instance.download(playlist, px, bitrate);
|
StateDownloads.instance.download(playlist, px, bitrate);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
fun showDownloadWatchlaterOverlay(container: ViewGroup) {
|
||||||
|
showUnknownVideoDownload(container.context.getString(R.string.watch_later), container, { px, bitrate ->
|
||||||
|
StateDownloads.instance.downloadWatchLater(px, bitrate);
|
||||||
|
})
|
||||||
|
}
|
||||||
private fun showUnknownVideoDownload(toDownload: String, container: ViewGroup, cb: (Long?, Long?)->Unit) {
|
private fun showUnknownVideoDownload(toDownload: String, container: ViewGroup, cb: (Long?, Long?)->Unit) {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
var menu: SlideUpMenuOverlay? = null;
|
var menu: SlideUpMenuOverlay? = null;
|
||||||
@@ -646,9 +688,17 @@ class UISlideOverlays {
|
|||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||||
(listOf(
|
(listOf(
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download), {
|
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), "download", {
|
||||||
showDownloadVideoOverlay(video, container, true);
|
showDownloadVideoOverlay(video, container, true);
|
||||||
}, false),
|
}, false),
|
||||||
|
SlideUpMenuItem(container.context, R.drawable.ic_share, container.context.getString(R.string.share), "Share the video", "share", {
|
||||||
|
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
|
||||||
|
container.context.startActivity(Intent.createChooser(Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND;
|
||||||
|
putExtra(Intent.EXTRA_TEXT, url);
|
||||||
|
type = "text/plain";
|
||||||
|
}, null));
|
||||||
|
}, false),
|
||||||
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
|
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
|
||||||
StateMeta.instance.addHiddenCreator(video.author.url);
|
StateMeta.instance.addHiddenCreator(video.author.url);
|
||||||
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
||||||
@@ -738,8 +788,8 @@ class UISlideOverlays {
|
|||||||
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
|
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.add_to), null, true, items).apply { show() };
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>): SlideUpMenuFilters {
|
fun showFiltersOverlay(lifecycleScope: CoroutineScope, container: ViewGroup, enabledClientsIds: List<String>, filterValues: HashMap<String, List<String>>, isChannelSearch: Boolean = false): SlideUpMenuFilters {
|
||||||
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues);
|
val overlay = SlideUpMenuFilters(lifecycleScope, container, enabledClientsIds, filterValues, isChannelSearch);
|
||||||
overlay.show();
|
overlay.show();
|
||||||
return overlay;
|
return overlay;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
|
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
|
||||||
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
||||||
if(it) {
|
if(it) {
|
||||||
StatePlatform.instance.clearUpdateAvailable(config)
|
StatePlugins.instance.clearUpdateAvailable(config)
|
||||||
if(isNew)
|
if(isNew)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
StatePlatform.instance.enableClient(listOf(config.id));
|
StatePlatform.instance.enableClient(listOf(config.id));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@@ -15,6 +16,7 @@ import com.futo.platformplayer.logging.LogLevel
|
|||||||
import com.futo.platformplayer.logging.Logging
|
import com.futo.platformplayer.logging.Logging
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -28,6 +30,7 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
private lateinit var _buttonSubmit: LinearLayout;
|
private lateinit var _buttonSubmit: LinearLayout;
|
||||||
private lateinit var _buttonRestart: LinearLayout;
|
private lateinit var _buttonRestart: LinearLayout;
|
||||||
private lateinit var _buttonClose: LinearLayout;
|
private lateinit var _buttonClose: LinearLayout;
|
||||||
|
private lateinit var _buttonCheckForUpdates: LinearLayout;
|
||||||
private var _file: File? = null;
|
private var _file: File? = null;
|
||||||
private var _submitted = false;
|
private var _submitted = false;
|
||||||
|
|
||||||
@@ -45,6 +48,7 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
_buttonSubmit = findViewById(R.id.button_submit);
|
_buttonSubmit = findViewById(R.id.button_submit);
|
||||||
_buttonRestart = findViewById(R.id.button_restart);
|
_buttonRestart = findViewById(R.id.button_restart);
|
||||||
_buttonClose = findViewById(R.id.button_close);
|
_buttonClose = findViewById(R.id.button_close);
|
||||||
|
_buttonCheckForUpdates = findViewById(R.id.button_check_for_updates);
|
||||||
|
|
||||||
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: getString(R.string.unknown_context);
|
val context = intent.getStringExtra(EXTRA_CONTEXT) ?: getString(R.string.unknown_context);
|
||||||
val stack = intent.getStringExtra(EXTRA_STACK) ?: getString(R.string.something_went_wrong_missing_stack_trace);
|
val stack = intent.getStringExtra(EXTRA_STACK) ?: getString(R.string.something_went_wrong_missing_stack_trace);
|
||||||
@@ -83,6 +87,17 @@ class ExceptionActivity : AppCompatActivity() {
|
|||||||
_buttonClose.setOnClickListener {
|
_buttonClose.setOnClickListener {
|
||||||
finish();
|
finish();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!BuildConfig.IS_PLAYSTORE_BUILD) {
|
||||||
|
_buttonCheckForUpdates.visibility = View.VISIBLE
|
||||||
|
_buttonCheckForUpdates.setOnClickListener {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
StateUpdate.instance.checkForUpdates(this@ExceptionActivity, true, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_buttonCheckForUpdates.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun submitFile() {
|
private fun submitFile() {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import androidx.fragment.app.FragmentContainerView
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
@@ -40,12 +41,14 @@ import com.futo.platformplayer.fragment.mainactivity.topbar.NavigationTopBarFrag
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
import com.futo.platformplayer.listeners.OrientationManager
|
import com.futo.platformplayer.listeners.OrientationManager
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||||
import com.futo.platformplayer.states.*
|
import com.futo.platformplayer.states.*
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import com.futo.platformplayer.views.ToastView
|
import com.futo.platformplayer.views.ToastView
|
||||||
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
@@ -141,7 +144,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
handleUrlAll(content)
|
runBlocking {
|
||||||
|
handleUrlAll(content)
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.i(TAG, "Failed to handle URL.", e)
|
Logger.i(TAG, "Failed to handle URL.", e)
|
||||||
UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
|
UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
|
||||||
@@ -150,6 +155,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor() : super() {
|
constructor() : super() {
|
||||||
|
ApiMethods.UserAgent = "Grayjay Android (${BuildConfig.VERSION_CODE})";
|
||||||
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
|
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
|
||||||
val writer = StringWriter();
|
val writer = StringWriter();
|
||||||
|
|
||||||
@@ -188,6 +195,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
Logger.i(TAG, "MainActivity Starting");
|
||||||
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
StateApp.instance.setGlobalContext(this, lifecycleScope);
|
||||||
StateApp.instance.mainAppStarting(this);
|
StateApp.instance.mainAppStarting(this);
|
||||||
|
|
||||||
@@ -540,7 +548,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Pair("grayjay") { req ->
|
Pair("grayjay") { req ->
|
||||||
StateApp.instance.contextOrNull?.let {
|
StateApp.instance.contextOrNull?.let {
|
||||||
if(it is MainActivity) {
|
if(it is MainActivity) {
|
||||||
it.handleUrlAll(req.url.toString());
|
runBlocking {
|
||||||
|
it.handleUrlAll(req.url.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -552,7 +562,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (targetData != null) {
|
if (targetData != null) {
|
||||||
handleUrlAll(targetData)
|
runBlocking {
|
||||||
|
handleUrlAll(targetData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
@@ -560,7 +572,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleUrlAll(url: String) {
|
suspend fun handleUrlAll(url: String) {
|
||||||
val uri = Uri.parse(url)
|
val uri = Uri.parse(url)
|
||||||
when (uri.scheme) {
|
when (uri.scheme) {
|
||||||
"grayjay" -> {
|
"grayjay" -> {
|
||||||
@@ -596,7 +608,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
this,
|
this,
|
||||||
R.drawable.ic_play,
|
R.drawable.ic_play,
|
||||||
getString(R.string.unknown_content_format) + " [${url}]",
|
getString(R.string.unknown_content_format) + " [${url}]\n[${intent.type}]",
|
||||||
"Ok",
|
"Ok",
|
||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
@@ -644,31 +656,38 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleUrl(url: String): Boolean {
|
suspend fun handleUrl(url: String): Boolean {
|
||||||
Logger.i(TAG, "handleUrl(url=$url)")
|
Logger.i(TAG, "handleUrl(url=$url)")
|
||||||
|
|
||||||
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
return withContext(Dispatchers.IO) {
|
||||||
navigate(_fragVideoDetail, url);
|
Logger.i(TAG, "handleUrl(url=$url) on IO");
|
||||||
_fragVideoDetail.maximizeVideoDetail(true);
|
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
||||||
return true;
|
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
||||||
} else if(StatePlatform.instance.hasEnabledChannelClient(url)) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
navigate(_fragMainChannel, url);
|
navigate(_fragVideoDetail, url);
|
||||||
|
|
||||||
lifecycleScope.launch {
|
_fragVideoDetail.maximizeVideoDetail(true);
|
||||||
delay(100);
|
}
|
||||||
_fragVideoDetail.minimizeVideoDetail();
|
return@withContext true;
|
||||||
};
|
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
|
||||||
return true;
|
Logger.i(TAG, "handleUrl(url=$url) found channel client");
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
navigate(_fragMainChannel, url);
|
||||||
|
delay(100);
|
||||||
|
_fragVideoDetail.minimizeVideoDetail();
|
||||||
|
};
|
||||||
|
return@withContext true;
|
||||||
|
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
||||||
|
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
navigate(_fragMainPlaylist, url);
|
||||||
|
delay(100);
|
||||||
|
_fragVideoDetail.minimizeVideoDetail();
|
||||||
|
};
|
||||||
|
return@withContext true;
|
||||||
|
}
|
||||||
|
return@withContext false;
|
||||||
}
|
}
|
||||||
else if(StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
|
||||||
navigate(_fragMainPlaylist, url);
|
|
||||||
lifecycleScope.launch {
|
|
||||||
delay(100);
|
|
||||||
_fragVideoDetail.minimizeVideoDetail();
|
|
||||||
};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
fun handleContent(file: String, mime: String? = null): Boolean {
|
fun handleContent(file: String, mime: String? = null): Boolean {
|
||||||
Logger.i(TAG, "handleContent(url=$file)");
|
Logger.i(TAG, "handleContent(url=$file)");
|
||||||
@@ -679,10 +698,22 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if(!recon.trim().startsWith("["))
|
if(!recon.trim().startsWith("["))
|
||||||
return handleUnknownJson(recon);
|
return handleUnknownJson(recon);
|
||||||
|
|
||||||
val reconLines = Json.decodeFromString<List<String>>(recon);
|
var reconLines = Json.decodeFromString<List<String>>(recon);
|
||||||
|
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
|
||||||
|
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
|
||||||
|
var cache: ImportCache? = null;
|
||||||
|
try {
|
||||||
|
if(cacheStr != null)
|
||||||
|
cache = Json.decodeFromString(cacheStr);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to deserialize cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
recon = reconLines.joinToString("\n");
|
recon = reconLines.joinToString("\n");
|
||||||
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
||||||
handleReconstruction(recon);
|
handleReconstruction(recon, cache);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if(file.lowercase().endsWith(".zip") || mime == "application/zip") {
|
else if(file.lowercase().endsWith(".zip") || mime == "application/zip") {
|
||||||
@@ -697,12 +728,25 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
fun handleFile(file: String): Boolean {
|
fun handleFile(file: String): Boolean {
|
||||||
Logger.i(TAG, "handleFile(url=$file)");
|
Logger.i(TAG, "handleFile(url=$file)");
|
||||||
if(file.lowercase().endsWith(".json")) {
|
if(file.lowercase().endsWith(".json")) {
|
||||||
val recon = String(readSharedFile(file));
|
var recon = String(readSharedFile(file));
|
||||||
if(!recon.startsWith("["))
|
if(!recon.startsWith("["))
|
||||||
return handleUnknownJson(recon);
|
return handleUnknownJson(recon);
|
||||||
|
|
||||||
|
var reconLines = Json.decodeFromString<List<String>>(recon);
|
||||||
|
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
|
||||||
|
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
|
||||||
|
var cache: ImportCache? = null;
|
||||||
|
try {
|
||||||
|
if(cacheStr != null)
|
||||||
|
cache = Json.decodeFromString(cacheStr);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to deserialize cache");
|
||||||
|
}
|
||||||
|
recon = reconLines.joinToString("\n");
|
||||||
|
|
||||||
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
||||||
handleReconstruction(recon);
|
handleReconstruction(recon, cache);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if(file.lowercase().endsWith(".zip")) {
|
else if(file.lowercase().endsWith(".zip")) {
|
||||||
@@ -714,7 +758,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
fun handleReconstruction(recon: String) {
|
fun handleReconstruction(recon: String, cache: ImportCache? = null) {
|
||||||
val type = ManagedStore.getReconstructionIdentifier(recon);
|
val type = ManagedStore.getReconstructionIdentifier(recon);
|
||||||
val store: ManagedStore<*> = when(type) {
|
val store: ManagedStore<*> = when(type) {
|
||||||
"Playlist" -> StatePlaylists.instance.playlistStore
|
"Playlist" -> StatePlaylists.instance.playlistStore
|
||||||
@@ -731,7 +775,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
|
|
||||||
if(!type.isNullOrEmpty()) {
|
if(!type.isNullOrEmpty()) {
|
||||||
UIDialogs.showImportDialog(this, store, name, listOf(recon)) {
|
UIDialogs.showImportDialog(this, store, name, listOf(recon), cache) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -12,6 +12,8 @@ import com.futo.platformplayer.R
|
|||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.logging.Logger
|
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.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
@@ -70,7 +72,13 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
processHandle = ProcessHandle.create();
|
processHandle = ProcessHandle.create();
|
||||||
Store.instance.addProcessSecret(processHandle.processSecret);
|
Store.instance.addProcessSecret(processHandle.processSecret);
|
||||||
|
|
||||||
processHandle.addServer("https://srv1-stg.polycentric.io");
|
try {
|
||||||
|
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
processHandle.addServer(PolycentricCache.SERVER);
|
||||||
processHandle.setUsername(username);
|
processHandle.setUsername(username);
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
|||||||
+7
@@ -13,6 +13,7 @@ import com.futo.platformplayer.R
|
|||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
@@ -126,6 +127,12 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
val processSecret = ProcessSecret(keyPair, Process.random());
|
val processSecret = ProcessSecret(keyPair, Process.random());
|
||||||
Store.instance.addProcessSecret(processSecret);
|
Store.instance.addProcessSecret(processSecret);
|
||||||
|
|
||||||
|
try {
|
||||||
|
PolycentricStorage.instance.addProcessSecret(processSecret)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
|
}
|
||||||
|
|
||||||
val processHandle = processSecret.toProcessHandle();
|
val processHandle = processSecret.toProcessHandle();
|
||||||
|
|
||||||
for (e in exportBundle.events.eventsList) {
|
for (e in exportBundle.events.eventsList) {
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ class CachedPlatformClient : IPlatformClient {
|
|||||||
filters: Map<String, List<String>>?
|
filters: Map<String, List<String>>?
|
||||||
): IPager<IPlatformContent> = _client.getChannelContents(channelUrl);
|
): IPager<IPlatformContent> = _client.getChannelContents(channelUrl);
|
||||||
|
|
||||||
|
override fun getPeekChannelTypes(): List<String> = _client.getPeekChannelTypes();
|
||||||
|
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = _client.peekChannelContents(channelUrl, type);
|
||||||
|
|
||||||
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String? = _client.getChannelUrlByClaim(claimType, claimValues)
|
override fun getChannelUrlByClaim(claimType: Int, claimValues: Map<Int, String>): String? = _client.getChannelUrlByClaim(claimType, claimValues)
|
||||||
|
|
||||||
override fun searchSuggestions(query: String): Array<String> = _client.searchSuggestions(query);
|
override fun searchSuggestions(query: String): Array<String> = _client.searchSuggestions(query);
|
||||||
|
|||||||
@@ -84,6 +84,15 @@ interface IPlatformClient {
|
|||||||
*/
|
*/
|
||||||
fun getChannelContents(channelUrl: String, type: String? = null, order: String? = null, filters: Map<String, List<String>>? = null): IPager<IPlatformContent>;
|
fun getChannelContents(channelUrl: String, type: String? = null, order: String? = null, filters: Map<String, List<String>>? = null): IPager<IPlatformContent>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes what the plugin is capable on peek channel results
|
||||||
|
*/
|
||||||
|
fun getPeekChannelTypes(): List<String>;
|
||||||
|
/**
|
||||||
|
* Peeks contents of a channel, upload time descending
|
||||||
|
*/
|
||||||
|
fun peekChannelContents(channelUrl: String, type: String? = null): List<IPlatformContent>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the channel url associated with a claimType
|
* Gets the channel url associated with a claimType
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ data class PlatformClientCapabilities(
|
|||||||
val hasGetChannelUrlByClaim: Boolean = false,
|
val hasGetChannelUrlByClaim: Boolean = false,
|
||||||
val hasGetChannelTemplateByClaimMap: Boolean = false,
|
val hasGetChannelTemplateByClaimMap: Boolean = false,
|
||||||
val hasGetSearchCapabilities: Boolean = false,
|
val hasGetSearchCapabilities: Boolean = false,
|
||||||
|
val hasGetSearchChannelContentsCapabilities: Boolean = false,
|
||||||
val hasGetChannelCapabilities: Boolean = false,
|
val hasGetChannelCapabilities: Boolean = false,
|
||||||
val hasGetLiveEvents: Boolean = false,
|
val hasGetLiveEvents: Boolean = false,
|
||||||
val hasGetLiveChatWindow: Boolean = false,
|
val hasGetLiveChatWindow: Boolean = false,
|
||||||
val hasGetContentChapters: Boolean = false
|
val hasGetContentChapters: Boolean = false,
|
||||||
|
val hasPeekChannelContents: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ open class PlatformAuthorLink {
|
|||||||
val id: PlatformID;
|
val id: PlatformID;
|
||||||
val name: String;
|
val name: String;
|
||||||
val url: String;
|
val url: String;
|
||||||
val thumbnail: String?;
|
var thumbnail: String?;
|
||||||
var subscribers: Long? = null; //Optional
|
var subscribers: Long? = null; //Optional
|
||||||
|
|
||||||
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null)
|
constructor(id: PlatformID, name: String, url: String, thumbnail: String? = null, subscribers: Long? = null)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.futo.platformplayer.api.media.models
|
|||||||
import com.caoccao.javet.values.reference.V8ValueArray
|
import com.caoccao.javet.values.reference.V8ValueArray
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8PluginConfig
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
@@ -31,7 +33,7 @@ class Thumbnails {
|
|||||||
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
|
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnails {
|
||||||
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
|
return Thumbnails((value.getOrThrow<V8ValueArray>(config, "sources", "Thumbnails"))
|
||||||
.toArray()
|
.toArray()
|
||||||
.map { Thumbnail.fromV8(it as V8ValueObject) }
|
.map { Thumbnail.fromV8(config, it as V8ValueObject) }
|
||||||
.toTypedArray());
|
.toTypedArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,10 +42,10 @@ class Thumbnails {
|
|||||||
data class Thumbnail(val url : String?, val quality : Int = 0) {
|
data class Thumbnail(val url : String?, val quality : Int = 0) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromV8(value: V8ValueObject): Thumbnail {
|
fun fromV8(config: IV8PluginConfig, value: V8ValueObject): Thumbnail {
|
||||||
return Thumbnail(
|
return Thumbnail(
|
||||||
value.getString("url"),
|
value.getOrDefault<String>(config,"url", "Thumbnail", null),
|
||||||
value.getInteger("quality"));
|
value.getOrDefault(config, "quality", "Thumbnail", 0) ?: 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
+4
@@ -37,6 +37,10 @@ class SerializedChannel(
|
|||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isSameUrl(url: String): Boolean {
|
||||||
|
return this.url == url || urlAlternatives.contains(url);
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromChannel(channel: IPlatformChannel): SerializedChannel {
|
fun fromChannel(channel: IPlatformChannel): SerializedChannel {
|
||||||
return SerializedChannel(
|
return SerializedChannel(
|
||||||
|
|||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.streams.sources
|
||||||
|
|
||||||
|
interface IAudioUrlWidevineSource : IAudioUrlSource {
|
||||||
|
val bearerToken: String
|
||||||
|
val licenseUri: String
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ import com.futo.platformplayer.api.media.platforms.js.internal.JSDocsParameter
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSOptional
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSOptional
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSParameterDocs
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.IJSContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChannel
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
|
import com.futo.platformplayer.api.media.platforms.js.models.JSChannelPager
|
||||||
@@ -45,6 +46,7 @@ import com.futo.platformplayer.constructs.Event2
|
|||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
import com.futo.platformplayer.engine.exceptions.PluginEngineException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
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.ScriptImplementationException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
import com.futo.platformplayer.engine.exceptions.ScriptValidationException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -56,8 +58,10 @@ import com.futo.platformplayer.states.StatePlugins
|
|||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
import kotlin.Exception
|
||||||
import kotlin.reflect.full.findAnnotations
|
import kotlin.reflect.full.findAnnotations
|
||||||
import kotlin.reflect.jvm.kotlinFunction
|
import kotlin.reflect.jvm.kotlinFunction
|
||||||
|
import kotlin.streams.asSequence
|
||||||
|
|
||||||
open class JSClient : IPlatformClient {
|
open class JSClient : IPlatformClient {
|
||||||
val config: SourcePluginConfig;
|
val config: SourcePluginConfig;
|
||||||
@@ -73,6 +77,7 @@ open class JSClient : IPlatformClient {
|
|||||||
private var _searchCapabilities: ResultCapabilities? = null;
|
private var _searchCapabilities: ResultCapabilities? = null;
|
||||||
private var _searchChannelContentsCapabilities: ResultCapabilities? = null;
|
private var _searchChannelContentsCapabilities: ResultCapabilities? = null;
|
||||||
private var _channelCapabilities: ResultCapabilities? = null;
|
private var _channelCapabilities: ResultCapabilities? = null;
|
||||||
|
private var _peekChannelTypes: List<String>? = null;
|
||||||
|
|
||||||
protected val _script: String;
|
protected val _script: String;
|
||||||
|
|
||||||
@@ -91,7 +96,11 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
private val _busyLock = Object();
|
private val _busyLock = Object();
|
||||||
private var _busyCounter = 0;
|
private var _busyCounter = 0;
|
||||||
|
private var _busyAction = "";
|
||||||
val isBusy: Boolean get() = _busyCounter > 0;
|
val isBusy: Boolean get() = _busyCounter > 0;
|
||||||
|
val isBusyAction: String get() {
|
||||||
|
return _busyAction;
|
||||||
|
}
|
||||||
|
|
||||||
val settings: HashMap<String, String?> get() = descriptor.settings;
|
val settings: HashMap<String, String?> get() = descriptor.settings;
|
||||||
|
|
||||||
@@ -150,6 +159,8 @@ open class JSClient : IPlatformClient {
|
|||||||
if(it is ScriptCaptchaRequiredException)
|
if(it is ScriptCaptchaRequiredException)
|
||||||
onCaptchaException.emit(this, it);
|
onCaptchaException.emit(this, it);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||||
}
|
}
|
||||||
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
constructor(context: Context, descriptor: SourcePluginDescriptor, saveState: String?, script: String) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
@@ -173,6 +184,8 @@ open class JSClient : IPlatformClient {
|
|||||||
if(it is ScriptCaptchaRequiredException)
|
if(it is ScriptCaptchaRequiredException)
|
||||||
onCaptchaException.emit(this, it);
|
onCaptchaException.emit(this, it);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_plugin.changeAllowDevSubmit(descriptor.appSettings.allowDeveloperSubmit);
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getCopy(): JSClient {
|
open fun getCopy(): JSClient {
|
||||||
@@ -214,9 +227,11 @@ open class JSClient : IPlatformClient {
|
|||||||
hasGetChannelTemplateByClaimMap = plugin.executeBoolean("!!source.getChannelTemplateByClaimMap") ?: false,
|
hasGetChannelTemplateByClaimMap = plugin.executeBoolean("!!source.getChannelTemplateByClaimMap") ?: false,
|
||||||
hasGetSearchCapabilities = plugin.executeBoolean("!!source.getSearchCapabilities") ?: false,
|
hasGetSearchCapabilities = plugin.executeBoolean("!!source.getSearchCapabilities") ?: false,
|
||||||
hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false,
|
hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false,
|
||||||
|
hasGetSearchChannelContentsCapabilities = plugin.executeBoolean("!!source.getSearchChannelContentsCapabilities") ?: false,
|
||||||
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
|
hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false,
|
||||||
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
|
hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false,
|
||||||
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false,
|
||||||
|
hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -260,7 +275,7 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform")
|
@JSDocs(2, "source.getHome()", "Gets the HomeFeed of the platform")
|
||||||
override fun getHome(): IPager<IPlatformContent> = isBusyWith {
|
override fun getHome(): IPager<IPlatformContent> = isBusyWith("getHome") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSContentPager(config, this,
|
return@isBusyWith JSContentPager(config, this,
|
||||||
plugin.executeTyped("source.getHome()"));
|
plugin.executeTyped("source.getHome()"));
|
||||||
@@ -268,7 +283,7 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
|
@JSDocs(3, "source.searchSuggestions(query)", "Gets search suggestions for a given query")
|
||||||
@JSDocsParameter("query", "Query to complete suggestions for")
|
@JSDocsParameter("query", "Query to complete suggestions for")
|
||||||
override fun searchSuggestions(query: String): Array<String> = isBusyWith {
|
override fun searchSuggestions(query: String): Array<String> = isBusyWith("searchSuggestions") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith plugin.executeTyped<V8ValueArray>("source.searchSuggestions(${Json.encodeToString(query)})")
|
return@isBusyWith plugin.executeTyped<V8ValueArray>("source.searchSuggestions(${Json.encodeToString(query)})")
|
||||||
.toArray()
|
.toArray()
|
||||||
@@ -298,7 +313,7 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||||
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
||||||
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
override fun search(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("search") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSContentPager(config, this,
|
return@isBusyWith JSContentPager(config, this,
|
||||||
plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
plugin.executeTyped("source.search(${Json.encodeToString(query)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||||
@@ -306,6 +321,9 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
@JSDocs(4, "source.getSearchChannelContentsCapabilities()", "Gets capabilities this plugin has for search videos")
|
@JSDocs(4, "source.getSearchChannelContentsCapabilities()", "Gets capabilities this plugin has for search videos")
|
||||||
override fun getSearchChannelContentsCapabilities(): ResultCapabilities {
|
override fun getSearchChannelContentsCapabilities(): ResultCapabilities {
|
||||||
|
if(!capabilities.hasGetSearchChannelContentsCapabilities)
|
||||||
|
return ResultCapabilities(listOf(ResultCapabilities.TYPE_MIXED));
|
||||||
|
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
if (_searchChannelContentsCapabilities != null)
|
if (_searchChannelContentsCapabilities != null)
|
||||||
return _searchChannelContentsCapabilities!!;
|
return _searchChannelContentsCapabilities!!;
|
||||||
@@ -319,7 +337,7 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
|
@JSDocsParameter("type", "(optional) Type of contents to get from search ")
|
||||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||||
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
override fun searchChannelContents(channelUrl: String, query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchChannelContents") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
if(!capabilities.hasSearchChannelContents)
|
if(!capabilities.hasSearchChannelContents)
|
||||||
throw IllegalStateException("This plugin does not support channel search");
|
throw IllegalStateException("This plugin does not support channel search");
|
||||||
@@ -331,7 +349,7 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSOptional
|
@JSOptional
|
||||||
@JSDocs(5, "source.searchChannels(query)", "Searches for channels on the platform")
|
@JSDocs(5, "source.searchChannels(query)", "Searches for channels on the platform")
|
||||||
@JSDocsParameter("query", "Query that channels should match")
|
@JSDocsParameter("query", "Query that channels should match")
|
||||||
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith {
|
override fun searchChannels(query: String): IPager<PlatformAuthorLink> = isBusyWith("searchChannels") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSChannelPager(config, this,
|
return@isBusyWith JSChannelPager(config, this,
|
||||||
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
plugin.executeTyped("source.searchChannels(${Json.encodeToString(query)})"));
|
||||||
@@ -351,7 +369,7 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
|
@JSDocs(7, "source.getChannel(channelUrl)", "Gets a channel by its url")
|
||||||
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
||||||
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith {
|
override fun getChannel(channelUrl: String): IPlatformChannel = isBusyWith("getChannel") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSChannel(config,
|
return@isBusyWith JSChannel(config,
|
||||||
plugin.executeTyped("source.getChannel(${Json.encodeToString(channelUrl)})"));
|
plugin.executeTyped("source.getChannel(${Json.encodeToString(channelUrl)})"));
|
||||||
@@ -378,12 +396,46 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
||||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||||
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
override fun getChannelContents(channelUrl: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("getChannelContents") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSContentPager(config, this,
|
return@isBusyWith JSContentPager(config, this,
|
||||||
plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
plugin.executeTyped("source.getChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)}, ${Json.encodeToString(order)}, ${Json.encodeToString(filters)})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JSDocs(10, "source.getPeekChannelTypes()", "Gets types this plugin has for peek channel contents")
|
||||||
|
override fun getPeekChannelTypes(): List<String> {
|
||||||
|
if(!capabilities.hasPeekChannelContents)
|
||||||
|
return listOf();
|
||||||
|
try {
|
||||||
|
if (_peekChannelTypes != null) {
|
||||||
|
return _peekChannelTypes!!;
|
||||||
|
}
|
||||||
|
val arr: V8ValueArray = plugin.executeTyped("source.getPeekChannelTypes()");
|
||||||
|
|
||||||
|
_peekChannelTypes = arr.keys.mapNotNull {
|
||||||
|
val str = arr.get<V8ValueString>(it);
|
||||||
|
return@mapNotNull str.value;
|
||||||
|
};
|
||||||
|
return _peekChannelTypes ?: listOf();
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
announcePluginUnhandledException("getPeekChannelTypes", ex);
|
||||||
|
return listOf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@JSDocs(10, "source.peekChannelContents(url, type)", "Peek contents of a channel (reverse chronological order)")
|
||||||
|
@JSDocsParameter("channelUrl", "A channel url (this platform)")
|
||||||
|
@JSDocsParameter("type", "(optional) Type of contents to get from channel")
|
||||||
|
override fun peekChannelContents(channelUrl: String, type: String?): List<IPlatformContent> = isBusyWith("peekChannelContents") {
|
||||||
|
ensureEnabled();
|
||||||
|
|
||||||
|
val items: V8ValueArray = plugin.executeTyped("source.peekChannelContents(${Json.encodeToString(channelUrl)}, ${Json.encodeToString(type)})");
|
||||||
|
return@isBusyWith items.keys.mapNotNull {
|
||||||
|
val obj = items.get<V8ValueObject>(it);
|
||||||
|
return@mapNotNull IJSContent.fromV8(this, obj);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@JSOptional
|
@JSOptional
|
||||||
@JSDocs(11, "source.getChannelUrlByClaim(claimType, claimValues)", "Gets the channel url that should be used to fetch a given polycentric claim")
|
@JSDocs(11, "source.getChannelUrlByClaim(claimType, claimValues)", "Gets the channel url that should be used to fetch a given polycentric claim")
|
||||||
@JSDocsParameter("claimType", "Polycentric claimtype id")
|
@JSDocsParameter("claimType", "Polycentric claimtype id")
|
||||||
@@ -444,7 +496,7 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
|
@JSDocs(14, "source.getContentDetails(url)", "Gets content details by its url")
|
||||||
@JSDocsParameter("url", "A content url (this platform)")
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith {
|
override fun getContentDetails(url: String): IPlatformContentDetails = isBusyWith("getContentDetails") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith IJSContentDetails.fromV8(this,
|
return@isBusyWith IJSContentDetails.fromV8(this,
|
||||||
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
|
plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})"));
|
||||||
@@ -453,7 +505,7 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSOptional //getContentChapters = function(url, initialData)
|
@JSOptional //getContentChapters = function(url, initialData)
|
||||||
@JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details")
|
@JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details")
|
||||||
@JSDocsParameter("url", "A content url (this platform)")
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
override fun getContentChapters(url: String): List<IChapter> = isBusyWith {
|
override fun getContentChapters(url: String): List<IChapter> = isBusyWith("getContentChapters") {
|
||||||
if(!capabilities.hasGetContentChapters)
|
if(!capabilities.hasGetContentChapters)
|
||||||
return@isBusyWith listOf();
|
return@isBusyWith listOf();
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
@@ -464,7 +516,7 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSOptional
|
@JSOptional
|
||||||
@JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url")
|
@JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url")
|
||||||
@JSDocsParameter("url", "A content url (this platform)")
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith {
|
override fun getPlaybackTracker(url: String): IPlaybackTracker? = isBusyWith("getPlaybackTracker") {
|
||||||
if(!capabilities.hasGetPlaybackTracker)
|
if(!capabilities.hasGetPlaybackTracker)
|
||||||
return@isBusyWith null;
|
return@isBusyWith null;
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
@@ -478,7 +530,7 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
@JSDocs(16, "source.getComments(url)", "Gets comments for a content by its url")
|
@JSDocs(16, "source.getComments(url)", "Gets comments for a content by its url")
|
||||||
@JSDocsParameter("url", "A content url (this platform)")
|
@JSDocsParameter("url", "A content url (this platform)")
|
||||||
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith {
|
override fun getComments(url: String): IPager<IPlatformComment> = isBusyWith("getComments") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})");
|
val pager = plugin.executeTyped<V8Value>("source.getComments(${Json.encodeToString(url)})");
|
||||||
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
|
if (pager !is V8ValueObject) { //TODO: Maybe solve this better
|
||||||
@@ -496,7 +548,7 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
@JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
|
@JSDocs(16, "source.getLiveChatWindow(url)", "Gets live events for a livestream")
|
||||||
@JSDocsParameter("url", "Url of live stream")
|
@JSDocsParameter("url", "Url of live stream")
|
||||||
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith {
|
override fun getLiveChatWindow(url: String): ILiveChatWindowDescriptor? = isBusyWith("getLiveChatWindow") {
|
||||||
if(!capabilities.hasGetLiveChatWindow)
|
if(!capabilities.hasGetLiveChatWindow)
|
||||||
return@isBusyWith null;
|
return@isBusyWith null;
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
@@ -505,7 +557,7 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
@JSDocs(16, "source.getLiveEvents(url)", "Gets live events for a livestream")
|
@JSDocs(16, "source.getLiveEvents(url)", "Gets live events for a livestream")
|
||||||
@JSDocsParameter("url", "Url of live stream")
|
@JSDocsParameter("url", "Url of live stream")
|
||||||
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith {
|
override fun getLiveEvents(url: String): IPager<IPlatformLiveEvent>? = isBusyWith("getLiveEvents") {
|
||||||
if(!capabilities.hasGetLiveEvents)
|
if(!capabilities.hasGetLiveEvents)
|
||||||
return@isBusyWith null;
|
return@isBusyWith null;
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
@@ -518,7 +570,7 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
@JSDocsParameter("order", "(optional) Order in which contents should be returned")
|
||||||
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
@JSDocsParameter("filters", "(optional) Filters to apply on contents")
|
||||||
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
@JSDocsParameter("channelId", "(optional) Channel id to search in")
|
||||||
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith {
|
override fun searchPlaylists(query: String, type: String?, order: String?, filters: Map<String, List<String>>?): IPager<IPlatformContent> = isBusyWith("searchPlaylists") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
if(!capabilities.hasSearchPlaylists)
|
if(!capabilities.hasSearchPlaylists)
|
||||||
throw IllegalStateException("This plugin does not support playlist search");
|
throw IllegalStateException("This plugin does not support playlist search");
|
||||||
@@ -528,15 +580,22 @@ open class JSClient : IPlatformClient {
|
|||||||
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
|
@JSDocs(20, "source.isPlaylistUrl(url)", "Validates if a playlist url is for this platform")
|
||||||
@JSDocsParameter("url", "Url of playlist")
|
@JSDocsParameter("url", "Url of playlist")
|
||||||
override fun isPlaylistUrl(url: String): Boolean {
|
override fun isPlaylistUrl(url: String): Boolean {
|
||||||
ensureEnabled();
|
|
||||||
if (!capabilities.hasGetPlaylist)
|
if (!capabilities.hasGetPlaylist)
|
||||||
return false;
|
return false;
|
||||||
return plugin.executeBoolean("source.isPlaylistUrl(${Json.encodeToString(url)})") ?: false;
|
|
||||||
|
try {
|
||||||
|
return plugin.executeTyped<V8ValueBoolean>("source.isPlaylistUrl(${Json.encodeToString(url)})")
|
||||||
|
.value;
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
announcePluginUnhandledException("isPlaylistUrl", ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@JSOptional
|
@JSOptional
|
||||||
@JSDocs(21, "source.getPlaylist(url)", "Gets the playlist of the current user")
|
@JSDocs(21, "source.getPlaylist(url)", "Gets the playlist of the current user")
|
||||||
@JSDocsParameter("url", "Url of playlist")
|
@JSDocsParameter("url", "Url of playlist")
|
||||||
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith {
|
override fun getPlaylist(url: String): IPlatformPlaylistDetails = isBusyWith("getPlaylist") {
|
||||||
ensureEnabled();
|
ensureEnabled();
|
||||||
return@isBusyWith JSPlaylistDetails(this, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
|
return@isBusyWith JSPlaylistDetails(this, plugin.config as SourcePluginConfig, plugin.executeTyped("source.getPlaylist(${Json.encodeToString(url)})"));
|
||||||
}
|
}
|
||||||
@@ -633,19 +692,24 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun <T> isBusyWith(handle: ()->T): T {
|
private fun <T> isBusyWith(actionName: String, handle: ()->T): T {
|
||||||
try {
|
try {
|
||||||
synchronized(_busyLock) {
|
synchronized(_busyLock) {
|
||||||
_busyCounter++;
|
_busyCounter++;
|
||||||
}
|
}
|
||||||
|
_busyAction = actionName;
|
||||||
return handle();
|
return handle();
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
_busyAction = "";
|
||||||
synchronized(_busyLock) {
|
synchronized(_busyLock) {
|
||||||
_busyCounter--;
|
_busyCounter--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private fun <T> isBusyWith(handle: ()->T): T {
|
||||||
|
return isBusyWith("Unknown", handle);
|
||||||
|
}
|
||||||
|
|
||||||
private fun announcePluginUnhandledException(method: String, ex: Throwable) {
|
private fun announcePluginUnhandledException(method: String, ex: Throwable) {
|
||||||
if(ex is PluginEngineException)
|
if(ex is PluginEngineException)
|
||||||
@@ -662,10 +726,43 @@ open class JSClient : IPlatformClient {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "JSClient";
|
val TAG = "JSClient";
|
||||||
|
private val _lock = Object();
|
||||||
|
private var _docs: Map<String, String>? = null;
|
||||||
|
|
||||||
|
fun getMethodDocs(names: List<String>): Map<String, String>? {
|
||||||
|
synchronized(_lock) {
|
||||||
|
if(_docs == null) {
|
||||||
|
val client = ManagedHttpClient();
|
||||||
|
val docs = names
|
||||||
|
.map { stringWithoutBrackets(it) }
|
||||||
|
.distinct()
|
||||||
|
.parallelStream()
|
||||||
|
.map {
|
||||||
|
val url = "https://github.com/futo-org/grayjay-android/blob/master/docs/source/${it}.md";
|
||||||
|
val resp = client.head(url);
|
||||||
|
if(resp.isOk)
|
||||||
|
return@map Pair(it, url);
|
||||||
|
else
|
||||||
|
return@map null;
|
||||||
|
}.asSequence()
|
||||||
|
.filterNotNull()
|
||||||
|
.toMap();
|
||||||
|
_docs = docs;
|
||||||
|
}
|
||||||
|
return _docs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun getMethodDocUrls(): Map<String, String>? {
|
||||||
|
if(_docs != null)
|
||||||
|
return _docs;
|
||||||
|
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
|
||||||
|
return getMethodDocs(methods.map { it.name });
|
||||||
|
}
|
||||||
|
|
||||||
fun getJSDocs(): List<JSCallDocs> {
|
fun getJSDocs(): List<JSCallDocs> {
|
||||||
val docs = mutableListOf<JSCallDocs>();
|
val docs = mutableListOf<JSCallDocs>();
|
||||||
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
|
val methods = JSClient::class.java.declaredMethods.filter { it.getAnnotation(JSDocs::class.java) != null }
|
||||||
|
|
||||||
for(method in methods.sortedBy { it.getAnnotation(JSDocs::class.java)?.order }) {
|
for(method in methods.sortedBy { it.getAnnotation(JSDocs::class.java)?.order }) {
|
||||||
val doc = method.getAnnotation(JSDocs::class.java);
|
val doc = method.getAnnotation(JSDocs::class.java);
|
||||||
val parameters = method.kotlinFunction!!.findAnnotations<JSDocsParameter>();
|
val parameters = method.kotlinFunction!!.findAnnotations<JSDocsParameter>();
|
||||||
@@ -678,5 +775,12 @@ open class JSClient : IPlatformClient {
|
|||||||
}
|
}
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun stringWithoutBrackets(name: String): String {
|
||||||
|
val index = name.indexOf('(');
|
||||||
|
if(index >= 0)
|
||||||
|
return name.substring(0, index);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+46
-1
@@ -45,7 +45,9 @@ class SourcePluginConfig(
|
|||||||
var enableInSearch: Boolean = true,
|
var enableInSearch: Boolean = true,
|
||||||
var enableInHome: Boolean = true,
|
var enableInHome: Boolean = true,
|
||||||
var supportedClaimTypes: List<Int> = listOf(),
|
var supportedClaimTypes: List<Int> = listOf(),
|
||||||
var primaryClaimFieldType: Int? = null
|
var primaryClaimFieldType: Int? = null,
|
||||||
|
var developerSubmitUrl: String? = null,
|
||||||
|
var allowAllHttpHeaderAccess: Boolean = false,
|
||||||
) : IV8PluginConfig {
|
) : IV8PluginConfig {
|
||||||
|
|
||||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||||
@@ -79,6 +81,44 @@ class SourcePluginConfig(
|
|||||||
return _allowUrlsLowerVal!!;
|
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>> {
|
fun getWarnings(scriptToCheck: String? = null) : List<Pair<String,String>> {
|
||||||
val list = mutableListOf<Pair<String,String>>();
|
val list = mutableListOf<Pair<String,String>>();
|
||||||
|
|
||||||
@@ -107,6 +147,11 @@ class SourcePluginConfig(
|
|||||||
list.add(Pair(
|
list.add(Pair(
|
||||||
"Unrestricted Web Access",
|
"Unrestricted Web Access",
|
||||||
"This plugin requires access to all URLs, this may include malicious URLs."));
|
"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;
|
return list;
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -8,6 +8,7 @@ import com.futo.platformplayer.states.StateAnnouncement
|
|||||||
import com.futo.platformplayer.views.fields.DropdownFieldOptions
|
import com.futo.platformplayer.views.fields.DropdownFieldOptions
|
||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.FormField
|
import com.futo.platformplayer.views.fields.FormField
|
||||||
|
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -90,8 +91,10 @@ class SourcePluginDescriptor {
|
|||||||
@Serializable
|
@Serializable
|
||||||
class AppPluginSettings {
|
class AppPluginSettings {
|
||||||
|
|
||||||
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, 1)
|
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, -1)
|
||||||
var checkForUpdates: Boolean = true;
|
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)
|
@FormField(R.string.visibility, "group", R.string.enable_where_this_plugins_content_are_visible, 2)
|
||||||
var tabEnabled = TabEnabled();
|
var tabEnabled = TabEnabled();
|
||||||
@@ -130,6 +133,11 @@ class SourcePluginDescriptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@FormField(R.string.allow_developer_submit, FieldForm.TOGGLE, R.string.allow_developer_submit_description, 1, "devSubmit")
|
||||||
|
var allowDeveloperSubmit: Boolean = false;
|
||||||
|
|
||||||
|
|
||||||
fun loadDefaults(config: SourcePluginConfig) {
|
fun loadDefaults(config: SourcePluginConfig) {
|
||||||
if(tabEnabled.enableHome == null)
|
if(tabEnabled.enableHome == null)
|
||||||
tabEnabled.enableHome = config.enableInHome
|
tabEnabled.enableHome = config.enableInHome
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ annotation class JSOptional()
|
|||||||
annotation class JSDocsParameter(val name: String, val description: String, val order: Int = 0)
|
annotation class JSDocsParameter(val name: String, val description: String, val order: Int = 0)
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false);
|
data class JSCallDocs(val title: String, val code: String, val description: String, val parameters: List<JSParameterDocs>, val isOptional: Boolean = false, val docsUrl: String? = null);
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
data class JSParameterDocs(val name: String, val description: String);
|
data class JSParameterDocs(val name: String, val description: String);
|
||||||
+27
-1
@@ -2,14 +2,22 @@ package com.futo.platformplayer.api.media.platforms.js.internal
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
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.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
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.SourceCaptchaData
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
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.JSRequest
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
|
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.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.matchesDomain
|
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
|
import java.util.UUID
|
||||||
|
|
||||||
class JSHttpClient : ManagedHttpClient {
|
class JSHttpClient : ManagedHttpClient {
|
||||||
@@ -28,7 +36,15 @@ class JSHttpClient : ManagedHttpClient {
|
|||||||
private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
|
private var _currentCookieMap: HashMap<String, HashMap<String, String>>;
|
||||||
private var _otherCookieMap: 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;
|
_jsClient = jsClient;
|
||||||
_jsConfig = config;
|
_jsConfig = config;
|
||||||
_auth = auth;
|
_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;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||||
|
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlWidevineSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
|
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
|
||||||
|
override val bearerToken: String
|
||||||
|
override val licenseUri: String
|
||||||
|
|
||||||
|
@Suppress("ConvertSecondaryConstructorToPrimary")
|
||||||
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
|
||||||
|
val contextName = "JSAudioUrlWidevineSource"
|
||||||
|
val config = plugin.config
|
||||||
|
bearerToken = _obj.getOrThrow(config, "bearerToken", contextName)
|
||||||
|
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
val url = getAudioUrl()
|
||||||
|
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration, bearerToken=$bearerToken, licenseUri=$licenseUri)"
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -66,6 +66,7 @@ abstract class JSSource {
|
|||||||
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
|
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
|
||||||
const val TYPE_DASH = "DashSource";
|
const val TYPE_DASH = "DashSource";
|
||||||
const val TYPE_HLS = "HLSSource";
|
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 fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
|
||||||
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource {
|
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource {
|
||||||
@@ -88,6 +89,7 @@ abstract class JSSource {
|
|||||||
return when(type) {
|
return when(type) {
|
||||||
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
|
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
|
||||||
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
|
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
|
||||||
|
TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(plugin, obj);
|
||||||
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
|
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
|
||||||
else -> throw NotImplementedError("Unknown type ${type}");
|
else -> throw NotImplementedError("Unknown type ${type}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,24 +163,25 @@ class AirPlayCastingDevice : CastingDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
connectionState = CastConnectionState.CONNECTED;
|
connectionState = CastConnectionState.CONNECTED;
|
||||||
delay(1000);
|
|
||||||
|
|
||||||
val progressIndex = progressInfo.lowercase().indexOf("position: ");
|
val progressIndex = progressInfo.lowercase().indexOf("position: ");
|
||||||
if (progressIndex == -1) {
|
if (progressIndex == -1) {
|
||||||
|
delay(1000);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
val progress = progressInfo.substring(progressIndex + "position: ".length).toDoubleOrNull() ?: continue;
|
val progress = progressInfo.substring(progressIndex + "position: ".length).toDoubleOrNull() ?: continue;
|
||||||
setTime(progress);
|
setTime(progress);
|
||||||
|
|
||||||
|
|
||||||
val durationIndex = progressInfo.lowercase().indexOf("duration: ");
|
val durationIndex = progressInfo.lowercase().indexOf("duration: ");
|
||||||
if (durationIndex == -1) {
|
if (durationIndex == -1) {
|
||||||
|
delay(1000);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
val duration = progressInfo.substring(durationIndex + "duration: ".length).toDoubleOrNull() ?: continue;
|
val duration = progressInfo.substring(durationIndex + "duration: ".length).toDoubleOrNull() ?: continue;
|
||||||
setDuration(duration);
|
setDuration(duration);
|
||||||
|
delay(1000);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to get server info from AirPlay device.", e)
|
Logger.w(TAG, "Failed to get server info from AirPlay device.", e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
|
|
||||||
private var _socket: SSLSocket? = null;
|
private var _socket: SSLSocket? = null;
|
||||||
private var _outputStream: DataOutputStream? = null;
|
private var _outputStream: DataOutputStream? = null;
|
||||||
|
private var _outputStreamLock = Object();
|
||||||
private var _inputStream: DataInputStream? = null;
|
private var _inputStream: DataInputStream? = null;
|
||||||
|
private var _inputStreamLock = Object();
|
||||||
private var _scopeIO: CoroutineScope? = null;
|
private var _scopeIO: CoroutineScope? = null;
|
||||||
private var _requestId = 1;
|
private var _requestId = 1;
|
||||||
private var _started: Boolean = false;
|
private var _started: Boolean = false;
|
||||||
@@ -383,39 +385,44 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
|
|
||||||
getStatus();
|
getStatus();
|
||||||
|
|
||||||
val buffer = ByteArray(4096);
|
val buffer = ByteArray(409600);
|
||||||
|
|
||||||
Logger.i(TAG, "Started receiving.");
|
Logger.i(TAG, "Started receiving.");
|
||||||
while (_scopeIO?.isActive == true) {
|
while (_scopeIO?.isActive == true) {
|
||||||
try {
|
try {
|
||||||
val inputStream = _inputStream ?: break;
|
val inputStream = _inputStream ?: break;
|
||||||
Log.d(TAG, "Receiving next packet...");
|
|
||||||
val b1 = inputStream.readUnsignedByte();
|
|
||||||
val b2 = inputStream.readUnsignedByte();
|
|
||||||
val b3 = inputStream.readUnsignedByte();
|
|
||||||
val b4 = inputStream.readUnsignedByte();
|
|
||||||
val size = ((b1.toLong() shl 24) or (b2.toLong() shl 16) or (b3.toLong() shl 8) or b4.toLong()).toInt();
|
|
||||||
if (size > buffer.size) {
|
|
||||||
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
|
|
||||||
inputStream.skip(size.toLong());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
synchronized(_inputStreamLock)
|
||||||
inputStream.read(buffer, 0, size);
|
{
|
||||||
|
Log.d(TAG, "Receiving next packet...");
|
||||||
|
val b1 = inputStream.readUnsignedByte();
|
||||||
|
val b2 = inputStream.readUnsignedByte();
|
||||||
|
val b3 = inputStream.readUnsignedByte();
|
||||||
|
val b4 = inputStream.readUnsignedByte();
|
||||||
|
val size =
|
||||||
|
((b1.toLong() shl 24) or (b2.toLong() shl 16) or (b3.toLong() shl 8) or b4.toLong()).toInt();
|
||||||
|
if (size > buffer.size) {
|
||||||
|
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
|
||||||
|
inputStream.skip(size.toLong());
|
||||||
|
return@synchronized
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
|
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
||||||
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
|
inputStream.read(buffer, 0, size);
|
||||||
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
|
||||||
val message = ChromeCast.CastMessage.parseFrom(messageBytes);
|
|
||||||
if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
|
|
||||||
Logger.i(TAG, "Received message: $message");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
|
||||||
handleMessage(message);
|
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
|
||||||
} catch (e:Throwable) {
|
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
||||||
Logger.w(TAG, "Failed to handle message.", e);
|
val message = ChromeCast.CastMessage.parseFrom(messageBytes);
|
||||||
|
if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
|
||||||
|
Logger.i(TAG, "Received message: $message");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
handleMessage(message);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to handle message.", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: java.net.SocketException) {
|
} catch (e: java.net.SocketException) {
|
||||||
Logger.e(TAG, "Socket exception while receiving.", e);
|
Logger.e(TAG, "Socket exception while receiving.", e);
|
||||||
@@ -588,13 +595,16 @@ class ChromecastCastingDevice : CastingDevice {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
val serializedSizeBE = ByteArray(4);
|
synchronized(_outputStreamLock)
|
||||||
serializedSizeBE[0] = (data.size shr 24 and 0xff).toByte();
|
{
|
||||||
serializedSizeBE[1] = (data.size shr 16 and 0xff).toByte();
|
val serializedSizeBE = ByteArray(4);
|
||||||
serializedSizeBE[2] = (data.size shr 8 and 0xff).toByte();
|
serializedSizeBE[0] = (data.size shr 24 and 0xff).toByte();
|
||||||
serializedSizeBE[3] = (data.size and 0xff).toByte();
|
serializedSizeBE[1] = (data.size shr 16 and 0xff).toByte();
|
||||||
outputStream.write(serializedSizeBE);
|
serializedSizeBE[2] = (data.size shr 8 and 0xff).toByte();
|
||||||
outputStream.write(data);
|
serializedSizeBE[3] = (data.size and 0xff).toByte();
|
||||||
|
outputStream.write(serializedSizeBE);
|
||||||
|
outputStream.write(data);
|
||||||
|
}
|
||||||
|
|
||||||
//Log.d(TAG, "Sent ${data.size} bytes.");
|
//Log.d(TAG, "Sent ${data.size} bytes.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ class StateCasting {
|
|||||||
jmDNS.addServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
|
jmDNS.addServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
|
||||||
jmDNS.addServiceListener("_airplay._tcp.local.", _airPlayServiceListener);
|
jmDNS.addServiceListener("_airplay._tcp.local.", _airPlayServiceListener);
|
||||||
jmDNS.addServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
|
jmDNS.addServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
|
||||||
|
jmDNS.addServiceListener("_fcast._tcp.local.", _fastCastServiceListener);
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
jmDNS.addServiceTypeListener(_serviceTypeListener);
|
jmDNS.addServiceTypeListener(_serviceTypeListener);
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import com.futo.platformplayer.api.http.server.HttpGET
|
|||||||
import com.futo.platformplayer.api.http.server.HttpPOST
|
import com.futo.platformplayer.api.http.server.HttpPOST
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.engine.dev.V8RemoteObject
|
import com.futo.platformplayer.engine.dev.V8RemoteObject
|
||||||
import com.futo.platformplayer.engine.dev.V8RemoteObject.Companion.gsonStandard
|
import com.futo.platformplayer.engine.dev.V8RemoteObject.Companion.gsonStandard
|
||||||
@@ -20,18 +23,29 @@ import com.futo.platformplayer.states.StateApp
|
|||||||
import com.futo.platformplayer.states.StateAssets
|
import com.futo.platformplayer.states.StateAssets
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.google.gson.ExclusionStrategy
|
||||||
|
import com.google.gson.FieldAttributes
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonElement
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import kotlin.reflect.full.findAnnotation
|
||||||
|
import kotlin.reflect.full.memberFunctions
|
||||||
|
import kotlin.reflect.jvm.javaType
|
||||||
import kotlin.reflect.jvm.jvmErasure
|
import kotlin.reflect.jvm.jvmErasure
|
||||||
|
|
||||||
class DeveloperEndpoints(private val context: Context) {
|
class DeveloperEndpoints(private val context: Context) {
|
||||||
private val TAG = "DeveloperEndpoints";
|
private val TAG = "DeveloperEndpoints";
|
||||||
private val _client = ManagedHttpClient();
|
private val _client = ManagedHttpClient();
|
||||||
private var _testPlugin: V8Plugin? = null;
|
private var _testPlugin: V8Plugin? = null;
|
||||||
|
private var _testPluginFull: JSClient? = null;
|
||||||
private val testPluginOrThrow: V8Plugin get() = _testPlugin ?: throw IllegalStateException("Attempted to use test plugin without plugin");
|
private val testPluginOrThrow: V8Plugin get() = _testPlugin ?: throw IllegalStateException("Attempted to use test plugin without plugin");
|
||||||
private val _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf();
|
private val _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf();
|
||||||
|
|
||||||
@@ -90,15 +104,22 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
@HttpGET("/source_docs.js", "application/javascript")
|
@HttpGET("/source_docs.js", "application/javascript")
|
||||||
val devSourceDocsJS = "const sourceDocs = $devSourceDocsJson";
|
val devSourceDocsJS = "const sourceDocs = $devSourceDocsJson";
|
||||||
|
|
||||||
|
@HttpGET("/source_doc_urls.json", "application/json")
|
||||||
|
fun devSourceDocUrlsJson(httpContext: HttpContext) {;
|
||||||
|
val docs = JSClient.getMethodDocUrls();
|
||||||
|
httpContext.respondCode(200, Json.encodeToString(docs), "application/json");
|
||||||
|
}
|
||||||
|
@HttpGET("/source_doc_urls.js", "application/javascript")
|
||||||
|
fun devSourceDocUrlsJs(httpContext: HttpContext) {;
|
||||||
|
val docs = JSClient.getMethodDocUrls();
|
||||||
|
httpContext.respondCode(200, "const sourceDocUrls = " + Json.encodeToString(docs), "application/javascript");
|
||||||
|
}
|
||||||
|
|
||||||
//Dependencies
|
//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")
|
@HttpGET("/dependencies/FutoMainLogo.svg", "image/svg+xml")
|
||||||
val depFutoLogo = StateAssets.readAsset(context, "devportal/dependencies/FutoMainLogo.svg");
|
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")
|
@HttpGET("/reference_plugin.d.ts", "text/plain")
|
||||||
fun devSourceTSWithRefs(httpContext: HttpContext) {
|
fun devSourceTSWithRefs(httpContext: HttpContext) {
|
||||||
@@ -190,6 +211,17 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
val client = JSHttpClient(null, null, null, config);
|
val client = JSHttpClient(null, null, null, config);
|
||||||
val clientAuth = JSHttpClient(null, null, null, config);
|
val clientAuth = JSHttpClient(null, null, null, config);
|
||||||
_testPlugin = V8Plugin(StateApp.instance.context, config, null, client, clientAuth);
|
_testPlugin = V8Plugin(StateApp.instance.context, config, null, client, clientAuth);
|
||||||
|
try {
|
||||||
|
val script = _client.get(config.absoluteScriptUrl);
|
||||||
|
_testPluginFull = JSClient(StateApp.instance.context, SourcePluginDescriptor(
|
||||||
|
config, null, null, null
|
||||||
|
), null, script.body?.string() ?: "");
|
||||||
|
_testPluginFull!!.initialize();
|
||||||
|
}
|
||||||
|
catch (ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Loading full client failed", ex);
|
||||||
|
_testPluginFull = null;
|
||||||
|
}
|
||||||
|
|
||||||
context.respondJson(200, testPluginOrThrow.getPackageVariables());
|
context.respondJson(200, testPluginOrThrow.getPackageVariables());
|
||||||
}
|
}
|
||||||
@@ -412,6 +444,25 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
context.respondCode(500, ex::class.simpleName + ":" + ex.message, "text/plain")
|
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")
|
@HttpGET("/plugin/getDevLogs")
|
||||||
fun pluginGetDevLogs(context: HttpContext) {
|
fun pluginGetDevLogs(context: HttpContext) {
|
||||||
@@ -423,6 +474,15 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
context.respondCode(500, ex.message ?: "", "text/plain")
|
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")
|
@HttpGET("/plugin/fakeDevLog")
|
||||||
fun pluginFakeDevLog(context: HttpContext) {
|
fun pluginFakeDevLog(context: HttpContext) {
|
||||||
try {
|
try {
|
||||||
@@ -440,6 +500,68 @@ class DeveloperEndpoints(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _fieldAttributesField = FieldAttributes::class.java.getDeclaredField("field");
|
||||||
|
init {
|
||||||
|
_fieldAttributesField.isAccessible = true;
|
||||||
|
}
|
||||||
|
private val _remoteTestGson = GsonBuilder()
|
||||||
|
.setExclusionStrategies(object : ExclusionStrategy {
|
||||||
|
override fun shouldSkipClass(clazz: Class<*>?): Boolean {
|
||||||
|
return clazz?.simpleName == "JSClient" ||
|
||||||
|
clazz?.simpleName == "KSerializer[]" ||
|
||||||
|
clazz?.simpleName == "V8ValueObject";
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldSkipField(f: FieldAttributes?): Boolean {
|
||||||
|
val isPublic = f?.hasModifier(Modifier.PUBLIC) ?: true;
|
||||||
|
if(!isPublic) {
|
||||||
|
val underlyingField = _fieldAttributesField.get(f) as Field;
|
||||||
|
return !(underlyingField.declaringClass as Class).methods.any { it.name == "get" + underlyingField.name.replaceFirstChar { it.uppercaseChar() } && Modifier.isPublic(it.modifiers) };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return !isPublic;
|
||||||
|
}
|
||||||
|
}).create();
|
||||||
|
@HttpPOST("/plugin/remoteTest")
|
||||||
|
fun pluginRemoteTest(context: HttpContext) {
|
||||||
|
val method = context.query.getOrDefault("method", "");
|
||||||
|
try {
|
||||||
|
|
||||||
|
val parameters = context.readContentString();
|
||||||
|
val paras = JsonParser.parseString(parameters);
|
||||||
|
if(!paras.isJsonArray)
|
||||||
|
throw IllegalArgumentException("Expected json array as body");
|
||||||
|
|
||||||
|
val plugin = _testPluginFull ?: throw IllegalStateException("Plugin not loaded");
|
||||||
|
|
||||||
|
val function = plugin::class.memberFunctions.filter { it.findAnnotation<JSDocs>() != null }
|
||||||
|
.find { it.name == method };
|
||||||
|
if(function == null)
|
||||||
|
throw java.lang.IllegalArgumentException("Plugin method [${function}] not found");
|
||||||
|
val callResult = function.call(*(listOf(plugin) + paras.asJsonArray.take(function.parameters.size - 1).mapIndexed { index, jsonElement ->
|
||||||
|
//For now, manual conversion.
|
||||||
|
val parameter = function.parameters[index + 1];
|
||||||
|
val value = _remoteTestGson.fromJson<Any>(jsonElement, parameter.type.javaType);
|
||||||
|
return@mapIndexed value;
|
||||||
|
}).toTypedArray());
|
||||||
|
val json = if(callResult is IPager<*>)
|
||||||
|
_remoteTestGson.toJson(callResult.getResults())
|
||||||
|
else
|
||||||
|
_remoteTestGson.toJson(callResult);
|
||||||
|
//val json = wrapRemoteResult(callResult, false);
|
||||||
|
|
||||||
|
context.respondCode(200, json);
|
||||||
|
}
|
||||||
|
catch(ex: InvocationTargetException) {
|
||||||
|
Logger.e(TAG, "Remote test for [${method}] is failed", ex.targetException);
|
||||||
|
context.respondCode(500, ex.targetException.message ?: "", "text/plain")
|
||||||
|
}
|
||||||
|
catch(ex: Exception) {
|
||||||
|
Logger.e(TAG, "Remote test for [${method}] is failed", ex);
|
||||||
|
context.respondCode(500, ex.message ?: "", "text/plain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Internal calls
|
//Internal calls
|
||||||
@HttpPOST("/get")
|
@HttpPOST("/get")
|
||||||
fun get(context: HttpContext) {
|
fun get(context: HttpContext) {
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
|||||||
Logger.i(TAG, "Cleared InstallReceiver.onReceiveResult handler.")
|
Logger.i(TAG, "Cleared InstallReceiver.onReceiveResult handler.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hideExceptionButtons() {
|
||||||
|
_buttonNever.visibility = View.GONE
|
||||||
|
_buttonShowChangelog.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
private fun update() {
|
private fun update() {
|
||||||
_buttonShowChangelog.visibility = Button.GONE;
|
_buttonShowChangelog.visibility = Button.GONE;
|
||||||
_buttonNever.visibility = Button.GONE;
|
_buttonNever.visibility = Button.GONE;
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ import com.futo.platformplayer.UIDialogs
|
|||||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
import com.futo.platformplayer.assume
|
import com.futo.platformplayer.assume
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateBackup
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -66,13 +68,15 @@ class ImportDialog : AlertDialog {
|
|||||||
private val _name: String;
|
private val _name: String;
|
||||||
private val _toImport: List<String>;
|
private val _toImport: List<String>;
|
||||||
|
|
||||||
|
private val _cache: ImportCache?;
|
||||||
|
|
||||||
constructor(context: Context, importStore: ManagedStore<*>, name: String, toReconstruct: List<String>, onConcluded: ()->Unit): super(context) {
|
constructor(context: Context, importStore: ManagedStore<*>, name: String, toReconstruct: List<String>, cache: ImportCache?, onConcluded: ()->Unit): super(context) {
|
||||||
_context = context;
|
_context = context;
|
||||||
_store = importStore;
|
_store = importStore;
|
||||||
_onConcluded = onConcluded;
|
_onConcluded = onConcluded;
|
||||||
_name = name;
|
_name = name;
|
||||||
_toImport = ArrayList(toReconstruct);
|
_toImport = ArrayList(toReconstruct);
|
||||||
|
_cache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -146,7 +150,7 @@ class ImportDialog : AlertDialog {
|
|||||||
val scope = StateApp.instance.scopeOrNull;
|
val scope = StateApp.instance.scopeOrNull;
|
||||||
scope?.launch(Dispatchers.IO) {
|
scope?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val migrationResult = _store.importReconstructions(_toImport) { finished, total ->
|
val migrationResult = _store.importReconstructions(_toImport, _cache) { finished, total ->
|
||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
_textProgress.text = "${finished}/${total}";
|
_textProgress.text = "${finished}/${total}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
package com.futo.platformplayer.dialogs
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.media.MediaCas.PluginDescriptor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.method.ScrollingMovementMethod
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.activities.AddSourceActivity
|
||||||
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor
|
||||||
|
import com.futo.platformplayer.assume
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImportCache
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateBackup
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class PluginUpdateDialog : AlertDialog {
|
||||||
|
companion object {
|
||||||
|
private val TAG = "PluginUpdateDialog";
|
||||||
|
}
|
||||||
|
private val _context: Context;
|
||||||
|
|
||||||
|
private lateinit var _buttonCancel1: Button;
|
||||||
|
private lateinit var _buttonCancel2: Button;
|
||||||
|
private lateinit var _buttonUpdate: LinearLayout;
|
||||||
|
|
||||||
|
private lateinit var _buttonOk: LinearLayout;
|
||||||
|
private lateinit var _buttonInstall: LinearLayout;
|
||||||
|
|
||||||
|
private lateinit var _textPlugin: TextView;
|
||||||
|
private lateinit var _textProgres: TextView;
|
||||||
|
private lateinit var _textError: TextView;
|
||||||
|
private lateinit var _textResult: TextView;
|
||||||
|
|
||||||
|
private lateinit var _uiChoiceTop: FrameLayout;
|
||||||
|
private lateinit var _uiProgressTop: FrameLayout;
|
||||||
|
private lateinit var _uiRiskTop: FrameLayout;
|
||||||
|
|
||||||
|
private lateinit var _uiChoiceBot: LinearLayout;
|
||||||
|
private lateinit var _uiResultBot: LinearLayout;
|
||||||
|
private lateinit var _uiRiskBot: LinearLayout;
|
||||||
|
private lateinit var _uiProgressBot: LinearLayout;
|
||||||
|
|
||||||
|
private lateinit var _iconPlugin: ImageView;
|
||||||
|
private lateinit var _updateSpinner: ImageView;
|
||||||
|
|
||||||
|
private var _isUpdating = false;
|
||||||
|
|
||||||
|
private val _oldConfig: SourcePluginConfig;
|
||||||
|
private val _newConfig: SourcePluginConfig;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(context: Context, oldConfig: SourcePluginConfig, newConfig: SourcePluginConfig): super(context) {
|
||||||
|
_context = context;
|
||||||
|
_oldConfig = oldConfig;
|
||||||
|
_newConfig = newConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(LayoutInflater.from(context).inflate(R.layout.dialog_plugin_update, null));
|
||||||
|
|
||||||
|
_buttonCancel1 = findViewById(R.id.button_cancel_1);
|
||||||
|
_buttonCancel2 = findViewById(R.id.button_cancel_2);
|
||||||
|
_buttonUpdate = findViewById(R.id.button_update);
|
||||||
|
|
||||||
|
_buttonOk = findViewById(R.id.button_ok);
|
||||||
|
_buttonInstall = findViewById(R.id.button_install);
|
||||||
|
|
||||||
|
_textPlugin = findViewById(R.id.text_plugin);
|
||||||
|
_textProgres = findViewById(R.id.text_progress);
|
||||||
|
_textError = findViewById(R.id.text_error);
|
||||||
|
_textResult = findViewById(R.id.text_result);
|
||||||
|
|
||||||
|
_uiChoiceTop = findViewById(R.id.dialog_ui_choice_top);
|
||||||
|
_uiProgressTop = findViewById(R.id.dialog_ui_progress_top);
|
||||||
|
_uiRiskTop = findViewById(R.id.dialog_ui_risk_top);
|
||||||
|
|
||||||
|
_uiChoiceBot = findViewById(R.id.dialog_ui_bottom_choice);
|
||||||
|
_uiResultBot = findViewById(R.id.dialog_ui_bottom_result);
|
||||||
|
_uiRiskBot = findViewById(R.id.dialog_ui_bottom_risk);
|
||||||
|
_uiProgressBot = findViewById(R.id.dialog_ui_bottom_progress);
|
||||||
|
|
||||||
|
_updateSpinner = findViewById(R.id.update_spinner);
|
||||||
|
_iconPlugin = findViewById(R.id.icon_plugin);
|
||||||
|
|
||||||
|
_buttonCancel1.setOnClickListener {
|
||||||
|
dismiss();
|
||||||
|
};
|
||||||
|
_buttonCancel2.setOnClickListener {
|
||||||
|
dismiss();
|
||||||
|
};
|
||||||
|
_buttonUpdate.setOnClickListener {
|
||||||
|
if (_isUpdating)
|
||||||
|
return@setOnClickListener;
|
||||||
|
_isUpdating = true;
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
|
||||||
|
Glide.with(_iconPlugin)
|
||||||
|
.load(_oldConfig.absoluteIconUrl)
|
||||||
|
.fallback(R.drawable.ic_sources)
|
||||||
|
.into(_iconPlugin);
|
||||||
|
_textPlugin.text = _oldConfig.name;
|
||||||
|
|
||||||
|
val descriptor = StatePlugins.instance.getPlugin(_oldConfig.id);
|
||||||
|
if(descriptor != null) {
|
||||||
|
if(descriptor.appSettings.automaticUpdate) {
|
||||||
|
if (_isUpdating)
|
||||||
|
return;
|
||||||
|
_isUpdating = true;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dismiss() {
|
||||||
|
super.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun update() {
|
||||||
|
_uiChoiceTop.visibility = View.GONE;
|
||||||
|
_uiRiskTop.visibility = View.GONE;
|
||||||
|
_uiChoiceBot.visibility = View.GONE;
|
||||||
|
_uiResultBot.visibility = View.GONE;
|
||||||
|
_uiRiskBot.visibility = View.GONE;
|
||||||
|
_uiProgressTop.visibility = View.VISIBLE;
|
||||||
|
_uiProgressBot.visibility = View.VISIBLE;
|
||||||
|
|
||||||
|
setCancelable(false);
|
||||||
|
setCanceledOnTouchOutside(false);
|
||||||
|
|
||||||
|
Logger.i(TAG, "Keep screen on set import")
|
||||||
|
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
|
||||||
|
_updateSpinner.drawable?.assume<Animatable>()?.start();
|
||||||
|
|
||||||
|
val scope = StateApp.instance.scopeOrNull;
|
||||||
|
scope?.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val client = ManagedHttpClient();
|
||||||
|
val script = StatePlugins.instance.getScript(_oldConfig.id) ?: "";
|
||||||
|
|
||||||
|
val newScript = client.get(_newConfig.absoluteScriptUrl)?.body?.string();
|
||||||
|
if(newScript.isNullOrEmpty())
|
||||||
|
throw IllegalStateException("No script found");
|
||||||
|
|
||||||
|
if(_oldConfig.isLowRiskUpdate(script, _newConfig, newScript)){
|
||||||
|
|
||||||
|
StatePlugins.instance.installPluginBackground(context, StateApp.instance.scope, _newConfig, newScript,
|
||||||
|
{ text: String, progress: Double ->
|
||||||
|
_textProgres.setText(text);
|
||||||
|
},
|
||||||
|
{ ex ->
|
||||||
|
if(ex == null) {
|
||||||
|
StatePlugins.instance.clearUpdateAvailable(_newConfig);
|
||||||
|
_iconPlugin.setImageResource(R.drawable.ic_check);
|
||||||
|
_textError.visibility = View.GONE;
|
||||||
|
_textResult.visibility = View.VISIBLE;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_iconPlugin.setImageResource(R.drawable.ic_error_pred);
|
||||||
|
_textError.text = ex.message + "\n\nYou can retry inside the sources tab";
|
||||||
|
_textError.visibility = View.VISIBLE;
|
||||||
|
_textResult.visibility = View.GONE;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
_buttonOk.setOnClickListener {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
_uiProgressTop.visibility = View.GONE;
|
||||||
|
_uiProgressBot.visibility = View.GONE;
|
||||||
|
_uiChoiceTop.visibility = View.VISIBLE;
|
||||||
|
_uiResultBot.visibility = View.VISIBLE;
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to update UI.", e)
|
||||||
|
} finally {
|
||||||
|
Logger.i(TAG, "Keep screen on unset update")
|
||||||
|
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
_buttonInstall.setOnClickListener {
|
||||||
|
dismiss();
|
||||||
|
|
||||||
|
val intent = Intent(_context, AddSourceActivity::class.java).apply {
|
||||||
|
data = Uri.parse(_newConfig.sourceUrl)
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiProgressTop.visibility = View.GONE;
|
||||||
|
_uiProgressBot.visibility = View.GONE;
|
||||||
|
_uiRiskTop.visibility = View.VISIBLE;
|
||||||
|
_uiRiskBot.visibility = View.VISIBLE;
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to update UI.", e)
|
||||||
|
} finally {
|
||||||
|
Logger.i(TAG, "Keep screen on unset update")
|
||||||
|
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to update.", e);
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_buttonOk.setOnClickListener {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
_iconPlugin.setImageResource(R.drawable.ic_error_pred);
|
||||||
|
_textResult.visibility = View.GONE;
|
||||||
|
_uiProgressTop.visibility = View.GONE;
|
||||||
|
_uiProgressBot.visibility = View.GONE;
|
||||||
|
_uiChoiceTop.visibility = View.VISIBLE;
|
||||||
|
_uiResultBot.visibility = View.VISIBLE;
|
||||||
|
_textError.visibility = View.VISIBLE;
|
||||||
|
_textError.text = e.message + "\n\nYou can retry inside the sources tab"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -755,6 +755,7 @@ class VideoDownload {
|
|||||||
companion object {
|
companion object {
|
||||||
const val TAG = "VideoDownload";
|
const val TAG = "VideoDownload";
|
||||||
const val GROUP_PLAYLIST = "Playlist";
|
const val GROUP_PLAYLIST = "Playlist";
|
||||||
|
const val GROUP_WATCHLATER= "WatchLater";
|
||||||
|
|
||||||
fun videoContainerToExtension(container: String): String? {
|
fun videoContainerToExtension(container: String): String? {
|
||||||
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.documentfile.provider.DocumentFile
|
|||||||
import com.arthenica.ffmpegkit.*
|
import com.arthenica.ffmpegkit.*
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.*
|
import com.futo.platformplayer.api.media.models.streams.sources.*
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.toHumanBitrate
|
import com.futo.platformplayer.toHumanBitrate
|
||||||
@@ -63,7 +64,7 @@ class VideoExport {
|
|||||||
val outputFile: DocumentFile?;
|
val outputFile: DocumentFile?;
|
||||||
val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
||||||
if (sourceCount > 1) {
|
if (sourceCount > 1) {
|
||||||
val outputFileName = toSafeFileName(videoLocal.name) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
||||||
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
||||||
?: throw Exception("Failed to create file in external directory.");
|
?: throw Exception("Failed to create file in external directory.");
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ class VideoExport {
|
|||||||
}
|
}
|
||||||
outputFile = f;
|
outputFile = f;
|
||||||
} else if (v != null) {
|
} else if (v != null) {
|
||||||
val outputFileName = toSafeFileName(videoLocal.name) + "." + VideoDownload.videoContainerToExtension(v.container);
|
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.videoContainerToExtension(v.container);
|
||||||
val f = downloadRoot.createFile(v.container, outputFileName)
|
val f = downloadRoot.createFile(v.container, outputFileName)
|
||||||
?: throw Exception("Failed to create file in external directory.");
|
?: throw Exception("Failed to create file in external directory.");
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ class VideoExport {
|
|||||||
|
|
||||||
outputFile = f;
|
outputFile = f;
|
||||||
} else if (a != null) {
|
} else if (a != null) {
|
||||||
val outputFileName = toSafeFileName(videoLocal.name) + "." + VideoDownload.audioContainerToExtension(a.container);
|
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container);
|
||||||
val f = downloadRoot.createFile(a.container, outputFileName)
|
val f = downloadRoot.createFile(a.container, outputFileName)
|
||||||
?: throw Exception("Failed to create file in external directory.");
|
?: throw Exception("Failed to create file in external directory.");
|
||||||
|
|
||||||
@@ -110,11 +111,6 @@ class VideoExport {
|
|||||||
return@coroutineScope outputFile;
|
return@coroutineScope outputFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toSafeFileName(input: String): String {
|
|
||||||
val safeCharacters = ('a'..'z') + ('A'..'Z') + ('0'..'9') + listOf('-', '_')
|
|
||||||
return input.map { if (it in safeCharacters) it else '_' }.joinToString(separator = "")
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun combine(inputPathAudio: String?, inputPathVideo: String?, inputPathSubtitles: String?, outputPath: String, duration: Double, onProgress: ((Double) -> Unit)? = null) = withContext(Dispatchers.IO) {
|
private suspend fun combine(inputPathAudio: String?, inputPathVideo: String?, inputPathSubtitles: String?, outputPath: String, duration: Double, onProgress: ((Double) -> Unit)? = null) = withContext(Dispatchers.IO) {
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
//ffmpeg -i a.mp4 -i b.m4a -scodec mov_text -i c.vtt -map 0:v -map 1:a -map 2 -c:v copy -c:a copy -c:s mov_text output.mp4
|
//ffmpeg -i a.mp4 -i b.m4a -scodec mov_text -i c.vtt -map 0:v -map 1:a -map 2 -c:v copy -c:a copy -c:s mov_text output.mp4
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.caoccao.javet.exceptions.JavetCompilationException
|
import com.caoccao.javet.exceptions.JavetCompilationException
|
||||||
|
import com.caoccao.javet.exceptions.JavetException
|
||||||
import com.caoccao.javet.exceptions.JavetExecutionException
|
import com.caoccao.javet.exceptions.JavetExecutionException
|
||||||
import com.caoccao.javet.interop.V8Host
|
import com.caoccao.javet.interop.V8Host
|
||||||
import com.caoccao.javet.interop.V8Runtime
|
import com.caoccao.javet.interop.V8Runtime
|
||||||
@@ -43,7 +44,6 @@ class V8Plugin {
|
|||||||
private val _clientAuth: ManagedHttpClient;
|
private val _clientAuth: ManagedHttpClient;
|
||||||
private val _clientOthers: ConcurrentHashMap<String, JSHttpClient> = ConcurrentHashMap();
|
private val _clientOthers: ConcurrentHashMap<String, JSHttpClient> = ConcurrentHashMap();
|
||||||
|
|
||||||
|
|
||||||
val httpClient: ManagedHttpClient get() = _client;
|
val httpClient: ManagedHttpClient get() = _client;
|
||||||
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
|
val httpClientAuth: ManagedHttpClient get() = _clientAuth;
|
||||||
val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers;
|
val httpClientOthers: Map<String, JSHttpClient> get() = _clientOthers;
|
||||||
@@ -69,6 +69,11 @@ class V8Plugin {
|
|||||||
private var _busyCounter = 0;
|
private var _busyCounter = 0;
|
||||||
val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 };
|
val isBusy get() = synchronized(_busyCounterLock) { _busyCounter > 0 };
|
||||||
|
|
||||||
|
var allowDevSubmit: Boolean = false
|
||||||
|
private set(value) {
|
||||||
|
field = value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called before a busy counter is about to be removed.
|
* Called before a busy counter is about to be removed.
|
||||||
* Is primarily used to prevent additional calls to dead runtimes.
|
* Is primarily used to prevent additional calls to dead runtimes.
|
||||||
@@ -90,6 +95,10 @@ class V8Plugin {
|
|||||||
withDependency(getPackage(pack));
|
withDependency(getPackage(pack));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun changeAllowDevSubmit(isAllowed: Boolean) {
|
||||||
|
allowDevSubmit = isAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
fun withDependency(context: Context, assetPath: String) : V8Plugin {
|
fun withDependency(context: Context, assetPath: String) : V8Plugin {
|
||||||
if(!_deps.containsKey(assetPath))
|
if(!_deps.containsKey(assetPath))
|
||||||
_deps.put(assetPath, getAssetFile(context, assetPath));
|
_deps.put(assetPath, getAssetFile(context, assetPath));
|
||||||
@@ -173,8 +182,16 @@ class V8Plugin {
|
|||||||
isStopped = true;
|
isStopped = true;
|
||||||
_runtime?.let {
|
_runtime?.let {
|
||||||
_runtime = null;
|
_runtime = null;
|
||||||
if(!it.isClosed && !it.isDead)
|
if(!it.isClosed && !it.isDead) {
|
||||||
it.close();
|
try {
|
||||||
|
it.close();
|
||||||
|
}
|
||||||
|
catch(ex: JavetException) {
|
||||||
|
//In case race conditions are going on, already closed runtimes are fine.
|
||||||
|
if(ex.message?.contains("Runtime is already closed") != true)
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
Logger.i(TAG, "Stopped plugin [${config.name}]");
|
Logger.i(TAG, "Stopped plugin [${config.name}]");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
@@ -12,6 +13,9 @@ import com.futo.platformplayer.engine.V8Plugin
|
|||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class PackageBridge : V8Package {
|
class PackageBridge : V8Package {
|
||||||
@Transient
|
@Transient
|
||||||
@@ -21,6 +25,7 @@ class PackageBridge : V8Package {
|
|||||||
@Transient
|
@Transient
|
||||||
private val _clientAuth: ManagedHttpClient
|
private val _clientAuth: ManagedHttpClient
|
||||||
|
|
||||||
|
|
||||||
override val name: String get() = "Bridge";
|
override val name: String get() = "Bridge";
|
||||||
override val variableName: String get() = "bridge";
|
override val variableName: String get() = "bridge";
|
||||||
|
|
||||||
@@ -47,6 +52,44 @@ class PackageBridge : V8Package {
|
|||||||
StateDeveloper.instance.logDevInfo(StateDeveloper.instance.currentDevID ?: "", str ?: "null");
|
StateDeveloper.instance.logDevInfo(StateDeveloper.instance.currentDevID ?: "", str ?: "null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _jsonSerializer = Json { this.prettyPrintIndent = " "; this.prettyPrint = true; };
|
||||||
|
private var _devSubmitClient: ManagedHttpClient? = null;
|
||||||
|
@V8Function
|
||||||
|
fun devSubmit(label: String, data: String) {
|
||||||
|
if(_plugin.config !is SourcePluginConfig)
|
||||||
|
return;
|
||||||
|
if(!_plugin.allowDevSubmit)
|
||||||
|
return;
|
||||||
|
val devUrl = _plugin.config.developerSubmitUrl ?: return;
|
||||||
|
if(_devSubmitClient == null)
|
||||||
|
_devSubmitClient = ManagedHttpClient();
|
||||||
|
|
||||||
|
val stackTrace = Thread.currentThread().stackTrace;
|
||||||
|
val callerMethod = stackTrace.findLast {
|
||||||
|
it.className == JSClient::class.java.name
|
||||||
|
}?.methodName ?: "";
|
||||||
|
val session = StateApp.instance.sessionId;
|
||||||
|
val pluginId = _plugin.config.id;
|
||||||
|
val pluginVersion = _plugin.config.version;
|
||||||
|
|
||||||
|
val obj = DevSubmitData(pluginId, pluginVersion, callerMethod, session, label, data);
|
||||||
|
|
||||||
|
UIDialogs.toast("DevSubmit [${callerMethod}] (${_plugin.config.name})", false);
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val json = _jsonSerializer.encodeToString(obj);
|
||||||
|
Logger.i(TAG, "DevSubmit [${callerMethod}] - ${devUrl}\n" + json);
|
||||||
|
val resp = _devSubmitClient?.post(devUrl, json, mutableMapOf(Pair("Content-Type", "application/json")));
|
||||||
|
Logger.i(TAG, "DevSubmit [${callerMethod}] - ${devUrl} Status: " + (resp?.code?.toString() ?: "-1"))
|
||||||
|
}
|
||||||
|
catch(ex: Exception) {
|
||||||
|
Logger.e(TAG, "DevSubmission to [${devUrl}] failed due to:\n" + ex.message, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Serializable
|
||||||
|
class DevSubmitData(val pluginId: String, val pluginVersion: Int, val caller: String, val session: String, val label: String, val data: String)
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun throwTest(str: String) {
|
fun throwTest(str: String) {
|
||||||
throw IllegalStateException(str);
|
throw IllegalStateException(str);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.caoccao.javet.interop.V8Runtime
|
|||||||
import com.caoccao.javet.values.V8Value
|
import com.caoccao.javet.values.V8Value
|
||||||
import com.caoccao.javet.values.reference.V8ValueObject
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
@@ -242,7 +243,8 @@ class PackageHttp: V8Package {
|
|||||||
val resp = client.requestMethod(method, url, headers);
|
val resp = client.requestMethod(method, url, headers);
|
||||||
val responseBody = resp.body?.string();
|
val responseBody = resp.body?.string();
|
||||||
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -256,7 +258,8 @@ class PackageHttp: V8Package {
|
|||||||
val resp = client.requestMethod(method, url, body, headers);
|
val resp = client.requestMethod(method, url, body, headers);
|
||||||
val responseBody = resp.body?.string();
|
val responseBody = resp.body?.string();
|
||||||
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
//logResponse(method, url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -271,7 +274,8 @@ class PackageHttp: V8Package {
|
|||||||
val resp = client.get(url, headers);
|
val resp = client.get(url, headers);
|
||||||
val responseBody = resp.body?.string();
|
val responseBody = resp.body?.string();
|
||||||
//logResponse("GET", url, resp.code, resp.headers, responseBody);
|
//logResponse("GET", url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -285,7 +289,8 @@ class PackageHttp: V8Package {
|
|||||||
val resp = client.post(url, body, headers);
|
val resp = client.post(url, body, headers);
|
||||||
val responseBody = resp.body?.string();
|
val responseBody = resp.body?.string();
|
||||||
//logResponse("POST", url, resp.code, resp.headers, responseBody);
|
//logResponse("POST", url, resp.code, resp.headers, responseBody);
|
||||||
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers));
|
return@catchHttp BridgeHttpResponse(resp.url, resp.code, responseBody, sanitizeResponseHeaders(resp.headers,
|
||||||
|
_client !is JSHttpClient || _client.isLoggedIn || _package._config !is SourcePluginConfig || !_package._config.allowAllHttpHeaderAccess));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -305,18 +310,31 @@ class PackageHttp: V8Package {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sanitizeResponseHeaders(headers: Map<String, List<String>>?): Map<String, List<String>> {
|
private fun sanitizeResponseHeaders(headers: Map<String, List<String>>?, onlyWhitelisted: Boolean = false): Map<String, List<String>> {
|
||||||
val result = mutableMapOf<String, List<String>>()
|
val result = mutableMapOf<String, List<String>>()
|
||||||
headers?.forEach { (header, values) ->
|
if(onlyWhitelisted)
|
||||||
val lowerCaseHeader = header.lowercase()
|
headers?.forEach { (header, values) ->
|
||||||
if (WHITELISTED_RESPONSE_HEADERS.contains(lowerCaseHeader)) {
|
val lowerCaseHeader = header.lowercase()
|
||||||
result[lowerCaseHeader] = values
|
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
|
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) {
|
Logger.v(TAG) {
|
||||||
val stringBuilder = StringBuilder();
|
val stringBuilder = StringBuilder();
|
||||||
stringBuilder.appendLine("HTTP request (useAuth = )");
|
stringBuilder.appendLine("HTTP request (useAuth = )");
|
||||||
@@ -333,7 +351,7 @@ class PackageHttp: V8Package {
|
|||||||
|
|
||||||
return@v stringBuilder.toString();
|
return@v stringBuilder.toString();
|
||||||
};
|
};
|
||||||
}*/
|
}
|
||||||
|
|
||||||
/*private fun logResponse(method: String, url: String, responseCode: Int? = null, responseHeaders: Map<String, List<String>> = HashMap(), responseBody: String? = null) {
|
/*private fun logResponse(method: String, url: String, responseCode: Int? = null, responseHeaders: Map<String, List<String>> = HashMap(), responseBody: String? = null) {
|
||||||
Logger.v(TAG) {
|
Logger.v(TAG) {
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import android.util.Base64
|
|||||||
import com.caoccao.javet.annotations.V8Function
|
import com.caoccao.javet.annotations.V8Function
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.google.common.hash.Hashing.md5
|
||||||
|
import java.security.MessageDigest
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
||||||
class PackageUtilities : V8Package {
|
class PackageUtilities : V8Package {
|
||||||
@Transient
|
@Transient
|
||||||
private val _config: IV8PluginConfig;
|
private val _config: IV8PluginConfig;
|
||||||
@@ -19,7 +22,31 @@ class PackageUtilities : V8Package {
|
|||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun toBase64(arr: ByteArray): String {
|
fun toBase64(arr: ByteArray): String {
|
||||||
return Base64.encodeToString(arr, Base64.NO_WRAP);
|
return Base64.encodeToString(arr, Base64.NO_PADDING or Base64.NO_WRAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun fromBase64(str: String): ByteArray {
|
||||||
|
return Base64.decode(str, Base64.NO_PADDING or Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun md5(arr: ByteArray): ByteArray {
|
||||||
|
return MessageDigest.getInstance("MD5").digest(arr);
|
||||||
|
}
|
||||||
|
@V8Function
|
||||||
|
fun md5String(str: String): String {
|
||||||
|
return md5(str.toByteArray(Charsets.UTF_8)).fold("") { str, it -> str + "%02x".format(it) };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun sha256(arr: ByteArray): ByteArray {
|
||||||
|
return MessageDigest.getInstance("SHA-256").digest(arr);
|
||||||
|
}
|
||||||
|
@V8Function
|
||||||
|
fun sha256String(str: String): String {
|
||||||
|
return sha256(str.toByteArray(Charsets.UTF_8)).fold("") { str, it -> str + "%02x".format(it) };
|
||||||
}
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
|
|||||||
+6
-3
@@ -60,8 +60,10 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||||
val onAddToClicked = Event1<IPlatformContent>();
|
val onAddToClicked = Event1<IPlatformContent>();
|
||||||
val onAddToQueueClicked = Event1<IPlatformContent>();
|
val onAddToQueueClicked = Event1<IPlatformContent>();
|
||||||
|
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
|
||||||
val onLongPress = Event1<IPlatformContent>();
|
val onLongPress = Event1<IPlatformContent>();
|
||||||
|
|
||||||
|
|
||||||
private fun getContentPager(channel: IPlatformChannel): IPager<IPlatformContent> {
|
private fun getContentPager(channel: IPlatformChannel): IPager<IPlatformContent> {
|
||||||
Logger.i(TAG, "getContentPager");
|
Logger.i(TAG, "getContentPager");
|
||||||
|
|
||||||
@@ -103,9 +105,9 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
}).success {
|
}).success {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
val posBefore = _results.size;
|
val posBefore = _results.size;
|
||||||
val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }
|
//val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo }
|
||||||
_results.addAll(toAdd);
|
_results.addAll(it);
|
||||||
_adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), toAdd.size); };
|
_adapterResults?.let { adapterVideo -> adapterVideo.notifyItemRangeInserted(adapterVideo.childToParentPosition(posBefore), it.size); };
|
||||||
}.exception<Throwable> {
|
}.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load next page.", it);
|
Logger.w(TAG, "Failed to load next page.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadNextPage() });
|
UIDialogs.showGeneralRetryErrorDialog(requireContext(), it.message ?: "", it, { loadNextPage() });
|
||||||
@@ -157,6 +159,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
this.onChannelClicked.subscribe(this@ChannelContentsFragment.onChannelClicked::emit);
|
this.onChannelClicked.subscribe(this@ChannelContentsFragment.onChannelClicked::emit);
|
||||||
this.onAddToClicked.subscribe(this@ChannelContentsFragment.onAddToClicked::emit);
|
this.onAddToClicked.subscribe(this@ChannelContentsFragment.onAddToClicked::emit);
|
||||||
this.onAddToQueueClicked.subscribe(this@ChannelContentsFragment.onAddToQueueClicked::emit);
|
this.onAddToQueueClicked.subscribe(this@ChannelContentsFragment.onAddToQueueClicked::emit);
|
||||||
|
this.onAddToWatchLaterClicked.subscribe(this@ChannelContentsFragment.onAddToWatchLaterClicked::emit);
|
||||||
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
this.onLongPress.subscribe(this@ChannelContentsFragment.onLongPress::emit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-3
@@ -247,11 +247,14 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
||||||
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
|
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
|
||||||
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
||||||
if (_buttonsVisible - 1 >= defs.size) {
|
if (_buttonsVisible >= defs.size) {
|
||||||
updateBottomMenuButtons(defs.toMutableList(), false);
|
updateBottomMenuButtons(defs.toMutableList(), false);
|
||||||
|
} else if (_buttonsVisible > 0) {
|
||||||
|
updateBottomMenuButtons(defs.take(_buttonsVisible - 1).toMutableList(), true);
|
||||||
|
updateMoreButtons(defs.drop(_buttonsVisible - 1).toMutableList());
|
||||||
} else {
|
} else {
|
||||||
updateBottomMenuButtons(defs.slice(IntRange(0, _buttonsVisible - 2)).toMutableList(), true);
|
updateBottomMenuButtons(mutableListOf(), false)
|
||||||
updateMoreButtons(defs.slice(IntRange(_buttonsVisible - 1, defs.size - 1)).toMutableList());
|
updateMoreButtons(defs.toMutableList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-2
@@ -26,6 +26,7 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
|
|||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
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.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment
|
import com.futo.platformplayer.fragment.channel.tab.ChannelAboutFragment
|
||||||
import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment
|
import com.futo.platformplayer.fragment.channel.tab.ChannelContentsFragment
|
||||||
@@ -151,7 +152,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.e(TAG, "Failed to load channel.", it);
|
Logger.e(TAG, "Failed to load channel.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadChannel() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadChannel() }, null, fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
val tabs: TabLayout = findViewById(R.id.tabs);
|
val tabs: TabLayout = findViewById(R.id.tabs);
|
||||||
@@ -206,6 +207,12 @@ class ChannelFragment : MainFragment() {
|
|||||||
StatePlayer.instance.addToQueue(content);
|
StatePlayer.instance.addToQueue(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
||||||
|
if(content is IPlatformVideo) {
|
||||||
|
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content));
|
||||||
|
UIDialogs.toast("Added to watch later\n[${content.name}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
adapter.onUrlClicked.subscribe { url ->
|
adapter.onUrlClicked.subscribe { url ->
|
||||||
fragment.navigate<BrowserFragment>(url);
|
fragment.navigate<BrowserFragment>(url);
|
||||||
}
|
}
|
||||||
@@ -264,7 +271,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
_taskLoadPolycentricProfile.cancel();
|
_taskLoadPolycentricProfile.cancel();
|
||||||
_selectedTabIndex = -1;
|
_selectedTabIndex = -1;
|
||||||
|
|
||||||
if (!isBack) {
|
if (!isBack || _url == null) {
|
||||||
_imageBanner.setImageDrawable(null);
|
_imageBanner.setImageDrawable(null);
|
||||||
|
|
||||||
if (parameter is String) {
|
if (parameter is String) {
|
||||||
|
|||||||
+16
@@ -1,7 +1,9 @@
|
|||||||
package com.futo.platformplayer.fragment.mainactivity.main
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.Browser
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -118,6 +120,7 @@ class CommentsFragment : MainFragment() {
|
|||||||
holder.onDelete.subscribe(::onDelete);
|
holder.onDelete.subscribe(::onDelete);
|
||||||
holder.onRepliesClick.subscribe(::onRepliesClick);
|
holder.onRepliesClick.subscribe(::onRepliesClick);
|
||||||
holder.onClick.subscribe(::onClick);
|
holder.onClick.subscribe(::onClick);
|
||||||
|
holder.onAuthorClick.subscribe(::onAuthorClick);
|
||||||
return@InsertedViewAdapterWithLoader holder;
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -211,6 +214,19 @@ class CommentsFragment : MainFragment() {
|
|||||||
setRepliesOverlayVisible(true, true)
|
setRepliesOverlayVisible(true, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private fun onAuthorClick(c: IPlatformComment) {
|
||||||
|
if (c !is PolycentricPlatformComment) {
|
||||||
|
return@onAuthorClick;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
||||||
|
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
||||||
|
//val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
|
||||||
|
val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
||||||
|
_fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
||||||
|
//_fragment.navigate<BrowserFragment>(navUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun onRepliesClick(c: IPlatformComment) {
|
private fun onRepliesClick(c: IPlatformComment) {
|
||||||
val replyCount = c.replyCount ?: 0;
|
val replyCount = c.replyCount ?: 0;
|
||||||
|
|||||||
+9
@@ -12,10 +12,12 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
import com.futo.platformplayer.api.media.models.post.IPlatformPost
|
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.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.structures.*
|
import com.futo.platformplayer.api.media.structures.*
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.video.PlayerManager
|
import com.futo.platformplayer.video.PlayerManager
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||||
@@ -81,6 +83,12 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
StatePlayer.instance.addToQueue(it);
|
StatePlayer.instance.addToQueue(it);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
||||||
|
if(it is IPlatformVideo) {
|
||||||
|
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it));
|
||||||
|
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||||
|
}
|
||||||
|
};
|
||||||
adapter.onLongPress.subscribe(this) {
|
adapter.onLongPress.subscribe(this) {
|
||||||
if (it is IPlatformVideo) {
|
if (it is IPlatformVideo) {
|
||||||
showVideoOptionsOverlay(it)
|
showVideoOptionsOverlay(it)
|
||||||
@@ -135,6 +143,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
adapter.onChannelClicked.remove(this);
|
adapter.onChannelClicked.remove(this);
|
||||||
adapter.onAddToClicked.remove(this);
|
adapter.onAddToClicked.remove(this);
|
||||||
adapter.onAddToQueueClicked.remove(this);
|
adapter.onAddToQueueClicked.remove(this);
|
||||||
|
adapter.onAddToWatchLaterClicked.remove(this);
|
||||||
adapter.onLongPress.remove(this);
|
adapter.onLongPress.remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-3
@@ -99,7 +99,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
.success { loadedResult(it); }.exception<ScriptCaptchaRequiredException> { }
|
.success { loadedResult(it); }.exception<ScriptCaptchaRequiredException> { }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load results.", it);
|
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);
|
setPreviewsEnabled(Settings.instance.search.previewFeedItems);
|
||||||
@@ -129,7 +129,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
onFilterClick.subscribe(this) {
|
onFilterClick.subscribe(this) {
|
||||||
_overlayContainer.let {
|
_overlayContainer.let {
|
||||||
val filterValuesCopy = HashMap(_filterValues);
|
val filterValuesCopy = HashMap(_filterValues);
|
||||||
val filtersOverlay = UISlideOverlays.showFiltersOverlay(lifecycleScope, it, _enabledClientIds!!, filterValuesCopy);
|
val filtersOverlay = UISlideOverlays.showFiltersOverlay(lifecycleScope, it, _enabledClientIds!!, filterValuesCopy, _channelUrl != null);
|
||||||
filtersOverlay.onOK.subscribe { enabledClientIds, changed ->
|
filtersOverlay.onOK.subscribe { enabledClientIds, changed ->
|
||||||
if (changed) {
|
if (changed) {
|
||||||
setFilterValues(filtersOverlay.commonCapabilities, filterValuesCopy);
|
setFilterValues(filtersOverlay.commonCapabilities, filterValuesCopy);
|
||||||
@@ -170,7 +170,11 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val commonCapabilities = StatePlatform.instance.getCommonSearchCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
|
val commonCapabilities =
|
||||||
|
if(_channelUrl == null)
|
||||||
|
StatePlatform.instance.getCommonSearchCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
|
||||||
|
else
|
||||||
|
StatePlatform.instance.getCommonSearchChannelContentsCapabilities(StatePlatform.instance.getEnabledClients().map { it.id });
|
||||||
val sorts = commonCapabilities?.sorts ?: listOf();
|
val sorts = commonCapabilities?.sorts ?: listOf();
|
||||||
if (sorts.size > 1) {
|
if (sorts.size > 1) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|||||||
+1
-1
@@ -60,7 +60,7 @@ class CreatorSearchResultsFragment : MainFragment() {
|
|||||||
.exception<ScriptCaptchaRequiredException> { }
|
.exception<ScriptCaptchaRequiredException> { }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }, null, fragment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-6
@@ -12,8 +12,10 @@ import com.futo.platformplayer.*
|
|||||||
import com.futo.platformplayer.downloads.VideoDownload
|
import com.futo.platformplayer.downloads.VideoDownload
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
||||||
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
||||||
import com.futo.platformplayer.views.others.ProgressBar
|
import com.futo.platformplayer.views.others.ProgressBar
|
||||||
@@ -143,6 +145,7 @@ class DownloadsFragment : MainFragment() {
|
|||||||
|
|
||||||
val activeDownloads = StateDownloads.instance.getDownloading();
|
val activeDownloads = StateDownloads.instance.getDownloading();
|
||||||
val playlists = StateDownloads.instance.getCachedPlaylists();
|
val playlists = StateDownloads.instance.getCachedPlaylists();
|
||||||
|
val watchLaterDownload = StateDownloads.instance.getWatchLaterDescriptor();
|
||||||
val downloaded = StateDownloads.instance.getDownloadedVideos()
|
val downloaded = StateDownloads.instance.getDownloadedVideos()
|
||||||
.filter { it.groupType != VideoDownload.GROUP_PLAYLIST || it.groupID == null || !StateDownloads.instance.hasCachedPlaylist(it.groupID!!) };
|
.filter { it.groupType != VideoDownload.GROUP_PLAYLIST || it.groupID == null || !StateDownloads.instance.hasCachedPlaylist(it.groupID!!) };
|
||||||
|
|
||||||
@@ -150,23 +153,35 @@ class DownloadsFragment : MainFragment() {
|
|||||||
_listActiveDownloadsContainer.visibility = GONE;
|
_listActiveDownloadsContainer.visibility = GONE;
|
||||||
else {
|
else {
|
||||||
_listActiveDownloadsContainer.visibility = VISIBLE;
|
_listActiveDownloadsContainer.visibility = VISIBLE;
|
||||||
_listActiveDownloadsMeta.text = "(${activeDownloads.size})";
|
_listActiveDownloadsMeta.text = "(${activeDownloads.size} videos)";
|
||||||
|
|
||||||
_listActiveDownloads.removeAllViews();
|
_listActiveDownloads.removeAllViews();
|
||||||
for(view in activeDownloads.map { ActiveDownloadItem(context, it, _frag.lifecycleScope) })
|
for(view in activeDownloads.take(4).map { ActiveDownloadItem(context, it, _frag.lifecycleScope) })
|
||||||
_listActiveDownloads.addView(view);
|
_listActiveDownloads.addView(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(playlists.isEmpty())
|
if(playlists.isEmpty() && watchLaterDownload == null)
|
||||||
_listPlaylistsContainer.visibility = GONE;
|
_listPlaylistsContainer.visibility = GONE;
|
||||||
else {
|
else {
|
||||||
_listPlaylistsContainer.visibility = VISIBLE;
|
_listPlaylistsContainer.visibility = VISIBLE;
|
||||||
_listPlaylistsMeta.text = "(${playlists.size} ${context.getString(R.string.playlists).lowercase()}, ${playlists.sumOf { it.playlist.videos.size }} ${context.getString(R.string.videos).lowercase()})";
|
|
||||||
|
val watchLater = if(watchLaterDownload != null) StatePlaylists.instance.getWatchLater() else listOf();
|
||||||
|
|
||||||
|
_listPlaylistsMeta.text = "(${playlists.size + (if(watchLaterDownload != null) 1 else 0)} ${context.getString(R.string.playlists).lowercase()}, ${playlists.sumOf { it.playlist.videos.size } + watchLater.size} ${context.getString(R.string.videos).lowercase()})";
|
||||||
|
|
||||||
_listPlaylists.removeAllViews();
|
_listPlaylists.removeAllViews();
|
||||||
for(view in playlists.map { PlaylistDownloadItem(context, it) }) {
|
if(watchLaterDownload != null) {
|
||||||
|
val pdView = PlaylistDownloadItem(context, "Watch Later", watchLater.firstOrNull()?.thumbnails?.getHQThumbnail(), "WATCHLATER");
|
||||||
|
pdView.setOnClickListener {
|
||||||
|
_frag.navigate<WatchLaterFragment>();
|
||||||
|
}
|
||||||
|
_listPlaylists.addView(pdView);
|
||||||
|
}
|
||||||
|
for(view in playlists.map { PlaylistDownloadItem(context, it.playlist.name, it.playlist.videos.firstOrNull()?.thumbnails?.getHQThumbnail(), it.playlist) }) {
|
||||||
view.setOnClickListener {
|
view.setOnClickListener {
|
||||||
_frag.navigate<PlaylistFragment>(view.playlist.playlist);
|
if(view.obj is Playlist) {
|
||||||
|
_frag.navigate<PlaylistFragment>(view.obj);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
_listPlaylists.addView(view);
|
_listPlaylists.addView(view);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
Logger.w(TAG, "Failed to load next page.", it);
|
Logger.w(TAG, "Failed to load next page.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
||||||
loadNextPage();
|
loadNextPage();
|
||||||
});
|
}, null, fragment);
|
||||||
//UIDialogs.showDataRetryDialog(layoutInflater, it.message, { loadNextPage() });
|
//UIDialogs.showDataRetryDialog(layoutInflater, it.message, { loadNextPage() });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -174,7 +174,7 @@ class HistoryFragment : MainFragment() {
|
|||||||
Logger.w(TAG, "Failed to load next page.", it);
|
Logger.w(TAG, "Failed to load next page.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
|
||||||
loadNextPage();
|
loadNextPage();
|
||||||
});
|
}, null, fragment);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-5
@@ -32,6 +32,9 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
|||||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||||
import com.futo.platformplayer.views.announcements.AnnouncementView
|
import com.futo.platformplayer.views.announcements.AnnouncementView
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -123,10 +126,10 @@ class HomeFragment : MainFragment() {
|
|||||||
Logger.w(TAG, "Failed to load channel.", it);
|
Logger.w(TAG, "Failed to load channel.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_get_home), it, {
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_get_home), it, {
|
||||||
loadResults()
|
loadResults()
|
||||||
}) {
|
}, {
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
}, fragment);
|
||||||
};
|
};
|
||||||
|
|
||||||
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||||
@@ -160,15 +163,17 @@ class HomeFragment : MainFragment() {
|
|||||||
val pluginsExist = StatePlatform.instance.getAvailableClients().isNotEmpty();
|
val pluginsExist = StatePlatform.instance.getAvailableClients().isNotEmpty();
|
||||||
if(StatePlatform.instance.getEnabledClients().isEmpty())
|
if(StatePlatform.instance.getEnabledClients().isEmpty())
|
||||||
//Initial setup
|
//Initial setup
|
||||||
return NoResultsView(context, "No enabled Sources", if(pluginsExist)
|
return NoResultsView(context, "No enabled sources", if(pluginsExist)
|
||||||
"Enable or install some Sources"
|
"Enable or install some sources"
|
||||||
else "This Grayjay version comes without any sources, install sources externally or using the button below.", R.drawable.ic_sources,
|
else "This Grayjay version comes without any sources, install sources externally or using the button below.", R.drawable.ic_sources,
|
||||||
listOf(BigButton(context, "Browse Online Sources", "View official sources online", R.drawable.ic_explore) {
|
listOf(BigButton(context, "Browse Online Sources", "View official sources online", R.drawable.ic_explore) {
|
||||||
fragment.navigate<BrowserFragment>(BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
|
fragment.navigate<BrowserFragment>(BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
|
||||||
Pair("grayjay") { req ->
|
Pair("grayjay") { req ->
|
||||||
StateApp.instance.contextOrNull?.let {
|
StateApp.instance.contextOrNull?.let {
|
||||||
if(it is MainActivity) {
|
if(it is MainActivity) {
|
||||||
it.handleUrlAll(req.url.toString());
|
runBlocking {
|
||||||
|
it.handleUrlAll(req.url.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-41
@@ -146,7 +146,7 @@ class PlaylistFragment : MainFragment() {
|
|||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load playlist.", it);
|
Logger.w(TAG, "Failed to load playlist.", it);
|
||||||
val c = context ?: return@exception;
|
val c = context ?: return@exception;
|
||||||
UIDialogs.showGeneralRetryErrorDialog(c, context.getString(R.string.failed_to_load_playlist), it, ::fetchPlaylist);
|
UIDialogs.showGeneralRetryErrorDialog(c, context.getString(R.string.failed_to_load_playlist), it, ::fetchPlaylist, null, fragment);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,14 +201,18 @@ class PlaylistFragment : MainFragment() {
|
|||||||
showConvertPlaylistButton();
|
showConvertPlaylistButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDownloadState();
|
_playlist?.let {
|
||||||
|
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this::download);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onResume() {
|
fun onResume() {
|
||||||
StateDownloads.instance.onDownloadsChanged.subscribe(this) {
|
StateDownloads.instance.onDownloadsChanged.subscribe(this) {
|
||||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
updateDownloadState();
|
_playlist?.let {
|
||||||
|
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this@PlaylistView::download);
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
||||||
}
|
}
|
||||||
@@ -217,7 +221,9 @@ class PlaylistFragment : MainFragment() {
|
|||||||
StateDownloads.instance.onDownloadedChanged.subscribe(this) {
|
StateDownloads.instance.onDownloadedChanged.subscribe(this) {
|
||||||
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
updateDownloadState();
|
_playlist?.let {
|
||||||
|
updateDownloadState(VideoDownload.GROUP_PLAYLIST, it.id, this@PlaylistView::download);
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
||||||
}
|
}
|
||||||
@@ -225,6 +231,12 @@ class PlaylistFragment : MainFragment() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun download() {
|
||||||
|
_playlist?.let {
|
||||||
|
UISlideOverlays.showDownloadPlaylistOverlay(it, overlayContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onPause() {
|
fun onPause() {
|
||||||
StateDownloads.instance.onDownloadsChanged.remove(this);
|
StateDownloads.instance.onDownloadsChanged.remove(this);
|
||||||
StateDownloads.instance.onDownloadedChanged.remove(this);
|
StateDownloads.instance.onDownloadedChanged.remove(this);
|
||||||
@@ -268,43 +280,6 @@ class PlaylistFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateDownloadState() {
|
|
||||||
val playlist = _playlist ?: return;
|
|
||||||
val isDownloading = StateDownloads.instance.getDownloading().any { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == playlist.id };
|
|
||||||
val isDownloaded = StateDownloads.instance.isPlaylistCached(playlist.id);
|
|
||||||
|
|
||||||
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics);
|
|
||||||
|
|
||||||
if(isDownloaded && !isDownloading)
|
|
||||||
_buttonDownload.setBackgroundResource(R.drawable.background_button_round_green);
|
|
||||||
else
|
|
||||||
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
|
|
||||||
|
|
||||||
if(isDownloading) {
|
|
||||||
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
|
||||||
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
|
||||||
_buttonDownload.setOnClickListener {
|
|
||||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
|
||||||
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(isDownloaded) {
|
|
||||||
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
|
||||||
_buttonDownload.setOnClickListener {
|
|
||||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
|
||||||
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
_buttonDownload.setImageResource(R.drawable.ic_download);
|
|
||||||
_buttonDownload.setOnClickListener {
|
|
||||||
UISlideOverlays.showDownloadPlaylistOverlay(playlist, overlayContainer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_buttonDownload.setPadding(dp10.toInt());
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canEdit(): Boolean { return _playlist != null; }
|
override fun canEdit(): Boolean { return _playlist != null; }
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ class PlaylistSearchResultsFragment : MainFragment() {
|
|||||||
.success { loadedResult(it); }
|
.success { loadedResult(it); }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load results.", it);
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() }, null, fragment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -35,6 +35,7 @@ import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
|
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
@@ -161,7 +162,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
.success { setPostDetails(it) }
|
.success { setPostDetails(it) }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, context.getString(R.string.failed_to_load_post), it);
|
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 };
|
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
||||||
|
|
||||||
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
||||||
@@ -211,6 +212,8 @@ class PostDetailFragment : MainFragment {
|
|||||||
|
|
||||||
_repliesOverlay = findViewById(R.id.replies_overlay);
|
_repliesOverlay = findViewById(R.id.replies_overlay);
|
||||||
|
|
||||||
|
_textContent.setPlatformPlayerLinkMovementMethod(context);
|
||||||
|
|
||||||
_buttonSubscribe.onSubscribed.subscribe {
|
_buttonSubscribe.onSubscribed.subscribe {
|
||||||
//TODO: add overlay to layout
|
//TODO: add overlay to layout
|
||||||
//UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
//UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||||
@@ -473,6 +476,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCommentType(true);
|
updateCommentType(true);
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPostOverview(value: IPlatformPost) {
|
fun setPostOverview(value: IPlatformPost) {
|
||||||
|
|||||||
+37
-8
@@ -12,6 +12,7 @@ import android.webkit.CookieManager
|
|||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
@@ -100,6 +101,11 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
loadConfig(parameter);
|
loadConfig(parameter);
|
||||||
updateSourceViews();
|
updateSourceViews();
|
||||||
}
|
}
|
||||||
|
else if(parameter is UpdatePluginAction) {
|
||||||
|
loadConfig(parameter.config);
|
||||||
|
updateSourceViews();
|
||||||
|
checkForUpdatesSource();
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -107,17 +113,20 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
fun onHide() {
|
fun onHide() {
|
||||||
val id = _config?.id ?: return;
|
val id = _config?.id ?: return;
|
||||||
|
|
||||||
if(_settingsChanged && _settings != null) {
|
var shouldReload = false;
|
||||||
_settingsChanged = false;
|
|
||||||
StatePlugins.instance.setPluginSettings(id, _settings!!);
|
|
||||||
reloadSource(id);
|
|
||||||
|
|
||||||
UIDialogs.toast(context.getString(R.string.plugin_settings_saved), false);
|
|
||||||
}
|
|
||||||
if(_settingsAppChanged) {
|
if(_settingsAppChanged) {
|
||||||
_settingsAppForm.setObjectValues();
|
_settingsAppForm.setObjectValues();
|
||||||
StatePlugins.instance.savePlugin(id);
|
StatePlugins.instance.savePlugin(id);
|
||||||
|
shouldReload = true;
|
||||||
}
|
}
|
||||||
|
if(_settingsChanged && _settings != null) {
|
||||||
|
_settingsChanged = false;
|
||||||
|
StatePlugins.instance.setPluginSettings(id, _settings!!);
|
||||||
|
shouldReload = true;
|
||||||
|
UIDialogs.toast(context.getString(R.string.plugin_settings_saved), false);
|
||||||
|
}
|
||||||
|
if(shouldReload)
|
||||||
|
reloadSource(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -137,9 +146,25 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
//App settings
|
//App settings
|
||||||
try {
|
try {
|
||||||
_settingsAppForm.fromObject(source.descriptor.appSettings);
|
_settingsAppForm.fromObject(source.descriptor.appSettings);
|
||||||
|
if(source.config.developerSubmitUrl.isNullOrEmpty()) {
|
||||||
|
val field = _settingsAppForm.findField("devSubmit");
|
||||||
|
field?.setValue(false);
|
||||||
|
if(field is View)
|
||||||
|
field.isVisible = false;
|
||||||
|
}
|
||||||
_settingsAppForm.onChanged.clear();
|
_settingsAppForm.onChanged.clear();
|
||||||
_settingsAppForm.onChanged.subscribe { _, _ ->
|
_settingsAppForm.onChanged.subscribe { field, value ->
|
||||||
_settingsAppChanged = true;
|
_settingsAppChanged = true;
|
||||||
|
if(field.descriptor?.id == "devSubmit") {
|
||||||
|
if(value is Boolean && value) {
|
||||||
|
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow,
|
||||||
|
"Are you sure you trust the developer?",
|
||||||
|
"Developers may gain access to sensitive data. Only enable this when you are trying to help the developer fix a bug.\nThe following domain is used:",
|
||||||
|
source.config.developerSubmitUrl ?: "", 0,
|
||||||
|
UIDialogs.Action("Cancel", { field.setValue(false); }, UIDialogs.ActionStyle.NONE),
|
||||||
|
UIDialogs.Action("Enable", { }, UIDialogs.ActionStyle.DANGEROUS));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to load app settings form from plugin settings", e)
|
Logger.e(TAG, "Failed to load app settings form from plugin settings", e)
|
||||||
@@ -547,4 +572,8 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
const val TAG = "SourceDetailFragment";
|
const val TAG = "SourceDetailFragment";
|
||||||
fun newInstance() = SourceDetailFragment().apply {}
|
fun newInstance() = SourceDetailFragment().apply {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class UpdatePluginAction(val config: SourcePluginConfig) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+8
-2
@@ -25,6 +25,7 @@ import com.futo.platformplayer.models.SearchType
|
|||||||
import com.futo.platformplayer.models.SubscriptionGroup
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateCache
|
import com.futo.platformplayer.states.StateCache
|
||||||
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
@@ -197,6 +198,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST);
|
val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST);
|
||||||
var allowLive: Boolean = true;
|
var allowLive: Boolean = true;
|
||||||
var allowPlanned: Boolean = false;
|
var allowPlanned: Boolean = false;
|
||||||
|
var allowWatched: Boolean = true;
|
||||||
override fun encode(): String {
|
override fun encode(): String {
|
||||||
return Json.encodeToString(this);
|
return Json.encodeToString(this);
|
||||||
}
|
}
|
||||||
@@ -260,7 +262,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
||||||
if(it !is CancellationException)
|
if(it !is CancellationException)
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults(true) });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults(true) }, null, fragment);
|
||||||
else {
|
else {
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -304,7 +306,8 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
SubscriptionBar.Toggle(context.getString(R.string.videos), _filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), it); },
|
SubscriptionBar.Toggle(context.getString(R.string.videos), _filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), it); },
|
||||||
SubscriptionBar.Toggle(context.getString(R.string.posts), _filterSettings.allowContentTypes.contains(ContentType.POST)) { toggleFilterContentType(ContentType.POST, it); },
|
SubscriptionBar.Toggle(context.getString(R.string.posts), _filterSettings.allowContentTypes.contains(ContentType.POST)) { toggleFilterContentType(ContentType.POST, it); },
|
||||||
SubscriptionBar.Toggle(context.getString(R.string.live), _filterSettings.allowLive) { _filterSettings.allowLive = it; _filterSettings.save(); loadResults(false); },
|
SubscriptionBar.Toggle(context.getString(R.string.live), _filterSettings.allowLive) { _filterSettings.allowLive = it; _filterSettings.save(); loadResults(false); },
|
||||||
SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); }
|
SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); },
|
||||||
|
SubscriptionBar.Toggle(context.getString(R.string.watched), _filterSettings.allowWatched) { _filterSettings.allowWatched = it; _filterSettings.save(); loadResults(false); }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,6 +339,9 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
return results.filter {
|
return results.filter {
|
||||||
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
|
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
|
||||||
|
|
||||||
|
if(it is IPlatformVideo && it.duration > 0 && !_filterSettings.allowWatched && StateHistory.instance.isHistoryWatched(it.url, it.duration))
|
||||||
|
return@filter false;
|
||||||
|
|
||||||
//TODO: Check against a sub cache
|
//TODO: Check against a sub cache
|
||||||
if(filterGroup != null && !filterGroup.urls.contains(it.author.url))
|
if(filterGroup != null && !filterGroup.urls.contains(it.author.url))
|
||||||
return@filter false;
|
return@filter false;
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ class SuggestionsFragment : MainFragment {
|
|||||||
.success { suggestions -> updateSuggestions(suggestions, false) }
|
.success { suggestions -> updateSuggestions(suggestions, false) }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load suggestions.", it);
|
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() {
|
constructor(): super() {
|
||||||
|
|||||||
+38
-15
@@ -23,6 +23,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import android.webkit.WebView
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
@@ -124,6 +125,7 @@ import com.futo.platformplayer.views.overlays.LiveChatOverlay
|
|||||||
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
||||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||||
import com.futo.platformplayer.views.overlays.SupportOverlay
|
import com.futo.platformplayer.views.overlays.SupportOverlay
|
||||||
|
import com.futo.platformplayer.views.overlays.WebviewOverlay
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuButtonList
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuButtonList
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
@@ -244,6 +246,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private val _container_content_replies: RepliesOverlay;
|
private val _container_content_replies: RepliesOverlay;
|
||||||
private val _container_content_description: DescriptionOverlay;
|
private val _container_content_description: DescriptionOverlay;
|
||||||
private val _container_content_liveChat: LiveChatOverlay;
|
private val _container_content_liveChat: LiveChatOverlay;
|
||||||
|
private val _container_content_browser: WebviewOverlay;
|
||||||
private val _container_content_support: SupportOverlay;
|
private val _container_content_support: SupportOverlay;
|
||||||
|
|
||||||
private var _container_content_current: View;
|
private var _container_content_current: View;
|
||||||
@@ -349,7 +352,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_container_content_replies = findViewById(R.id.videodetail_container_replies);
|
_container_content_replies = findViewById(R.id.videodetail_container_replies);
|
||||||
_container_content_description = findViewById(R.id.videodetail_container_description);
|
_container_content_description = findViewById(R.id.videodetail_container_description);
|
||||||
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
||||||
_container_content_support = findViewById(R.id.videodetail_container_support)
|
_container_content_support = findViewById(R.id.videodetail_container_support);
|
||||||
|
_container_content_browser = findViewById(R.id.videodetail_container_webview)
|
||||||
|
|
||||||
_textComments = findViewById(R.id.text_comments);
|
_textComments = findViewById(R.id.text_comments);
|
||||||
_addCommentView = findViewById(R.id.add_comment_view);
|
_addCommentView = findViewById(R.id.add_comment_view);
|
||||||
@@ -398,6 +402,10 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
_monetization.onUrlTap.subscribe {
|
||||||
|
fragment.navigate<BrowserFragment>(it);
|
||||||
|
onMinimize.emit();
|
||||||
|
}
|
||||||
|
|
||||||
_player.attachPlayer();
|
_player.attachPlayer();
|
||||||
|
|
||||||
@@ -620,6 +628,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_container_content_queue.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_queue.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
|
_container_content_browser.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
|
|
||||||
_description_viewMore.setOnClickListener {
|
_description_viewMore.setOnClickListener {
|
||||||
switchContentView(_container_content_description);
|
switchContentView(_container_content_description);
|
||||||
@@ -640,6 +649,20 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
_container_content_current = _container_content_main;
|
_container_content_current = _container_content_main;
|
||||||
|
|
||||||
|
_commentsList.onAuthorClick.subscribe { c ->
|
||||||
|
if (c !is PolycentricPlatformComment) {
|
||||||
|
return@subscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "onAuthorClick: " + c.author.id.value);
|
||||||
|
if(c.author.id.value?.startsWith("polycentric://") ?: false) {
|
||||||
|
//val navUrl = "https://harbor.social/" + c.author.id.value?.substring("polycentric://".length);
|
||||||
|
val navUrl = "https://polycentric.io/user/" + c.author.id.value?.substring("polycentric://".length);
|
||||||
|
fragment.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(navUrl)))
|
||||||
|
//_container_content_browser.goto(navUrl);
|
||||||
|
//switchContentView(_container_content_browser);
|
||||||
|
}
|
||||||
|
};
|
||||||
_commentsList.onRepliesClick.subscribe { c ->
|
_commentsList.onRepliesClick.subscribe { c ->
|
||||||
val replyCount = c.replyCount ?: 0;
|
val replyCount = c.replyCount ?: 0;
|
||||||
var metadata = "";
|
var metadata = "";
|
||||||
@@ -859,11 +882,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
|
|
||||||
private val _historyIndexLock = Mutex(false);
|
private val _historyIndexLock = Mutex(false);
|
||||||
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index = withContext(Dispatchers.IO){
|
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index? = withContext(Dispatchers.IO){
|
||||||
_historyIndexLock.withLock {
|
_historyIndexLock.withLock {
|
||||||
val current = _historyIndex;
|
val current = _historyIndex;
|
||||||
if(current == null || current.url != video.url) {
|
if(current == null || current.url != video.url) {
|
||||||
val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
|
val index = StateHistory.instance.getHistoryByVideo(video, true);
|
||||||
_historyIndex = index;
|
_historyIndex = index;
|
||||||
return@withContext index;
|
return@withContext index;
|
||||||
}
|
}
|
||||||
@@ -1035,10 +1058,10 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
switchContentView(_container_content_main);
|
switchContentView(_container_content_main);
|
||||||
}
|
}
|
||||||
fun setVideoOverview(video: IPlatformVideo, fetch: Boolean = true, resumeSeconds: Long = 0) {
|
fun setVideoOverview(video: IPlatformVideo, fetch: Boolean = true, resumeSeconds: Long = 0, bypassSameVideoCheck: Boolean = false) {
|
||||||
Logger.i(TAG, "setVideoOverview")
|
Logger.i(TAG, "setVideoOverview")
|
||||||
|
|
||||||
if(this.video?.url == video.url)
|
if(!bypassSameVideoCheck && this.video?.url == video.url)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
val cachedVideo = StateDownloads.instance.getCachedVideo(video.id);
|
val cachedVideo = StateDownloads.instance.getCachedVideo(video.id);
|
||||||
@@ -1390,7 +1413,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
if (video !is TutorialFragment.TutorialVideo) {
|
if (video !is TutorialFragment.TutorialVideo) {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val historyItem = getHistoryIndex(videoDetail);
|
val historyItem = getHistoryIndex(videoDetail) ?: return@launch;
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
|
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
|
||||||
@@ -1663,7 +1686,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
Logger.i(TAG, "prevVideo")
|
Logger.i(TAG, "prevVideo")
|
||||||
val next = StatePlayer.instance.prevQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
|
val next = StatePlayer.instance.prevQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
|
||||||
if(next != null) {
|
if(next != null) {
|
||||||
setVideoOverview(next);
|
setVideoOverview(next, true, 0, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1673,7 +1696,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if(next == null && forceLoop)
|
if(next == null && forceLoop)
|
||||||
next = StatePlayer.instance.restartQueue();
|
next = StatePlayer.instance.restartQueue();
|
||||||
if(next != null) {
|
if(next != null) {
|
||||||
setVideoOverview(next);
|
setVideoOverview(next, true, 0, true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -2208,11 +2231,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
videoSourceHeight = 9;
|
videoSourceHeight = 9;
|
||||||
}
|
}
|
||||||
val aspectRatio = videoSourceWidth.toDouble() / videoSourceHeight;
|
val aspectRatio = videoSourceWidth.toDouble() / videoSourceHeight;
|
||||||
if(aspectRatio > 3) {
|
if(aspectRatio > 2.38) {
|
||||||
videoSourceWidth = 16;
|
videoSourceWidth = 16;
|
||||||
videoSourceHeight = 9;
|
videoSourceHeight = 9;
|
||||||
}
|
}
|
||||||
else if(aspectRatio < 0.3) {
|
else if(aspectRatio < 0.43) {
|
||||||
videoSourceHeight = 16;
|
videoSourceHeight = 16;
|
||||||
videoSourceWidth = 9;
|
videoSourceWidth = 9;
|
||||||
}
|
}
|
||||||
@@ -2252,7 +2275,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
|
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
|
||||||
if (v !is TutorialFragment.TutorialVideo) {
|
if (v !is TutorialFragment.TutorialVideo) {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val history = getHistoryIndex(v);
|
val history = getHistoryIndex(v) ?: return@launch;
|
||||||
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2453,7 +2476,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
Logger.w(TAG, "exception<ScriptImplementationException>", it)
|
Logger.w(TAG, "exception<ScriptImplementationException>", it)
|
||||||
|
|
||||||
if (!nextVideo()) {
|
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 {
|
} else {
|
||||||
StateAnnouncement.instance.registerAnnouncement(video?.id?.value + "_Q_INVALIDVIDEO", context.getString(R.string.invalid_video), context.getString(
|
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)
|
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)
|
||||||
@@ -2489,7 +2512,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_retryJob = null;
|
_retryJob = null;
|
||||||
_liveTryJob?.cancel();
|
_liveTryJob?.cancel();
|
||||||
_liveTryJob = null;
|
_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> {
|
.exception<Throwable> {
|
||||||
@@ -2501,7 +2524,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_retryJob = null;
|
_retryJob = null;
|
||||||
_liveTryJob?.cancel();
|
_liveTryJob?.cancel();
|
||||||
_liveTryJob = null;
|
_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});
|
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
|
||||||
|
|
||||||
@@ -2562,7 +2585,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
setVideoDetails(videoDetail, true);
|
setVideoDetails(videoDetail, false);
|
||||||
_liveTryJob = null;
|
_liveTryJob = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
@@ -1,6 +1,7 @@
|
|||||||
package com.futo.platformplayer.fragment.mainactivity.main
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
@@ -8,10 +9,17 @@ import android.widget.ImageButton
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.setPadding
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.assume
|
||||||
|
import com.futo.platformplayer.downloads.VideoDownload
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.views.lists.VideoListEditorView
|
import com.futo.platformplayer.views.lists.VideoListEditorView
|
||||||
|
|
||||||
abstract class VideoListEditorView : LinearLayout {
|
abstract class VideoListEditorView : LinearLayout {
|
||||||
@@ -85,6 +93,44 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun updateDownloadState(groupType: String, playlistId: String, onDownload: ()->Unit) {
|
||||||
|
//val playlist = _playlist ?: return;
|
||||||
|
val isDownloading = StateDownloads.instance.getDownloading().any { it.groupType == groupType && it.groupID == playlistId };
|
||||||
|
val isDownloaded = StateDownloads.instance.isPlaylistCached(playlistId);
|
||||||
|
|
||||||
|
val dp10 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics);
|
||||||
|
|
||||||
|
if(isDownloaded && !isDownloading)
|
||||||
|
_buttonDownload.setBackgroundResource(R.drawable.background_button_round_green);
|
||||||
|
else
|
||||||
|
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
|
||||||
|
|
||||||
|
if(isDownloading) {
|
||||||
|
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
||||||
|
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
||||||
|
_buttonDownload.setOnClickListener {
|
||||||
|
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||||
|
StateDownloads.instance.deleteCachedPlaylist(playlistId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(isDownloaded) {
|
||||||
|
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
||||||
|
_buttonDownload.setOnClickListener {
|
||||||
|
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||||
|
StateDownloads.instance.deleteCachedPlaylist(playlistId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_buttonDownload.setImageResource(R.drawable.ic_download);
|
||||||
|
_buttonDownload.setOnClickListener {
|
||||||
|
onDownload();
|
||||||
|
//UISlideOverlays.showDownloadPlaylistOverlay(playlist, overlayContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_buttonDownload.setPadding(dp10.toInt());
|
||||||
|
}
|
||||||
|
|
||||||
protected fun setName(name: String?) {
|
protected fun setName(name: String?) {
|
||||||
_textName.text = name ?: "";
|
_textName.text = name ?: "";
|
||||||
|
|||||||
+41
@@ -5,10 +5,17 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
|
import com.futo.platformplayer.downloads.VideoDownload
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class WatchLaterFragment : MainFragment() {
|
class WatchLaterFragment : MainFragment() {
|
||||||
override val isMainView : Boolean = true;
|
override val isMainView : Boolean = true;
|
||||||
@@ -28,6 +35,11 @@ class WatchLaterFragment : MainFragment() {
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
_view?.onResume();
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyMainView() {
|
override fun onDestroyMainView() {
|
||||||
super.onDestroyMainView();
|
super.onDestroyMainView();
|
||||||
_view = null;
|
_view = null;
|
||||||
@@ -45,6 +57,34 @@ class WatchLaterFragment : MainFragment() {
|
|||||||
fun onShown() {
|
fun onShown() {
|
||||||
setName("Watch Later");
|
setName("Watch Later");
|
||||||
setVideos(StatePlaylists.instance.getWatchLater(), true);
|
setVideos(StatePlaylists.instance.getWatchLater(), true);
|
||||||
|
|
||||||
|
setButtonDownloadVisible(true);
|
||||||
|
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onResume(){
|
||||||
|
StateDownloads.instance.onDownloadsChanged.subscribe(this) {
|
||||||
|
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
StateDownloads.instance.onDownloadedChanged.subscribe(this) {
|
||||||
|
_fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
updateDownloadState(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER, this@WatchLaterView::download);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to update download state onDownloadedChanged.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fun download(){
|
||||||
|
UISlideOverlays.showDownloadWatchlaterOverlay(overlayContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayAllClick() {
|
override fun onPlayAllClick() {
|
||||||
@@ -76,6 +116,7 @@ class WatchLaterFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
val TAG = "WatchLaterFragment";
|
||||||
fun newInstance() = WatchLaterFragment().apply {}
|
fun newInstance() = WatchLaterFragment().apply {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,17 @@ package com.futo.platformplayer.helpers
|
|||||||
|
|
||||||
class FileHelper {
|
class FileHelper {
|
||||||
companion object {
|
companion object {
|
||||||
val allowedCharacters = HashSet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-.".toCharArray().toList());
|
fun String.sanitizeFileName(allowSpace: Boolean = false): String {
|
||||||
|
return this.filter {
|
||||||
|
(it in '0' .. '9') ||
|
||||||
fun String.sanitizeFileName(): String {
|
(it in 'a'..'z') ||
|
||||||
return this.filter { allowedCharacters.contains(it) };
|
(it in 'A'..'Z') ||
|
||||||
|
(it == '-' || it == '.' || it == '_' || (it == ' ' && allowSpace)) ||
|
||||||
|
(it in '丁'..'龤') || //Chinese/Kanji
|
||||||
|
(it in '\u3040'..'\u309f') || //Hiragana
|
||||||
|
(it in '\u30A0'..'\u30ff') || //Katakana
|
||||||
|
(it in '\u0600'..'\u06FF') //Arabic
|
||||||
|
}; //Chinese
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,8 @@ class FileLogConsumer : ILogConsumer, Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
while (_linesToWrite.isNotEmpty()) {
|
while (_linesToWrite.isNotEmpty()) {
|
||||||
_writer?.appendLine(_linesToWrite.remove());
|
val todo = _linesToWrite.remove()
|
||||||
|
_writer?.appendLine(todo);
|
||||||
}
|
}
|
||||||
|
|
||||||
_writer?.flush();
|
_writer?.flush();
|
||||||
@@ -85,7 +86,7 @@ class FileLogConsumer : ILogConsumer, Closeable {
|
|||||||
_running = false;
|
_running = false;
|
||||||
_writer?.close();
|
_writer?.close();
|
||||||
_writer = null;
|
_writer = null;
|
||||||
_logThread?.join();
|
//_logThread?.join();
|
||||||
_logThread = null;
|
_logThread = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class HistoryVideo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromReconString(str: String, resolve: ((url: String)->SerializedPlatformVideo)? = null): HistoryVideo {
|
fun fromReconString(str: String, resolve: ((url: String)->SerializedPlatformVideo?)? = null): HistoryVideo {
|
||||||
var index = str.indexOf("|||");
|
var index = str.indexOf("|||");
|
||||||
if(index < 0) throw IllegalArgumentException("Invalid history string: " + str);
|
if(index < 0) throw IllegalArgumentException("Invalid history string: " + str);
|
||||||
val url = str.substring(0, index);
|
val url = str.substring(0, index);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.futo.platformplayer.models
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ImportCache(
|
||||||
|
var videos: List<SerializedPlatformVideo>? = null,
|
||||||
|
var channels: List<SerializedChannel>? = null
|
||||||
|
);
|
||||||
@@ -40,6 +40,9 @@ class Subscription {
|
|||||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
var lastPostUpdate : OffsetDateTime = OffsetDateTime.MIN;
|
var lastPostUpdate : OffsetDateTime = OffsetDateTime.MIN;
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||||
|
var lastPeekVideo : OffsetDateTime = OffsetDateTime.MIN;
|
||||||
|
|
||||||
//Last video interval
|
//Last video interval
|
||||||
var uploadInterval : Int = 0;
|
var uploadInterval : Int = 0;
|
||||||
var uploadStreamInterval : Int = 0;
|
var uploadStreamInterval : Int = 0;
|
||||||
@@ -126,6 +129,7 @@ class Subscription {
|
|||||||
else if(lastVideo.year > 3000)
|
else if(lastVideo.year > 3000)
|
||||||
lastVideo = OffsetDateTime.MIN;
|
lastVideo = OffsetDateTime.MIN;
|
||||||
lastVideoUpdate = OffsetDateTime.now();
|
lastVideoUpdate = OffsetDateTime.now();
|
||||||
|
lastPeekVideo = OffsetDateTime.MIN;
|
||||||
}
|
}
|
||||||
ResultCapabilities.TYPE_MIXED -> {
|
ResultCapabilities.TYPE_MIXED -> {
|
||||||
uploadInterval = interval;
|
uploadInterval = interval;
|
||||||
@@ -134,6 +138,7 @@ class Subscription {
|
|||||||
else if(lastVideo.year > 3000)
|
else if(lastVideo.year > 3000)
|
||||||
lastVideo = OffsetDateTime.MIN;
|
lastVideo = OffsetDateTime.MIN;
|
||||||
lastVideoUpdate = OffsetDateTime.now();
|
lastVideoUpdate = OffsetDateTime.now();
|
||||||
|
lastPeekVideo = OffsetDateTime.MIN;
|
||||||
}
|
}
|
||||||
ResultCapabilities.TYPE_SUBSCRIPTIONS -> {
|
ResultCapabilities.TYPE_SUBSCRIPTIONS -> {
|
||||||
uploadInterval = interval;
|
uploadInterval = interval;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.futo.platformplayer.activities.MainActivity
|
|||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||||
import com.futo.platformplayer.timestampRegex
|
import com.futo.platformplayer.timestampRegex
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class PlatformLinkMovementMethod : LinkMovementMethod {
|
class PlatformLinkMovementMethod : LinkMovementMethod {
|
||||||
private val _context: Context;
|
private val _context: Context;
|
||||||
@@ -32,33 +33,36 @@ class PlatformLinkMovementMethod : LinkMovementMethod {
|
|||||||
val links = buffer.getSpans(off, off, URLSpan::class.java);
|
val links = buffer.getSpans(off, off, URLSpan::class.java);
|
||||||
|
|
||||||
if (links.isNotEmpty()) {
|
if (links.isNotEmpty()) {
|
||||||
for (link in links) {
|
runBlocking {
|
||||||
Logger.i(TAG) { "Link clicked '${link.url}'." };
|
for (link in links) {
|
||||||
|
Logger.i(TAG) { "Link clicked '${link.url}'." };
|
||||||
|
|
||||||
if (_context is MainActivity) {
|
if (_context is MainActivity) {
|
||||||
if (_context.handleUrl(link.url)) {
|
if (_context.handleUrl(link.url)) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timestampRegex.matches(link.url)) {
|
|
||||||
val tokens = link.url.split(':');
|
|
||||||
|
|
||||||
var time_s = -1L;
|
|
||||||
if (tokens.size == 2) {
|
|
||||||
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
|
||||||
} else if (tokens.size == 3) {
|
|
||||||
time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (time_s != -1L) {
|
|
||||||
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (timestampRegex.matches(link.url)) {
|
||||||
|
val tokens = link.url.split(':');
|
||||||
|
|
||||||
|
var time_s = -1L;
|
||||||
|
if (tokens.size == 2) {
|
||||||
|
time_s = tokens[0].toLong() * 60 + tokens[1].toLong();
|
||||||
|
} else if (tokens.size == 3) {
|
||||||
|
time_s =
|
||||||
|
tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time_s != -1L) {
|
||||||
|
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -316,7 +316,6 @@ class PolycentricCache {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
private const val TAG = "PolycentricCache"
|
private const val TAG = "PolycentricCache"
|
||||||
const val STAGING_SERVER = "https://srv1-stg.polycentric.io"
|
|
||||||
const val SERVER = "https://srv1-prod.polycentric.io"
|
const val SERVER = "https://srv1-prod.polycentric.io"
|
||||||
private var _instance: PolycentricCache? = null;
|
private var _instance: PolycentricCache? = null;
|
||||||
private val CACHE_EXPIRATION_SECONDS = 60 * 5;
|
private val CACHE_EXPIRATION_SECONDS = 60 * 5;
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.futo.platformplayer.polycentric
|
||||||
|
|
||||||
|
import com.futo.platformplayer.encryption.GEncryptionProviderV1
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
|
import com.futo.polycentric.core.ProcessSecret
|
||||||
|
import com.futo.polycentric.core.base64ToByteArray
|
||||||
|
import com.futo.polycentric.core.toBase64
|
||||||
|
import userpackage.Protocol
|
||||||
|
|
||||||
|
class PolycentricStorage {
|
||||||
|
private val _processSecrets = FragmentedStorage.get<StringArrayStorage>("processSecrets");
|
||||||
|
|
||||||
|
fun addProcessSecret(processSecret: ProcessSecret) {
|
||||||
|
_processSecrets.addDistinct(GEncryptionProviderV1.instance.encrypt(processSecret.toProto().toByteArray()).toBase64())
|
||||||
|
_processSecrets.saveBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getProcessSecrets(): List<ProcessSecret> {
|
||||||
|
val processSecrets = arrayListOf<ProcessSecret>()
|
||||||
|
for (p in _processSecrets.getAllValues()) {
|
||||||
|
try {
|
||||||
|
processSecrets.add(ProcessSecret.fromProto(Protocol.StorageTypeProcessSecret.parseFrom(GEncryptionProviderV1.instance.decrypt(p.base64ToByteArray()))))
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to decrypt process secret", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return processSecrets
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG = "PolycentricStorage";
|
||||||
|
private var _instance : PolycentricStorage? = null;
|
||||||
|
val instance : PolycentricStorage
|
||||||
|
get(){
|
||||||
|
if(_instance == null)
|
||||||
|
_instance = PolycentricStorage();
|
||||||
|
return _instance!!;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,13 +4,18 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
class AudioNoisyReceiver : BroadcastReceiver() {
|
class AudioNoisyReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
Logger.i(TAG, "Audio Noisy received");
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
MediaControlReceiver.onPauseReceived.emit();
|
Logger.i(TAG, "Audio Noisy received");
|
||||||
|
MediaControlReceiver.onPauseReceived.emit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class DownloadService : Service() {
|
|||||||
private val DOWNLOAD_NOTIF_ID = 3;
|
private val DOWNLOAD_NOTIF_ID = 3;
|
||||||
private val DOWNLOAD_NOTIF_TAG = "download";
|
private val DOWNLOAD_NOTIF_TAG = "download";
|
||||||
private val DOWNLOAD_NOTIF_CHANNEL_ID = "downloadChannel";
|
private val DOWNLOAD_NOTIF_CHANNEL_ID = "downloadChannel";
|
||||||
|
private val DOWNLOAD_NOTIF_CHANNEL_NAME = "Downloads";
|
||||||
|
|
||||||
//Context
|
//Context
|
||||||
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
|
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
|
||||||
@@ -95,7 +96,7 @@ class DownloadService : Service() {
|
|||||||
}
|
}
|
||||||
fun setupNotificationRequirements() {
|
fun setupNotificationRequirements() {
|
||||||
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||||
_notificationChannel = NotificationChannel(DOWNLOAD_NOTIF_CHANNEL_ID, "Temp", NotificationManager.IMPORTANCE_DEFAULT).apply {
|
_notificationChannel = NotificationChannel(DOWNLOAD_NOTIF_CHANNEL_ID, DOWNLOAD_NOTIF_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
|
||||||
this.enableVibration(false);
|
this.enableVibration(false);
|
||||||
this.setSound(null, null);
|
this.setSound(null, null);
|
||||||
};
|
};
|
||||||
@@ -269,7 +270,7 @@ class DownloadService : Service() {
|
|||||||
|
|
||||||
fun closeDownloadSession() {
|
fun closeDownloadSession() {
|
||||||
Logger.i(TAG, "closeDownloadSession");
|
Logger.i(TAG, "closeDownloadSession");
|
||||||
stopForeground(STOP_FOREGROUND_DETACH);
|
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||||
_notificationManager?.cancel(DOWNLOAD_NOTIF_ID);
|
_notificationManager?.cancel(DOWNLOAD_NOTIF_ID);
|
||||||
stopService();
|
stopService();
|
||||||
_started = false;
|
_started = false;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class ExportingService : Service() {
|
|||||||
private val EXPORT_NOTIF_ID = 4;
|
private val EXPORT_NOTIF_ID = 4;
|
||||||
private val EXPORT_NOTIF_TAG = "export";
|
private val EXPORT_NOTIF_TAG = "export";
|
||||||
private val EXPORT_NOTIF_CHANNEL_ID = "exportChannel";
|
private val EXPORT_NOTIF_CHANNEL_ID = "exportChannel";
|
||||||
|
private val EXPORT_NOTIF_CHANNEL_NAME = "Export";
|
||||||
|
|
||||||
//Context
|
//Context
|
||||||
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
|
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
|
||||||
@@ -88,7 +89,7 @@ class ExportingService : Service() {
|
|||||||
}
|
}
|
||||||
fun setupNotificationRequirements() {
|
fun setupNotificationRequirements() {
|
||||||
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
|
||||||
_notificationChannel = NotificationChannel(EXPORT_NOTIF_CHANNEL_ID, "Temp", NotificationManager.IMPORTANCE_DEFAULT).apply {
|
_notificationChannel = NotificationChannel(EXPORT_NOTIF_CHANNEL_ID, EXPORT_NOTIF_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
|
||||||
this.enableVibration(false);
|
this.enableVibration(false);
|
||||||
this.setSound(null, null);
|
this.setSound(null, null);
|
||||||
};
|
};
|
||||||
@@ -187,7 +188,7 @@ class ExportingService : Service() {
|
|||||||
|
|
||||||
fun closeExportSession() {
|
fun closeExportSession() {
|
||||||
Logger.i(TAG, "closeExportSession");
|
Logger.i(TAG, "closeExportSession");
|
||||||
stopForeground(STOP_FOREGROUND_DETACH);
|
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||||
_notificationManager?.cancel(EXPORT_NOTIF_ID);
|
_notificationManager?.cancel(EXPORT_NOTIF_ID);
|
||||||
stopService();
|
stopService();
|
||||||
_started = false;
|
_started = false;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import android.net.NetworkRequest
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
|
import android.util.Log
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -53,6 +54,9 @@ import kotlin.system.measureTimeMillis
|
|||||||
class StateApp {
|
class StateApp {
|
||||||
val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active
|
val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active
|
||||||
|
|
||||||
|
val sessionId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
|
||||||
fun getExternalGeneralDirectory(context: Context): DocumentFile? {
|
fun getExternalGeneralDirectory(context: Context): DocumentFile? {
|
||||||
val generalUri = Settings.instance.storage.getStorageGeneralUri();
|
val generalUri = Settings.instance.storage.getStorageGeneralUri();
|
||||||
if(isValidStorageUri(context, generalUri))
|
if(isValidStorageUri(context, generalUri))
|
||||||
@@ -329,7 +333,7 @@ class StateApp {
|
|||||||
suspend fun backgroundStarting(context: Context, scope: CoroutineScope, withFiles: Boolean, withPlugins: Boolean) {
|
suspend fun backgroundStarting(context: Context, scope: CoroutineScope, withFiles: Boolean, withPlugins: Boolean) {
|
||||||
if(contextOrNull == null) {
|
if(contextOrNull == null) {
|
||||||
Logger.i(TAG, "BACKGROUND STATE: Starting");
|
Logger.i(TAG, "BACKGROUND STATE: Starting");
|
||||||
if(!Logger.hasConsumers && BuildConfig.DEBUG) {
|
if(!Logger.hasConsumers && (BuildConfig.DEBUG)) {
|
||||||
Logger.i(TAG, "BACKGROUND STATE: Initialize logger");
|
Logger.i(TAG, "BACKGROUND STATE: Initialize logger");
|
||||||
Logger.setLogConsumers(listOf(AndroidLogConsumer()));
|
Logger.setLogConsumers(listOf(AndroidLogConsumer()));
|
||||||
}
|
}
|
||||||
@@ -473,7 +477,11 @@ class StateApp {
|
|||||||
Logger.i(TAG, "MainApp Started: Initialize [Noisy]");
|
Logger.i(TAG, "MainApp Started: Initialize [Noisy]");
|
||||||
_receiverBecomingNoisy?.let {
|
_receiverBecomingNoisy?.let {
|
||||||
_receiverBecomingNoisy = null;
|
_receiverBecomingNoisy = null;
|
||||||
context.unregisterReceiver(it);
|
try {
|
||||||
|
context.unregisterReceiver(it);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, "Failed to unregister receiver.", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_receiverBecomingNoisy = AudioNoisyReceiver();
|
_receiverBecomingNoisy = AudioNoisyReceiver();
|
||||||
context.registerReceiver(_receiverBecomingNoisy, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
context.registerReceiver(_receiverBecomingNoisy, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
||||||
@@ -563,18 +571,22 @@ class StateApp {
|
|||||||
StateAnnouncement.instance.deleteAnnouncement("plugin-update")
|
StateAnnouncement.instance.deleteAnnouncement("plugin-update")
|
||||||
|
|
||||||
scopeOrNull?.launch(Dispatchers.IO) {
|
scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
val updateAvailable = StatePlatform.instance.checkForUpdates()
|
val updateAvailable = StatePlugins.instance.checkForUpdates()
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (updateAvailable.isNotEmpty()) {
|
if (updateAvailable.isNotEmpty()) {
|
||||||
UIDialogs.appToast(
|
UIDialogs.appToast(
|
||||||
ToastView.Toast(updateAvailable
|
ToastView.Toast(updateAvailable
|
||||||
.map { " - " + it.name }
|
.map { " - " + it.first.name }
|
||||||
.joinToString("\n"),
|
.joinToString("\n"),
|
||||||
true,
|
true,
|
||||||
null,
|
null,
|
||||||
"Plugin updates available"
|
"Plugin updates available"
|
||||||
));
|
));
|
||||||
|
|
||||||
|
for(update in updateAvailable)
|
||||||
|
if(StatePlatform.instance.isClientEnabled(update.first.id))
|
||||||
|
UIDialogs.showPluginUpdateDialog(context, update.first, update.second);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -639,7 +651,11 @@ class StateApp {
|
|||||||
Logger.i(TAG, "App ended");
|
Logger.i(TAG, "App ended");
|
||||||
_receiverBecomingNoisy?.let {
|
_receiverBecomingNoisy?.let {
|
||||||
_receiverBecomingNoisy = null;
|
_receiverBecomingNoisy = null;
|
||||||
context.unregisterReceiver(it);
|
try {
|
||||||
|
context.unregisterReceiver(it);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, "Failed to unregister receiver.", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "Unregistered network callback on connectivityManager.")
|
Logger.i(TAG, "Unregistered network callback on connectivityManager.")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.futo.platformplayer.UIDialogs
|
|||||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.activities.SettingsActivity
|
import com.futo.platformplayer.activities.SettingsActivity
|
||||||
|
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.copyTo
|
import com.futo.platformplayer.copyTo
|
||||||
import com.futo.platformplayer.encryption.GPasswordEncryptionProvider
|
import com.futo.platformplayer.encryption.GPasswordEncryptionProvider
|
||||||
@@ -17,6 +18,7 @@ import com.futo.platformplayer.encryption.GPasswordEncryptionProviderV0
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ImportSubscriptionsFragment
|
||||||
import com.futo.platformplayer.getNowDiffHours
|
import com.futo.platformplayer.getNowDiffHours
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.readBytes
|
import com.futo.platformplayer.readBytes
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
@@ -58,6 +60,19 @@ class StateBackup {
|
|||||||
StatePlaylists.instance.toMigrateCheck()
|
StatePlaylists.instance.toMigrateCheck()
|
||||||
).flatten();
|
).flatten();
|
||||||
|
|
||||||
|
fun getCache(): ImportCache {
|
||||||
|
val allPlaylists = StatePlaylists.instance.getPlaylists();
|
||||||
|
val videos = allPlaylists.flatMap { it.videos }.distinctBy { it.url };
|
||||||
|
|
||||||
|
val allSubscriptions = StateSubscriptions.instance.getSubscriptions();
|
||||||
|
val channels = allSubscriptions.map { it.channel };
|
||||||
|
|
||||||
|
return ImportCache(
|
||||||
|
videos = videos,
|
||||||
|
channels = channels
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun getAutomaticBackupPassword(customPassword: String? = null): String {
|
private fun getAutomaticBackupPassword(customPassword: String? = null): String {
|
||||||
val password = customPassword ?: Settings.instance.backup.autoBackupPassword ?: "";
|
val password = customPassword ?: Settings.instance.backup.autoBackupPassword ?: "";
|
||||||
@@ -233,11 +248,10 @@ class StateBackup {
|
|||||||
.associateBy { it.config.id }
|
.associateBy { it.config.id }
|
||||||
.mapValues { it.value.config.sourceUrl!! };
|
.mapValues { it.value.config.sourceUrl!! };
|
||||||
|
|
||||||
|
val cache = getCache();
|
||||||
|
|
||||||
|
val export = ExportStructure(exportInfo, settings, storesToSave, pluginUrls, pluginSettings, cache);
|
||||||
|
|
||||||
val export = ExportStructure(exportInfo, settings, storesToSave, pluginUrls, pluginSettings);
|
|
||||||
//export.videoCache = StatePlaylists.instance.getHistory()
|
|
||||||
// .distinctBy { it.video.url }
|
|
||||||
// .map { it.video };
|
|
||||||
return export;
|
return export;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +338,7 @@ class StateBackup {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.showImportDialog(context, relevantStore, store.key, store.value) {
|
UIDialogs.showImportDialog(context, relevantStore, store.key, store.value, export.cache) {
|
||||||
synchronized(toAwait) {
|
synchronized(toAwait) {
|
||||||
toAwait.remove(store.key);
|
toAwait.remove(store.key);
|
||||||
if(toAwait.isEmpty())
|
if(toAwait.isEmpty())
|
||||||
@@ -453,8 +467,8 @@ class StateBackup {
|
|||||||
val stores: Map<String, List<String>>,
|
val stores: Map<String, List<String>>,
|
||||||
val plugins: Map<String, String>,
|
val plugins: Map<String, String>,
|
||||||
val pluginSettings: Map<String, Map<String, String?>>,
|
val pluginSettings: Map<String, Map<String, String?>>,
|
||||||
|
var cache: ImportCache? = null
|
||||||
) {
|
) {
|
||||||
var videoCache: List<SerializedPlatformVideo>? = null;
|
|
||||||
|
|
||||||
fun asZip(): ByteArray {
|
fun asZip(): ByteArray {
|
||||||
return ByteArrayOutputStream().use { byteStream ->
|
return ByteArrayOutputStream().use { byteStream ->
|
||||||
@@ -478,6 +492,17 @@ class StateBackup {
|
|||||||
|
|
||||||
zipStream.putNextEntry(ZipEntry("plugin_settings"));
|
zipStream.putNextEntry(ZipEntry("plugin_settings"));
|
||||||
zipStream.write(Json.encodeToString(pluginSettings).toByteArray());
|
zipStream.write(Json.encodeToString(pluginSettings).toByteArray());
|
||||||
|
|
||||||
|
if(cache != null) {
|
||||||
|
if(cache?.videos != null) {
|
||||||
|
zipStream.putNextEntry(ZipEntry("cache_videos"));
|
||||||
|
zipStream.write(Json.encodeToString(cache!!.videos).toByteArray());
|
||||||
|
}
|
||||||
|
if(cache?.channels != null) {
|
||||||
|
zipStream.putNextEntry(ZipEntry("cache_channels"));
|
||||||
|
zipStream.write(Json.encodeToString(cache!!.channels).toByteArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return byteStream.toByteArray();
|
return byteStream.toByteArray();
|
||||||
}
|
}
|
||||||
@@ -492,6 +517,8 @@ class StateBackup {
|
|||||||
val stores: MutableMap<String, List<String>> = mutableMapOf();
|
val stores: MutableMap<String, List<String>> = mutableMapOf();
|
||||||
var plugins: Map<String, String> = mapOf();
|
var plugins: Map<String, String> = mapOf();
|
||||||
var pluginSettings: Map<String, Map<String, String?>> = mapOf();
|
var pluginSettings: Map<String, Map<String, String?>> = mapOf();
|
||||||
|
var videoCache: List<SerializedPlatformVideo>? = null
|
||||||
|
var channelCache: List<SerializedChannel>? = null
|
||||||
|
|
||||||
while (zipStream.nextEntry.also { entry = it } != null) {
|
while (zipStream.nextEntry.also { entry = it } != null) {
|
||||||
if(entry!!.isDirectory)
|
if(entry!!.isDirectory)
|
||||||
@@ -503,6 +530,22 @@ class StateBackup {
|
|||||||
"settings" -> settings = String(zipStream.readBytes());
|
"settings" -> settings = String(zipStream.readBytes());
|
||||||
"plugins" -> plugins = Json.decodeFromString(String(zipStream.readBytes()));
|
"plugins" -> plugins = Json.decodeFromString(String(zipStream.readBytes()));
|
||||||
"plugin_settings" -> pluginSettings = Json.decodeFromString(String(zipStream.readBytes()));
|
"plugin_settings" -> pluginSettings = Json.decodeFromString(String(zipStream.readBytes()));
|
||||||
|
"cache_videos" -> {
|
||||||
|
try {
|
||||||
|
videoCache = Json.decodeFromString(String(zipStream.readBytes()));
|
||||||
|
}
|
||||||
|
catch(ex: Exception) {
|
||||||
|
Logger.e(TAG, "Couldn't deserialize video cache", ex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
"cache_channels" -> {
|
||||||
|
try {
|
||||||
|
channelCache = Json.decodeFromString(String(zipStream.readBytes()));
|
||||||
|
}
|
||||||
|
catch(ex: Exception) {
|
||||||
|
Logger.e(TAG, "Couldn't deserialize channel cache", ex);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
stores[entry!!.name.substring("stores/".length)] = Json.decodeFromString(String(zipStream.readBytes()));
|
stores[entry!!.name.substring("stores/".length)] = Json.decodeFromString(String(zipStream.readBytes()));
|
||||||
@@ -511,7 +554,10 @@ class StateBackup {
|
|||||||
throw IllegalStateException("Failed to parse zip [${entry?.name}] due to ${ex.message}");
|
throw IllegalStateException("Failed to parse zip [${entry?.name}] due to ${ex.message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ExportStructure(exportInfo, settings, stores, plugins, pluginSettings);
|
return ExportStructure(exportInfo, settings, stores, plugins, pluginSettings, ImportCache(
|
||||||
|
videos = videoCache,
|
||||||
|
channels = channelCache
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ class StateDeveloper {
|
|||||||
|
|
||||||
private var _devLogsIndex: Int = 0;
|
private var _devLogsIndex: Int = 0;
|
||||||
private val _devLogs: MutableList<DevLog> = mutableListOf();
|
private val _devLogs: MutableList<DevLog> = mutableListOf();
|
||||||
|
private val _devHttpExchanges: MutableList<DevHttpExchange> = mutableListOf();
|
||||||
|
|
||||||
|
var devProxy: DevProxySettings? = null;
|
||||||
|
|
||||||
fun initializeDev(id: String) {
|
fun initializeDev(id: String) {
|
||||||
currentDevID = id;
|
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?>) {
|
fun setDevClientSettings(settings: HashMap<String, String?>) {
|
||||||
val client = StatePlatform.instance.getDevClient();
|
val client = StatePlatform.instance.getDevClient();
|
||||||
client?.let {
|
client?.let {
|
||||||
@@ -138,4 +156,12 @@ class StateDeveloper {
|
|||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
data class DevLog(val id: Int, val devId: String, val type: String, val log: String);
|
data class DevLog(val id: Int, val devId: String, val type: String, val log: String);
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
data class DevHttpRequest(val method: String, val url: String, val headers: Map<String, String>, val body: String, val status: Int = 0);
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
data class DevHttpExchange(val request: DevHttpRequest, val response: DevHttpRequest);
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
data class DevProxySettings(val url: String, val port: Int)
|
||||||
}
|
}
|
||||||
@@ -97,6 +97,9 @@ class StateDownloads {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getWatchLaterDescriptor(): PlaylistDownloadDescriptor? {
|
||||||
|
return _downloadPlaylists.getItems().find { it.id == VideoDownload.GROUP_WATCHLATER };
|
||||||
|
}
|
||||||
fun getCachedPlaylists(): List<PlaylistDownloaded> {
|
fun getCachedPlaylists(): List<PlaylistDownloaded> {
|
||||||
return _downloadPlaylists.getItems()
|
return _downloadPlaylists.getItems()
|
||||||
.map { Pair(it, StatePlaylists.instance.getPlaylist(it.id)) }
|
.map { Pair(it, StatePlaylists.instance.getPlaylist(it.id)) }
|
||||||
@@ -124,19 +127,32 @@ class StateDownloads {
|
|||||||
val pdl = getPlaylistDownload(id);
|
val pdl = getPlaylistDownload(id);
|
||||||
if(pdl != null)
|
if(pdl != null)
|
||||||
_downloadPlaylists.delete(pdl);
|
_downloadPlaylists.delete(pdl);
|
||||||
getDownloading().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
|
if(id == VideoDownload.GROUP_WATCHLATER) {
|
||||||
.forEach { removeDownload(it) };
|
getDownloading().filter { it.groupType == VideoDownload.GROUP_WATCHLATER && it.groupID == id }
|
||||||
getDownloadedVideos().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
|
.forEach { removeDownload(it) };
|
||||||
.forEach { deleteCachedVideo(it.id) };
|
getDownloadedVideos().filter { it.groupType == VideoDownload.GROUP_WATCHLATER && it.groupID == id }
|
||||||
|
.forEach { deleteCachedVideo(it.id) };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
getDownloading().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
|
||||||
|
.forEach { removeDownload(it) };
|
||||||
|
getDownloadedVideos().filter { it.groupType == VideoDownload.GROUP_PLAYLIST && it.groupID == id }
|
||||||
|
.forEach { deleteCachedVideo(it.id) };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDownloadedVideos(): List<VideoLocal> {
|
fun getDownloadedVideos(): List<VideoLocal> {
|
||||||
return _downloaded.getItems();
|
return _downloaded.getItems();
|
||||||
}
|
}
|
||||||
|
fun getDownloadedVideosPlaylist(str: String): List<VideoLocal> {
|
||||||
|
val videos = _downloaded.findItems { it.groupID == str };
|
||||||
|
return videos;
|
||||||
|
}
|
||||||
|
|
||||||
fun getDownloadPlaylists(): List<PlaylistDownloadDescriptor> {
|
fun getDownloadPlaylists(): List<PlaylistDownloadDescriptor> {
|
||||||
return _downloadPlaylists.getItems();
|
return _downloadPlaylists.getItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isPlaylistCached(id: String): Boolean {
|
fun isPlaylistCached(id: String): Boolean {
|
||||||
return getDownloadPlaylists().any{it.id == id};
|
return getDownloadPlaylists().any{it.id == id};
|
||||||
}
|
}
|
||||||
@@ -177,6 +193,21 @@ class StateDownloads {
|
|||||||
DownloadService.getOrCreateService(it);
|
DownloadService.getOrCreateService(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun checkForOutdatedPlaylistVideos(playlistId: String) {
|
||||||
|
val playlistVideos = if(playlistId == VideoDownload.GROUP_WATCHLATER)
|
||||||
|
(if(getWatchLaterDescriptor() != null) StatePlaylists.instance.getWatchLater() else listOf())
|
||||||
|
else
|
||||||
|
getCachedPlaylist(playlistId)?.playlist?.videos ?: return;
|
||||||
|
val playlistVideosDownloaded = getDownloadedVideosPlaylist(playlistId);
|
||||||
|
val urls = playlistVideos.map { it.url }.toHashSet();
|
||||||
|
for(item in playlistVideosDownloaded) {
|
||||||
|
if(!urls.contains(item.url)) {
|
||||||
|
Logger.i(TAG, "Playlist [${playlistId}] deleting removed video [${item.name}]");
|
||||||
|
deleteCachedVideo(item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
fun checkForOutdatedPlaylists(): Boolean {
|
fun checkForOutdatedPlaylists(): Boolean {
|
||||||
var hasChanged = false;
|
var hasChanged = false;
|
||||||
val playlistsDownloaded = getCachedPlaylists();
|
val playlistsDownloaded = getCachedPlaylists();
|
||||||
@@ -192,9 +223,59 @@ class StateDownloads {
|
|||||||
else
|
else
|
||||||
Logger.v(TAG, "Offline playlist [${playlist.playlist.name}] is up to date");
|
Logger.v(TAG, "Offline playlist [${playlist.playlist.name}] is up to date");
|
||||||
}
|
}
|
||||||
|
val downloadWatchLater = getWatchLaterDescriptor();
|
||||||
|
if(downloadWatchLater != null) {
|
||||||
|
continueDownloadWatchLater(downloadWatchLater);
|
||||||
|
}
|
||||||
return hasChanged;
|
return hasChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun continueDownloadWatchLater(playlistDownload: PlaylistDownloadDescriptor) {
|
||||||
|
var hasNew = false;
|
||||||
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
|
for(item in watchLater) {
|
||||||
|
val existing = getCachedVideo(item.id);
|
||||||
|
|
||||||
|
if(!playlistDownload.shouldDownload(item)) {
|
||||||
|
Logger.i(TAG, "Not downloading for watchlater [${playlistDownload.id}] Video [${item.name}]:${item.url}")
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(existing == null) {
|
||||||
|
val ongoingDownload = getDownloading().find { it.id.value == item.id.value && it.id.value != null };
|
||||||
|
if(ongoingDownload != null) {
|
||||||
|
Logger.i(TAG, "New watchlater video (already downloading) ${item.name}");
|
||||||
|
ongoingDownload.groupID = VideoDownload.GROUP_WATCHLATER;
|
||||||
|
ongoingDownload.groupType = VideoDownload.GROUP_WATCHLATER;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Logger.i(TAG, "New watchlater video ${item.name}");
|
||||||
|
download(VideoDownload(item, playlistDownload.targetPxCount, playlistDownload.targetBitrate)
|
||||||
|
.withGroup(VideoDownload.GROUP_WATCHLATER, VideoDownload.GROUP_WATCHLATER), false);
|
||||||
|
hasNew = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Logger.i(TAG, "New watchlater video (already downloaded) ${item.name}");
|
||||||
|
if(existing.groupID == null) {
|
||||||
|
existing.groupID = VideoDownload.GROUP_WATCHLATER;
|
||||||
|
existing.groupType = VideoDownload.GROUP_WATCHLATER;
|
||||||
|
synchronized(_downloadedSet) {
|
||||||
|
_downloadedSet.add(existing.id);
|
||||||
|
}
|
||||||
|
_downloaded.save(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(watchLater.isNotEmpty() && Settings.instance.downloads.shouldDownload()) {
|
||||||
|
if(hasNew) {
|
||||||
|
UIDialogs.toast("Downloading [Watch Later]")
|
||||||
|
StateApp.withContext {
|
||||||
|
DownloadService.getOrCreateService(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onDownloadsChanged.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
fun continueDownload(playlistDownload: PlaylistDownloadDescriptor, playlist: Playlist) {
|
fun continueDownload(playlistDownload: PlaylistDownloadDescriptor, playlist: Playlist) {
|
||||||
var hasNew = false;
|
var hasNew = false;
|
||||||
for(item in playlist.videos) {
|
for(item in playlist.videos) {
|
||||||
@@ -240,6 +321,11 @@ class StateDownloads {
|
|||||||
onDownloadsChanged.emit();
|
onDownloadsChanged.emit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun downloadWatchLater(targetPixelCount: Long?, targetBitrate: Long?) {
|
||||||
|
val playlistDownload = PlaylistDownloadDescriptor(VideoDownload.GROUP_WATCHLATER, targetPixelCount, targetBitrate);
|
||||||
|
_downloadPlaylists.save(playlistDownload);
|
||||||
|
continueDownloadWatchLater(playlistDownload);
|
||||||
|
}
|
||||||
fun download(playlist: Playlist, targetPixelcount: Long?, targetBitrate: Long?) {
|
fun download(playlist: Playlist, targetPixelcount: Long?, targetBitrate: Long?) {
|
||||||
val playlistDownload = PlaylistDownloadDescriptor(playlist.id, targetPixelcount, targetBitrate);
|
val playlistDownload = PlaylistDownloadDescriptor(playlist.id, targetPixelcount, targetBitrate);
|
||||||
_downloadPlaylists.save(playlistDownload);
|
_downloadPlaylists.save(playlistDownload);
|
||||||
@@ -370,6 +456,18 @@ class StateDownloads {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val currentDownloads = _downloaded.getItems().map { it.url }.toHashSet();
|
||||||
|
val exporting = _exporting.findItems { !currentDownloads.contains(it.videoLocal.url) };
|
||||||
|
for (export in exporting)
|
||||||
|
_exporting.delete(export);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to delete dangling export:", ex);
|
||||||
|
UIDialogs.toast("Failed to delete dangling export:\n" + ex);
|
||||||
|
}
|
||||||
|
|
||||||
return Pair(totalDeletedCount, totalDeleted);
|
return Pair(totalDeletedCount, totalDeleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.HistoryVideo
|
import com.futo.platformplayer.models.HistoryVideo
|
||||||
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.db.ManagedDBStore
|
import com.futo.platformplayer.stores.db.ManagedDBStore
|
||||||
import com.futo.platformplayer.stores.db.types.DBHistory
|
import com.futo.platformplayer.stores.db.types.DBHistory
|
||||||
@@ -19,8 +21,8 @@ class StateHistory {
|
|||||||
private val _historyStore = FragmentedStorage.storeJson<HistoryVideo>("history")
|
private val _historyStore = FragmentedStorage.storeJson<HistoryVideo>("history")
|
||||||
.withRestore(object: ReconstructStore<HistoryVideo>() {
|
.withRestore(object: ReconstructStore<HistoryVideo>() {
|
||||||
override fun toReconstruction(obj: HistoryVideo): String = obj.toReconString();
|
override fun toReconstruction(obj: HistoryVideo): String = obj.toReconString();
|
||||||
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): HistoryVideo
|
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, cache: ImportCache?): HistoryVideo
|
||||||
= HistoryVideo.fromReconString(backup, null);
|
= HistoryVideo.fromReconString(backup) { url -> cache?.videos?.find { it.url == url } };
|
||||||
})
|
})
|
||||||
.load();
|
.load();
|
||||||
|
|
||||||
@@ -49,6 +51,9 @@ class StateHistory {
|
|||||||
fun getHistoryPosition(url: String): Long {
|
fun getHistoryPosition(url: String): Long {
|
||||||
return historyIndex[url]?.position ?: 0;
|
return historyIndex[url]?.position ?: 0;
|
||||||
}
|
}
|
||||||
|
fun isHistoryWatched(url: String, duration: Long): Boolean {
|
||||||
|
return getHistoryPosition(url) > duration * 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun updateHistoryPosition(liveObj: IPlatformVideo, index: DBHistory.Index, updateExisting: Boolean, position: Long = -1L): Long {
|
fun updateHistoryPosition(liveObj: IPlatformVideo, index: DBHistory.Index, updateExisting: Boolean, position: Long = -1L): Long {
|
||||||
@@ -92,14 +97,20 @@ class StateHistory {
|
|||||||
}
|
}
|
||||||
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? {
|
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? {
|
||||||
val existing = historyIndex[video.url];
|
val existing = historyIndex[video.url];
|
||||||
if(existing != null)
|
var result: DBHistory.Index? = null;
|
||||||
return _historyDBStore.get(existing.id!!);
|
if(existing != null) {
|
||||||
|
result = _historyDBStore.getOrNull(existing.id!!);
|
||||||
|
if(result == null)
|
||||||
|
UIDialogs.toast("History item null?\nNo history tracking..");
|
||||||
|
}
|
||||||
else if(create) {
|
else if(create) {
|
||||||
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now());
|
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now());
|
||||||
val id = _historyDBStore.insert(newHistItem);
|
val id = _historyDBStore.insert(newHistItem);
|
||||||
return _historyDBStore.get(id);
|
result = _historyDBStore.getOrNull(id);
|
||||||
|
if(result == null)
|
||||||
|
UIDialogs.toast("History creation failed?\nNo history tracking..");
|
||||||
}
|
}
|
||||||
return null;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeHistory(url: String) {
|
fun removeHistory(url: String) {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import com.futo.platformplayer.models.ImageVariable
|
|||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringArrayStorage
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
import com.futo.platformplayer.stores.StringStorage
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
|
import com.futo.platformplayer.views.ToastView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -79,7 +80,6 @@ class StatePlatform {
|
|||||||
private val _clientsLock = Object();
|
private val _clientsLock = Object();
|
||||||
private val _availableClients : ArrayList<IPlatformClient> = ArrayList();
|
private val _availableClients : ArrayList<IPlatformClient> = ArrayList();
|
||||||
private val _enabledClients : 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
|
//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
|
//This prevents for example a background task like subscriptions from blocking a user from opening a video
|
||||||
@@ -166,8 +166,13 @@ class StatePlatform {
|
|||||||
var enabled: Array<String>;
|
var enabled: Array<String>;
|
||||||
synchronized(_clientsLock) {
|
synchronized(_clientsLock) {
|
||||||
for(e in _enabledClients) {
|
for(e in _enabledClients) {
|
||||||
e.disable();
|
try {
|
||||||
onSourceDisabled.emit(e);
|
e.disable();
|
||||||
|
onSourceDisabled.emit(e);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
UIDialogs.appToast(ToastView.Toast("If this happens often, please inform the developers on Github", false, null, "Plugin [${e.name}] failed to disable"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_enabledClients.clear();
|
_enabledClients.clear();
|
||||||
@@ -523,12 +528,23 @@ class StatePlatform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getCommonSearchCapabilities(clientIds: List<String>): ResultCapabilities? {
|
fun getCommonSearchCapabilities(clientIds: List<String>): ResultCapabilities? {
|
||||||
|
return getCommonSearchCapabilitiesType(clientIds){
|
||||||
|
it.getSearchCapabilities()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fun getCommonSearchChannelContentsCapabilities(clientIds: List<String>): ResultCapabilities? {
|
||||||
|
return getCommonSearchCapabilitiesType(clientIds){
|
||||||
|
it.getSearchChannelContentsCapabilities()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCommonSearchCapabilitiesType(clientIds: List<String>, capabilitiesGetter: (client: IPlatformClient)-> ResultCapabilities): ResultCapabilities? {
|
||||||
try {
|
try {
|
||||||
Logger.i(TAG, "Platform - getCommonSearchCapabilities");
|
Logger.i(TAG, "Platform - getCommonSearchCapabilities");
|
||||||
|
|
||||||
val clients = getEnabledClients().filter { clientIds.contains(it.id) };
|
val clients = getEnabledClients().filter { clientIds.contains(it.id) };
|
||||||
val c = clients.firstOrNull() ?: return null;
|
val c = clients.firstOrNull() ?: return null;
|
||||||
val cap = c.getSearchCapabilities();
|
val cap = capabilitiesGetter(c)//c.getSearchCapabilities();
|
||||||
|
|
||||||
//var types = arrayListOf<String>();
|
//var types = arrayListOf<String>();
|
||||||
var sorts = cap.sorts.toMutableList();
|
var sorts = cap.sorts.toMutableList();
|
||||||
@@ -538,7 +554,7 @@ class StatePlatform {
|
|||||||
val filtersToRemove = arrayListOf<Int>();
|
val filtersToRemove = arrayListOf<Int>();
|
||||||
|
|
||||||
for (i in 1 until clients.size) {
|
for (i in 1 until clients.size) {
|
||||||
val clientSearchCapabilities = clients[i].getSearchCapabilities();
|
val clientSearchCapabilities = capabilitiesGetter(clients[i]);//.getSearchCapabilities();
|
||||||
|
|
||||||
for (j in 0 until sorts.size) {
|
for (j in 0 until sorts.size) {
|
||||||
if (!clientSearchCapabilities.sorts.contains(sorts[j])) {
|
if (!clientSearchCapabilities.sorts.contains(sorts[j])) {
|
||||||
@@ -659,8 +675,11 @@ class StatePlatform {
|
|||||||
|
|
||||||
val pagerResult: IPager<IPlatformContent>;
|
val pagerResult: IPager<IPlatformContent>;
|
||||||
if(!clientCapabilities.hasType(ResultCapabilities.TYPE_MIXED) &&
|
if(!clientCapabilities.hasType(ResultCapabilities.TYPE_MIXED) &&
|
||||||
clientCapabilities.hasType(ResultCapabilities.TYPE_VIDEOS) &&
|
( clientCapabilities.hasType(ResultCapabilities.TYPE_VIDEOS) ||
|
||||||
clientCapabilities.hasType(ResultCapabilities.TYPE_STREAMS)) {
|
clientCapabilities.hasType(ResultCapabilities.TYPE_STREAMS) ||
|
||||||
|
clientCapabilities.hasType(ResultCapabilities.TYPE_LIVE) ||
|
||||||
|
clientCapabilities.hasType(ResultCapabilities.TYPE_POSTS)
|
||||||
|
)) {
|
||||||
val toQuery = mutableListOf<String>();
|
val toQuery = mutableListOf<String>();
|
||||||
if(clientCapabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
if(clientCapabilities.hasType(ResultCapabilities.TYPE_VIDEOS))
|
||||||
toQuery.add(ResultCapabilities.TYPE_VIDEOS);
|
toQuery.add(ResultCapabilities.TYPE_VIDEOS);
|
||||||
@@ -780,6 +799,10 @@ class StatePlatform {
|
|||||||
return client.getChannelContents(channelUrl, type, ordering) ;
|
return client.getChannelContents(channelUrl, type, ordering) ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun peekChannelContents(baseClient: IPlatformClient, channelUrl: String, type: String?): List<IPlatformContent> {
|
||||||
|
val client = _channelClientPool.getClientPooled(baseClient, Settings.instance.subscriptions.getSubscriptionsConcurrency());
|
||||||
|
return client.peekChannelContents(channelUrl, type) ;
|
||||||
|
}
|
||||||
|
|
||||||
fun getChannelLive(url: String, updateSubscriptions: Boolean = true): IPlatformChannel {
|
fun getChannelLive(url: String, updateSubscriptions: Boolean = true): IPlatformChannel {
|
||||||
val channel = getChannelClient(url).getChannel(url);
|
val channel = getChannelClient(url).getChannel(url);
|
||||||
@@ -901,66 +924,7 @@ class StatePlatform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasUpdateAvailable(c: SourcePluginConfig): Boolean {
|
|
||||||
val updatesAvailableMap = _updatesAvailableMap
|
|
||||||
synchronized(updatesAvailableMap) {
|
|
||||||
return updatesAvailableMap.contains(c.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun checkForUpdates(): List<SourcePluginConfig> = withContext(Dispatchers.IO) {
|
|
||||||
var configs = mutableListOf<SourcePluginConfig>()
|
|
||||||
val updatesAvailableFor = hashSetOf<String>()
|
|
||||||
for (availableClient in getAvailableClients().filter { it is JSClient && it.descriptor.appSettings.checkForUpdates }) {
|
|
||||||
if (availableClient !is JSClient) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkForUpdates(availableClient.config)) {
|
|
||||||
configs.add(availableClient.config);
|
|
||||||
updatesAvailableFor.add(availableClient.config.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_updatesAvailableMap = updatesAvailableFor
|
|
||||||
return@withContext configs;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearUpdateAvailable(c: SourcePluginConfig) {
|
|
||||||
val updatesAvailableMap = _updatesAvailableMap
|
|
||||||
synchronized(updatesAvailableMap) {
|
|
||||||
updatesAvailableMap.remove(c.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun checkForUpdates(c: SourcePluginConfig): Boolean = withContext(Dispatchers.IO) {
|
|
||||||
val sourceUrl = c.sourceUrl ?: return@withContext false;
|
|
||||||
|
|
||||||
Logger.i(TAG, "Check for source updates '${c.name}'.");
|
|
||||||
try {
|
|
||||||
val client = ManagedHttpClient();
|
|
||||||
val response = client.get(sourceUrl);
|
|
||||||
Logger.i(TAG, "Downloading source config '$sourceUrl'.");
|
|
||||||
|
|
||||||
if (!response.isOk || response.body == null) {
|
|
||||||
return@withContext false;
|
|
||||||
}
|
|
||||||
|
|
||||||
val configJson = response.body.string();
|
|
||||||
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
|
||||||
|
|
||||||
val config = SourcePluginConfig.fromJson(configJson);
|
|
||||||
if (config.version <= c.version) {
|
|
||||||
return@withContext false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Update is available (config.version=${config.version}, source.config.version=${c.version}).");
|
|
||||||
return@withContext true;
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to check for updates.", e);
|
|
||||||
return@withContext false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var _instance : StatePlatform? = null;
|
private var _instance : StatePlatform? = null;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.services.MediaPlaybackService
|
import com.futo.platformplayer.services.MediaPlaybackService
|
||||||
import com.futo.platformplayer.video.PlayerManager
|
import com.futo.platformplayer.video.PlayerManager
|
||||||
@@ -633,6 +634,7 @@ class StatePlayer {
|
|||||||
val instance = _instance;
|
val instance = _instance;
|
||||||
_instance = null;
|
_instance = null;
|
||||||
instance?.dispose();
|
instance?.dispose();
|
||||||
|
Logger.i(TAG, "Disposed StatePlayer");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,9 +11,11 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.downloads.VideoDownload
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||||
import com.futo.platformplayer.exceptions.ReconstructionException
|
import com.futo.platformplayer.exceptions.ReconstructionException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringArrayStorage
|
import com.futo.platformplayer.stores.StringArrayStorage
|
||||||
@@ -32,8 +34,10 @@ class StatePlaylists {
|
|||||||
.withUnique { it.url }
|
.withUnique { it.url }
|
||||||
.withRestore(object: ReconstructStore<SerializedPlatformVideo>() {
|
.withRestore(object: ReconstructStore<SerializedPlatformVideo>() {
|
||||||
override fun toReconstruction(obj: SerializedPlatformVideo): String = obj.url;
|
override fun toReconstruction(obj: SerializedPlatformVideo): String = obj.url;
|
||||||
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): SerializedPlatformVideo
|
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): SerializedPlatformVideo
|
||||||
= SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
|
= SerializedPlatformVideo.fromVideo(
|
||||||
|
importCache?.videos?.find { it.url == backup }?.let { Logger.i(TAG, "Reconstruction [${backup}] from cache"); return@let it; } ?:
|
||||||
|
StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
|
||||||
})
|
})
|
||||||
.load();
|
.load();
|
||||||
private val _watchlistOrderStore = FragmentedStorage.get<StringArrayStorage>("watchListOrder"); //Temporary workaround to add order..
|
private val _watchlistOrderStore = FragmentedStorage.get<StringArrayStorage>("watchListOrder"); //Temporary workaround to add order..
|
||||||
@@ -63,6 +67,10 @@ class StatePlaylists {
|
|||||||
_watchlistOrderStore.save();
|
_watchlistOrderStore.save();
|
||||||
}
|
}
|
||||||
onWatchLaterChanged.emit();
|
onWatchLaterChanged.emit();
|
||||||
|
|
||||||
|
if(StateDownloads.instance.getWatchLaterDescriptor() != null) {
|
||||||
|
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fun removeFromWatchLater(video: SerializedPlatformVideo) {
|
fun removeFromWatchLater(video: SerializedPlatformVideo) {
|
||||||
synchronized(_watchlistStore) {
|
synchronized(_watchlistStore) {
|
||||||
@@ -71,6 +79,10 @@ class StatePlaylists {
|
|||||||
_watchlistOrderStore.save();
|
_watchlistOrderStore.save();
|
||||||
}
|
}
|
||||||
onWatchLaterChanged.emit();
|
onWatchLaterChanged.emit();
|
||||||
|
|
||||||
|
if(StateDownloads.instance.getWatchLaterDescriptor() != null) {
|
||||||
|
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fun addToWatchLater(video: SerializedPlatformVideo) {
|
fun addToWatchLater(video: SerializedPlatformVideo) {
|
||||||
synchronized(_watchlistStore) {
|
synchronized(_watchlistStore) {
|
||||||
@@ -79,6 +91,8 @@ class StatePlaylists {
|
|||||||
_watchlistOrderStore.save();
|
_watchlistOrderStore.save();
|
||||||
}
|
}
|
||||||
onWatchLaterChanged.emit();
|
onWatchLaterChanged.emit();
|
||||||
|
|
||||||
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLastPlayedPlaylist() : Playlist? {
|
fun getLastPlayedPlaylist() : Playlist? {
|
||||||
@@ -128,6 +142,11 @@ class StatePlaylists {
|
|||||||
fun createOrUpdatePlaylist(playlist: Playlist) {
|
fun createOrUpdatePlaylist(playlist: Playlist) {
|
||||||
playlist.dateUpdate = OffsetDateTime.now();
|
playlist.dateUpdate = OffsetDateTime.now();
|
||||||
playlistStore.saveAsync(playlist, true);
|
playlistStore.saveAsync(playlist, true);
|
||||||
|
if(playlist.id.isNotEmpty()) {
|
||||||
|
if (StateDownloads.instance.isPlaylistCached(playlist.id)) {
|
||||||
|
StateDownloads.instance.checkForOutdatedPlaylistVideos(playlist.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fun addToPlaylist(id: String, video: IPlatformVideo) {
|
fun addToPlaylist(id: String, video: IPlatformVideo) {
|
||||||
synchronized(playlistStore) {
|
synchronized(playlistStore) {
|
||||||
@@ -140,6 +159,9 @@ class StatePlaylists {
|
|||||||
|
|
||||||
fun removePlaylist(playlist: Playlist) {
|
fun removePlaylist(playlist: Playlist) {
|
||||||
playlistStore.delete(playlist);
|
playlistStore.delete(playlist);
|
||||||
|
if(StateDownloads.instance.isPlaylistCached(playlist.id)) {
|
||||||
|
StateDownloads.instance.deleteCachedPlaylist(playlist.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createPlaylistShareUri(context: Context, playlist: Playlist): Uri {
|
fun createPlaylistShareUri(context: Context, playlist: Playlist): Uri {
|
||||||
@@ -154,7 +176,11 @@ class StatePlaylists {
|
|||||||
val reconstruction = playlistStore.getReconstructionString(playlist, true);
|
val reconstruction = playlistStore.getReconstructionString(playlist, true);
|
||||||
|
|
||||||
val newFile = File(playlistShareDir, playlist.name + ".json");
|
val newFile = File(playlistShareDir, playlist.name + ".json");
|
||||||
newFile.writeText(Json.encodeToString(reconstruction.split("\n")), Charsets.UTF_8);
|
newFile.writeText(Json.encodeToString(reconstruction.split("\n") + listOf(
|
||||||
|
"__CACHE:" + Json.encodeToString(ImportCache(
|
||||||
|
videos = playlist.videos.toList()
|
||||||
|
))
|
||||||
|
)), Charsets.UTF_8);
|
||||||
|
|
||||||
return FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), newFile);
|
return FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), newFile);
|
||||||
}
|
}
|
||||||
@@ -185,7 +211,7 @@ class StatePlaylists {
|
|||||||
items.addAll(obj.videos.map { it.url });
|
items.addAll(obj.videos.map { it.url });
|
||||||
return items.map { it.replace("\n","") }.joinToString("\n");
|
return items.map { it.replace("\n","") }.joinToString("\n");
|
||||||
}
|
}
|
||||||
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): Playlist {
|
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): Playlist {
|
||||||
val items = backup.split("\n");
|
val items = backup.split("\n");
|
||||||
if(items.size <= 0) {
|
if(items.size <= 0) {
|
||||||
throw IllegalStateException("Cannot reconstructor playlist ${id}");
|
throw IllegalStateException("Cannot reconstructor playlist ${id}");
|
||||||
@@ -194,10 +220,17 @@ class StatePlaylists {
|
|||||||
val name = items[0];
|
val name = items[0];
|
||||||
val videos = items.drop(1).filter { it.isNotEmpty() }.map {
|
val videos = items.drop(1).filter { it.isNotEmpty() }.map {
|
||||||
try {
|
try {
|
||||||
val video = StatePlatform.instance.getContentDetails(it).await();
|
val videoUrl = it;
|
||||||
|
val video = importCache?.videos?.find { it.url == videoUrl } ?:
|
||||||
|
StatePlatform.instance.getContentDetails(it).await();
|
||||||
if (video is IPlatformVideoDetails) {
|
if (video is IPlatformVideoDetails) {
|
||||||
return@map SerializedPlatformVideo.fromVideo(video);
|
return@map SerializedPlatformVideo.fromVideo(video);
|
||||||
} else {
|
}
|
||||||
|
else if(video is SerializedPlatformVideo) {
|
||||||
|
Logger.i(TAG, "Reconstruction [${it}] from cache");
|
||||||
|
return@map video;
|
||||||
|
}
|
||||||
|
else {
|
||||||
return@map null
|
return@map null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class StatePlugins {
|
|||||||
private var _embeddedSourcesDefault: List<String>? = null
|
private var _embeddedSourcesDefault: List<String>? = null
|
||||||
private var _sourcesUnderConstruction: Map<String, ImageVariable>? = null
|
private var _sourcesUnderConstruction: Map<String, ImageVariable>? = null
|
||||||
|
|
||||||
|
private var _updatesAvailableMap: HashSet<String> = hashSetOf();
|
||||||
|
|
||||||
fun getPluginIconOrNull(id: String): ImageVariable? {
|
fun getPluginIconOrNull(id: String): ImageVariable? {
|
||||||
if(iconsDir.hasIcon(id))
|
if(iconsDir.hasIcon(id))
|
||||||
@@ -55,6 +56,70 @@ class StatePlugins {
|
|||||||
.load();
|
.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 {
|
fun loginPlugin(context: Context, id: String, afterLogin: ()->Unit): Boolean {
|
||||||
val descriptor = getPlugin(id) ?: return false;
|
val descriptor = getPlugin(id) ?: return false;
|
||||||
val config = descriptor.config;
|
val config = descriptor.config;
|
||||||
@@ -134,8 +199,11 @@ class StatePlugins {
|
|||||||
val embeddedConfig = getEmbeddedPluginConfig(context, embedded.value);
|
val embeddedConfig = getEmbeddedPluginConfig(context, embedded.value);
|
||||||
if(embeddedConfig != null) {
|
if(embeddedConfig != null) {
|
||||||
val existing = getPlugin(embedded.key);
|
val existing = getPlugin(embedded.key);
|
||||||
if(existing != null && (existing.config.version < embeddedConfig.version || (force || FORCE_REINSTALL_EMBEDDED))) {
|
if(existing == null || (existing.config.version < embeddedConfig.version || (force || FORCE_REINSTALL_EMBEDDED))) {
|
||||||
Logger.i(TAG, "Outdated Embedded plugin [${existing.config.id}] ${existing.config.name} (${existing.config.version} < ${embeddedConfig.version}), reinstalling");
|
if (existing != null)
|
||||||
|
Logger.i(TAG, "Outdated Embedded plugin [${existing.config.id}] ${existing.config.name} (${existing.config.version} < ${embeddedConfig.version}), reinstalling");
|
||||||
|
else
|
||||||
|
Logger.i(TAG, "Embedded plugin nog installed [${embeddedConfig.id}] ${embeddedConfig.name} (${embeddedConfig.version}), installing");
|
||||||
installEmbeddedPlugin(context, embedded.value)
|
installEmbeddedPlugin(context, embedded.value)
|
||||||
}
|
}
|
||||||
else if(existing != null && _isFirstEmbedUpdate) {
|
else if(existing != null && _isFirstEmbedUpdate) {
|
||||||
@@ -350,6 +418,49 @@ class StatePlugins {
|
|||||||
else verifyCanInstall();
|
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? {
|
fun getPlugin(id: String): SourcePluginDescriptor? {
|
||||||
if(id == StateDeveloper.DEV_ID)
|
if(id == StateDeveloper.DEV_ID)
|
||||||
throw IllegalStateException("Attempted to retrieve a persistent developer plugin, this is not allowed");
|
throw IllegalStateException("Attempted to retrieve a persistent developer plugin, this is not allowed");
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.futo.platformplayer.dp
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
@@ -67,28 +68,40 @@ class StatePolycentric {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
for (i in 0 .. 1) {
|
||||||
val db = SqlLiteDbHelper(context);
|
try {
|
||||||
Store.initializeSqlLiteStore(db);
|
val db = SqlLiteDbHelper(context);
|
||||||
|
Store.initializeSqlLiteStore(db);
|
||||||
|
|
||||||
val activeProcessHandleString = _activeProcessHandle.value;
|
val activeProcessHandleString = _activeProcessHandle.value;
|
||||||
if (activeProcessHandleString.isNotEmpty()) {
|
if (activeProcessHandleString.isNotEmpty()) {
|
||||||
try {
|
try {
|
||||||
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
||||||
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
db.upgradeOldSecrets(db.writableDatabase);
|
db.upgradeOldSecrets(db.writableDatabase);
|
||||||
|
|
||||||
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
||||||
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
||||||
|
|
||||||
|
Log.i(TAG, "Failed to initialize Polycentric.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getProcessHandles()
|
||||||
|
|
||||||
|
break;
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (i == 0) {
|
||||||
|
Logger.i(TAG, "Clearing Polycentric database due to corruption");
|
||||||
|
val db = SqlLiteDbHelper(context);
|
||||||
|
db.recreate()
|
||||||
|
} else {
|
||||||
|
_transientEnabled = false
|
||||||
|
UIDialogs.showGeneralErrorDialog(context, "Failed to initialize Polycentric.", e);
|
||||||
Log.i(TAG, "Failed to initialize Polycentric.", e)
|
Log.i(TAG, "Failed to initialize Polycentric.", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
|
||||||
_transientEnabled = false
|
|
||||||
UIDialogs.showGeneralErrorDialog(context, "Failed to initialize Polycentric.", e);
|
|
||||||
Log.i(TAG, "Failed to initialize Polycentric.", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +116,32 @@ class StatePolycentric {
|
|||||||
return listOf()
|
return listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
return Store.instance.getProcessSecrets().map { it.toProcessHandle(); };
|
val storeProcessSecrets = Store.instance.getProcessSecrets().toMutableList()
|
||||||
|
val processSecrets = PolycentricStorage.instance.getProcessSecrets()
|
||||||
|
|
||||||
|
for (processSecret in processSecrets)
|
||||||
|
{
|
||||||
|
if (!storeProcessSecrets.contains(processSecret)) {
|
||||||
|
try {
|
||||||
|
Store.instance.addProcessSecret(processSecret)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to backfill process secret.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (processSecret in storeProcessSecrets)
|
||||||
|
{
|
||||||
|
if (!processSecrets.contains(processSecret)) {
|
||||||
|
try {
|
||||||
|
PolycentricStorage.instance.addProcessSecret(processSecret)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to backfill process secret.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (storeProcessSecrets + processSecrets).distinct().map { it.toProcessHandle() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setProcessHandle(processHandle: ProcessHandle?) {
|
fun setProcessHandle(processHandle: ProcessHandle?) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.futo.platformplayer.constructs.Event2
|
|||||||
import com.futo.platformplayer.constructs.Event3
|
import com.futo.platformplayer.constructs.Event3
|
||||||
import com.futo.platformplayer.functional.CentralizedFeed
|
import com.futo.platformplayer.functional.CentralizedFeed
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.models.SubscriptionGroup
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
@@ -38,8 +39,8 @@ class StateSubscriptions {
|
|||||||
.withRestore(object: ReconstructStore<Subscription>(){
|
.withRestore(object: ReconstructStore<Subscription>(){
|
||||||
override fun toReconstruction(obj: Subscription): String =
|
override fun toReconstruction(obj: Subscription): String =
|
||||||
obj.channel.url;
|
obj.channel.url;
|
||||||
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): Subscription =
|
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): Subscription =
|
||||||
Subscription(SerializedChannel.fromChannel(StatePlatform.instance.getChannelLive(backup, false)));
|
Subscription(importCache?.channels?.find { it.isSameUrl(backup) } ?: SerializedChannel.fromChannel(StatePlatform.instance.getChannelLive(backup, false)));
|
||||||
}).load();
|
}).load();
|
||||||
private val _subscriptionOthers = FragmentedStorage.storeJson<Subscription>("subscriptions_others")
|
private val _subscriptionOthers = FragmentedStorage.storeJson<Subscription>("subscriptions_others")
|
||||||
.withUnique { it.channel.url }
|
.withUnique { it.channel.url }
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ class StateUpdate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean) = withContext(Dispatchers.IO) {
|
suspend fun checkForUpdates(context: Context, showUpToDateToast: Boolean, hideExceptionButtons: Boolean = false) = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val client = ManagedHttpClient();
|
val client = ManagedHttpClient();
|
||||||
val latestVersion = downloadVersionCode(client);
|
val latestVersion = downloadVersionCode(client);
|
||||||
@@ -167,7 +167,7 @@ class StateUpdate {
|
|||||||
if (latestVersion > currentVersion) {
|
if (latestVersion > currentVersion) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
UIDialogs.showUpdateAvailableDialog(context, latestVersion);
|
UIDialogs.showUpdateAvailableDialog(context, latestVersion, hideExceptionButtons);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
UIDialogs.toast(context, "Failed to show update dialog");
|
UIDialogs.toast(context, "Failed to show update dialog");
|
||||||
Logger.w(TAG, "Error occurred in update dialog.");
|
Logger.w(TAG, "Error occurred in update dialog.");
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.futo.platformplayer.states.StateApp
|
|||||||
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
|
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
|
||||||
import com.futo.platformplayer.stores.v2.StoreSerializer
|
import com.futo.platformplayer.stores.v2.StoreSerializer
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
|
import java.lang.IllegalArgumentException
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.ConcurrentMap
|
import java.util.concurrent.ConcurrentMap
|
||||||
@@ -209,7 +210,9 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
|||||||
|
|
||||||
fun getObject(id: Long) = get(id).obj!!;
|
fun getObject(id: Long) = get(id).obj!!;
|
||||||
fun get(id: Long): I {
|
fun get(id: Long): I {
|
||||||
return deserializeIndex(dbDaoBase.get(_sqlGet(id)));
|
val result = dbDaoBase.getNullable(_sqlGet(id))
|
||||||
|
?: throw IllegalArgumentException("DB [${name}] has no entry with id ${id}");
|
||||||
|
return deserializeIndex(result);
|
||||||
}
|
}
|
||||||
fun getOrNull(id: Long): I? {
|
fun getOrNull(id: Long): I? {
|
||||||
val result = dbDaoBase.getNullable(_sqlGet(id));
|
val result = dbDaoBase.getNullable(_sqlGet(id));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.stores.v2
|
|||||||
|
|
||||||
import com.futo.platformplayer.assume
|
import com.futo.platformplayer.assume
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -105,7 +106,7 @@ class ManagedStore<T>{
|
|||||||
_toReconstruct.clear();
|
_toReconstruct.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
suspend fun importReconstructions(items: List<String>, onProgress: ((Int, Int)->Unit)? = null): ReconstructionResult {
|
suspend fun importReconstructions(items: List<String>, cache: ImportCache? = null, onProgress: ((Int, Int)->Unit)? = null): ReconstructionResult {
|
||||||
var successes = 0;
|
var successes = 0;
|
||||||
val exs = ArrayList<Throwable>();
|
val exs = ArrayList<Throwable>();
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ class ManagedStore<T>{
|
|||||||
for (i in 0 .. 1) {
|
for (i in 0 .. 1) {
|
||||||
try {
|
try {
|
||||||
Logger.i(TAG, "Importing ${logName(recon)}");
|
Logger.i(TAG, "Importing ${logName(recon)}");
|
||||||
val reconId = createFromReconstruction(recon, builder);
|
val reconId = createFromReconstruction(recon, builder, cache);
|
||||||
successes++;
|
successes++;
|
||||||
Logger.i(TAG, "Imported ${logName(reconId)}");
|
Logger.i(TAG, "Imported ${logName(reconId)}");
|
||||||
break;
|
break;
|
||||||
@@ -272,12 +273,12 @@ class ManagedStore<T>{
|
|||||||
save(obj, withReconstruction, onlyExisting);
|
save(obj, withReconstruction, onlyExisting);
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createFromReconstruction(reconstruction: String, builder: ReconstructStore.Builder): String {
|
suspend fun createFromReconstruction(reconstruction: String, builder: ReconstructStore.Builder, cache: ImportCache? = null): String {
|
||||||
if(_reconstructStore == null)
|
if(_reconstructStore == null)
|
||||||
throw IllegalStateException("Can't reconstruct as no reconstruction is implemented for this type");
|
throw IllegalStateException("Can't reconstruct as no reconstruction is implemented for this type");
|
||||||
|
|
||||||
val id = UUID.randomUUID().toString();
|
val id = UUID.randomUUID().toString();
|
||||||
val reconstruct = _reconstructStore!!.toObjectWithHeader(id, reconstruction, builder);
|
val reconstruct = _reconstructStore!!.toObjectWithHeader(id, reconstruction, builder, cache);
|
||||||
save(reconstruct);
|
save(reconstruct);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.futo.platformplayer.stores.v2
|
package com.futo.platformplayer.stores.v2
|
||||||
|
|
||||||
|
import com.futo.platformplayer.models.ImportCache
|
||||||
|
|
||||||
abstract class ReconstructStore<T> {
|
abstract class ReconstructStore<T> {
|
||||||
open val backupOnSave: Boolean = false;
|
open val backupOnSave: Boolean = false;
|
||||||
open val backupOnCreate: Boolean = true;
|
open val backupOnCreate: Boolean = true;
|
||||||
@@ -11,18 +13,18 @@ abstract class ReconstructStore<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract fun toReconstruction(obj: T): String;
|
abstract fun toReconstruction(obj: T): String;
|
||||||
abstract suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder): T;
|
abstract suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache? = null): T;
|
||||||
|
|
||||||
fun toReconstructionWithHeader(obj: T, fallbackName: String): String {
|
fun toReconstructionWithHeader(obj: T, fallbackName: String): String {
|
||||||
val identifier = identifierName ?: fallbackName;
|
val identifier = identifierName ?: fallbackName;
|
||||||
return "@/${identifier}\n${toReconstruction(obj)}";
|
return "@/${identifier}\n${toReconstruction(obj)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun toObjectWithHeader(id: String, backup: String, builder: Builder): T {
|
suspend fun toObjectWithHeader(id: String, backup: String, builder: Builder, importCache: ImportCache? = null): T {
|
||||||
if(backup.startsWith("@/") && backup.contains("\n"))
|
if(backup.startsWith("@/") && backup.contains("\n"))
|
||||||
return toObject(id, backup.substring(backup.indexOf("\n") + 1), builder);
|
return toObject(id, backup.substring(backup.indexOf("\n") + 1), builder, importCache);
|
||||||
else
|
else
|
||||||
return toObject(id, backup, builder);
|
return toObject(id, backup, builder, importCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+21
-4
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.subscription
|
package com.futo.platformplayer.subscription
|
||||||
|
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.getNowDiffHours
|
import com.futo.platformplayer.getNowDiffHours
|
||||||
@@ -7,6 +8,7 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.ForkJoinPool
|
import java.util.concurrent.ForkJoinPool
|
||||||
|
|
||||||
class SmartSubscriptionAlgorithm(
|
class SmartSubscriptionAlgorithm(
|
||||||
@@ -70,18 +72,30 @@ class SmartSubscriptionAlgorithm(
|
|||||||
} else {
|
} else {
|
||||||
val fetchTasks = mutableListOf<SubscriptionTask>();
|
val fetchTasks = mutableListOf<SubscriptionTask>();
|
||||||
val cacheTasks = mutableListOf<SubscriptionTask>();
|
val cacheTasks = mutableListOf<SubscriptionTask>();
|
||||||
|
var peekTasks = mutableListOf<SubscriptionTask>();
|
||||||
|
|
||||||
for(task in clientTasks.second) {
|
for(task in clientTasks.second) {
|
||||||
if (!task.fromCache && fetchTasks.size < limit) {
|
if (!task.fromCache && fetchTasks.size < limit) {
|
||||||
fetchTasks.add(task);
|
fetchTasks.add(task);
|
||||||
} else {
|
} else {
|
||||||
task.fromCache = true;
|
if(peekTasks.size < 100 &&
|
||||||
cacheTasks.add(task);
|
Settings.instance.subscriptions.peekChannelContents &&
|
||||||
|
(task.sub.lastPeekVideo.year < 1971 || task.sub.lastPeekVideo < task.sub.lastVideoUpdate) &&
|
||||||
|
task.client.capabilities.hasPeekChannelContents &&
|
||||||
|
task.client.getPeekChannelTypes().contains(task.type)) {
|
||||||
|
task.fromPeek = true;
|
||||||
|
task.fromCache = true;
|
||||||
|
peekTasks.add(task);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
task.fromCache = true;
|
||||||
|
cacheTasks.add(task);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Logger.i(TAG, "Subscription Client Budget [${clientTasks.first.name}]: ${fetchTasks.size}/${limit}")
|
Logger.i(TAG, "Subscription Client Budget [${clientTasks.first.name}]: ${fetchTasks.size}/${limit}")
|
||||||
|
|
||||||
finalTasks.addAll(fetchTasks + cacheTasks);
|
finalTasks.addAll(fetchTasks + peekTasks + cacheTasks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +129,9 @@ class SmartSubscriptionAlgorithm(
|
|||||||
val lastUpdateHoursAgo = lastUpdate.getNowDiffHours();
|
val lastUpdateHoursAgo = lastUpdate.getNowDiffHours();
|
||||||
val expectedHours = (interval * 24) - lastUpdateHoursAgo.toDouble();
|
val expectedHours = (interval * 24) - lastUpdateHoursAgo.toDouble();
|
||||||
|
|
||||||
return (expectedHours * 100).toInt();
|
if((type == ResultCapabilities.TYPE_MIXED || type == ResultCapabilities.TYPE_VIDEOS) && (sub.lastPeekVideo.year > 1970 && sub.lastPeekVideo > sub.lastVideoUpdate))
|
||||||
|
return 0;
|
||||||
|
else
|
||||||
|
return (expectedHours * 100).toInt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+31
-5
@@ -24,6 +24,7 @@ import com.futo.platformplayer.states.StateCache
|
|||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
import java.util.concurrent.ForkJoinPool
|
import java.util.concurrent.ForkJoinPool
|
||||||
import java.util.concurrent.ForkJoinTask
|
import java.util.concurrent.ForkJoinTask
|
||||||
@@ -48,15 +49,17 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
val tasksGrouped = tasks.groupBy { it.client }
|
val tasksGrouped = tasks.groupBy { it.client }
|
||||||
|
|
||||||
Logger.i(TAG, "Starting Subscriptions Fetch:\n" +
|
Logger.i(TAG, "Starting Subscriptions Fetch:\n" +
|
||||||
tasksGrouped.map { " ${it.key.name}: ${it.value.count { !it.fromCache }}, Cached(${it.value.count { it.fromCache } })" }.joinToString("\n"));
|
tasksGrouped.map { " ${it.key.name}: ${it.value.count { !it.fromCache }}, Cached(${it.value.count { it.fromCache } - it.value.count { it.fromPeek && it.fromCache }}), Peek(${it.value.count { it.fromPeek }})" }.joinToString("\n"));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for(clientTasks in tasksGrouped) {
|
for(clientTasks in tasksGrouped) {
|
||||||
val clientTaskCount = clientTasks.value.filter { !it.fromCache }.size;
|
val clientTaskCount = clientTasks.value.count { !it.fromCache };
|
||||||
val clientCacheCount = clientTasks.value.size - clientTaskCount;
|
val clientCacheCount = clientTasks.value.count { it.fromCache && !it.fromPeek };
|
||||||
|
val clientPeekCount = clientTasks.value.count { it.fromPeek };
|
||||||
val limit = clientTasks.key.getSubscriptionRateLimit();
|
val limit = clientTasks.key.getSubscriptionRateLimit();
|
||||||
if(clientCacheCount > 0 && clientTaskCount > 0 && limit != null && clientTaskCount >= limit && StateApp.instance.contextOrNull?.let { it is MainActivity && it.isFragmentActive<SubscriptionsFeedFragment>() } == true) {
|
if(clientCacheCount > 0 && clientTaskCount > 0 && limit != null && clientTaskCount >= limit && StateApp.instance.contextOrNull?.let { it is MainActivity && it.isFragmentActive<SubscriptionsFeedFragment>() } == true) {
|
||||||
UIDialogs.appToast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)");
|
UIDialogs.appToast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). " +
|
||||||
|
"(${if(clientPeekCount > 0) "${clientPeekCount} peek, " else ""}${clientCacheCount} cached)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,8 +138,30 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
|
|
||||||
for(task in tasks) {
|
for(task in tasks) {
|
||||||
val forkTask = threadPool.submit<SubscriptionTaskResult> {
|
val forkTask = threadPool.submit<SubscriptionTaskResult> {
|
||||||
|
if(task.fromPeek) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
val time = measureTimeMillis {
|
||||||
|
val peekResults = StatePlatform.instance.peekChannelContents(task.client, task.url, task.type);
|
||||||
|
val mostRecent = peekResults.firstOrNull();
|
||||||
|
task.sub.lastPeekVideo = mostRecent?.datetime ?: OffsetDateTime.MIN;
|
||||||
|
task.sub.saveAsync();
|
||||||
|
val cacheItems = peekResults.filter { it.datetime != null && it.datetime!! > task.sub.lastVideoUpdate };
|
||||||
|
//Fix for current situation
|
||||||
|
for(item in cacheItems) {
|
||||||
|
if(item.author.thumbnail.isNullOrEmpty())
|
||||||
|
item.author.thumbnail = task.sub.channel.thumbnail;
|
||||||
|
}
|
||||||
|
StateCache.instance.cacheContents(cacheItems, false);
|
||||||
|
}
|
||||||
|
Logger.i("StateSubscriptions", "Subscription peek [${task.sub.channel.name}]:${task.type} results in ${time}ms");
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(StateSubscriptions.TAG, "Subscription peek [${task.sub.channel.name}] failed", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
synchronized(cachedChannels) {
|
synchronized(cachedChannels) {
|
||||||
if(task.fromCache) {
|
if(task.fromCache || task.fromPeek) {
|
||||||
finished++;
|
finished++;
|
||||||
onProgress.emit(finished, forkTasks.size);
|
onProgress.emit(finished, forkTasks.size);
|
||||||
if(cachedChannels.contains(task.url)) {
|
if(cachedChannels.contains(task.url)) {
|
||||||
@@ -218,6 +243,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
val url: String,
|
val url: String,
|
||||||
val type: String,
|
val type: String,
|
||||||
var fromCache: Boolean = false,
|
var fromCache: Boolean = false,
|
||||||
|
var fromPeek: Boolean = false,
|
||||||
var urgency: Int = 0
|
var urgency: Int = 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import android.view.View
|
|||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.HorizontalSpaceItemDecoration
|
import com.futo.platformplayer.HorizontalSpaceItemDecoration
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
@@ -61,6 +63,7 @@ class MonetizationView : LinearLayout {
|
|||||||
|
|
||||||
val onSupportTap = Event0();
|
val onSupportTap = Event0();
|
||||||
val onStoreTap = Event0();
|
val onStoreTap = Event0();
|
||||||
|
val onUrlTap = Event1<String>();
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||||
inflate(context, R.layout.view_monetization, this);
|
inflate(context, R.layout.view_monetization, this);
|
||||||
@@ -70,10 +73,12 @@ class MonetizationView : LinearLayout {
|
|||||||
_membershipPlatform = findViewById(R.id.membership_platform);
|
_membershipPlatform = findViewById(R.id.membership_platform);
|
||||||
_buttonMembership.setOnClickListener {
|
_buttonMembership.setOnClickListener {
|
||||||
_membershipUrl?.let {
|
_membershipUrl?.let {
|
||||||
|
/*
|
||||||
val uri = Uri.parse(it);
|
val uri = Uri.parse(it);
|
||||||
val intent = Intent(Intent.ACTION_VIEW);
|
val intent = Intent(Intent.ACTION_VIEW);
|
||||||
intent.data = uri;
|
intent.data = uri;
|
||||||
context.startActivity(intent);
|
context.startActivity(intent);*/
|
||||||
|
onUrlTap.emit(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,9 +134,18 @@ class MonetizationView : LinearLayout {
|
|||||||
_buttonStore.visibility = View.GONE;
|
_buttonStore.visibility = View.GONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(profile.systemState.donationDestinations.isNotEmpty() ||
|
||||||
|
profile.systemState.membershipUrls.isNotEmpty() ||
|
||||||
|
profile.systemState.store.isNotEmpty() ||
|
||||||
|
profile.systemState.promotion.isNotEmpty())
|
||||||
|
_buttonSupport.isVisible = true;
|
||||||
|
else
|
||||||
|
_buttonSupport.isVisible = false;
|
||||||
|
|
||||||
_root.visibility = View.VISIBLE;
|
_root.visibility = View.VISIBLE;
|
||||||
} else {
|
} else {
|
||||||
_root.visibility = View.GONE;
|
_root.visibility = View.GONE;
|
||||||
|
_buttonSupport.isVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMerchandise(null);
|
setMerchandise(null);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import android.view.View
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.size
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
@@ -33,6 +35,13 @@ class SupportView : LinearLayout {
|
|||||||
private var _textNoSupportOptionsSet: TextView
|
private var _textNoSupportOptionsSet: TextView
|
||||||
private var _polycentricProfile: PolycentricProfile? = null
|
private var _polycentricProfile: PolycentricProfile? = null
|
||||||
|
|
||||||
|
val hasSupportItems: Boolean get() {
|
||||||
|
return (_layoutPromotions.isVisible && _buttonPromotion.isVisible) ||
|
||||||
|
(_layoutMemberships.isVisible && _layoutMembershipEntries.isVisible && _layoutMembershipEntries.size > 0) ||
|
||||||
|
(_layoutDonation.isVisible && _layoutDonationEntries.isVisible && _layoutDonationEntries.size > 0) ||
|
||||||
|
_buttonStore.isVisible;
|
||||||
|
};
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||||
inflate(context, R.layout.view_support, this);
|
inflate(context, R.layout.view_support, this);
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
|
|||||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||||
val onAddToClicked = Event1<IPlatformContent>();
|
val onAddToClicked = Event1<IPlatformContent>();
|
||||||
val onAddToQueueClicked = Event1<IPlatformContent>();
|
val onAddToQueueClicked = Event1<IPlatformContent>();
|
||||||
|
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
|
||||||
val onLongPress = Event1<IPlatformContent>();
|
val onLongPress = Event1<IPlatformContent>();
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
@@ -56,6 +57,7 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
|
|||||||
onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit);
|
onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit);
|
||||||
onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit);
|
onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit);
|
||||||
onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit);
|
onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit);
|
||||||
|
onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit);
|
||||||
onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit);
|
onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit);
|
||||||
};
|
};
|
||||||
1 -> ChannelListFragment.newInstance().apply { onClickChannel.subscribe(onChannelClicked::emit) };
|
1 -> ChannelListFragment.newInstance().apply { onClickChannel.subscribe(onChannelClicked::emit) };
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class CommentViewHolder : ViewHolder {
|
|||||||
|
|
||||||
var onRepliesClick = Event1<IPlatformComment>();
|
var onRepliesClick = Event1<IPlatformComment>();
|
||||||
var onDelete = Event1<IPlatformComment>();
|
var onDelete = Event1<IPlatformComment>();
|
||||||
|
var onAuthorClick = Event1<IPlatformComment>();
|
||||||
var comment: IPlatformComment? = null
|
var comment: IPlatformComment? = null
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
@@ -95,6 +96,19 @@ class CommentViewHolder : ViewHolder {
|
|||||||
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
|
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_creatorThumbnail.onClick.subscribe {
|
||||||
|
val c = comment ?: return@subscribe;
|
||||||
|
onAuthorClick.emit(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
_creatorThumbnail.setOnClickListener {
|
||||||
|
val c = comment ?: return@setOnClickListener;
|
||||||
|
onAuthorClick.emit(c);
|
||||||
|
}
|
||||||
|
_textAuthor.setOnClickListener {
|
||||||
|
val c = comment ?: return@setOnClickListener;
|
||||||
|
onAuthorClick.emit(c);
|
||||||
|
}
|
||||||
_buttonReplies.onClick.subscribe {
|
_buttonReplies.onClick.subscribe {
|
||||||
val c = comment ?: return@subscribe;
|
val c = comment ?: return@subscribe;
|
||||||
onRepliesClick.emit(c);
|
onRepliesClick.emit(c);
|
||||||
|
|||||||
+12
-3
@@ -53,9 +53,10 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
|||||||
hideLikesDislikesReplies()
|
hideLikesDislikesReplies()
|
||||||
}
|
}
|
||||||
|
|
||||||
var onRepliesClick = Event1<IPlatformComment>();
|
val onRepliesClick = Event1<IPlatformComment>();
|
||||||
var onDelete = Event1<IPlatformComment>();
|
val onDelete = Event1<IPlatformComment>();
|
||||||
var onClick = Event1<IPlatformComment>();
|
val onClick = Event1<IPlatformComment>();
|
||||||
|
val onAuthorClick = Event1<IPlatformComment>();
|
||||||
var comment: IPlatformComment? = null
|
var comment: IPlatformComment? = null
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
@@ -99,6 +100,14 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
|||||||
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
|
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_creatorThumbnail.onClick.subscribe {
|
||||||
|
val c = comment ?: return@subscribe;
|
||||||
|
onAuthorClick.emit(c);
|
||||||
|
}
|
||||||
|
_textAuthor.setOnClickListener {
|
||||||
|
val c = comment ?: return@setOnClickListener;
|
||||||
|
onAuthorClick.emit(c);
|
||||||
|
}
|
||||||
_buttonReplies.onClick.subscribe {
|
_buttonReplies.onClick.subscribe {
|
||||||
val c = comment ?: return@subscribe;
|
val c = comment ?: return@subscribe;
|
||||||
onRepliesClick.emit(c);
|
onRepliesClick.emit(c);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.futo.platformplayer.api.media.platforms.js.JSClient
|
|||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
|
|
||||||
class DisabledSourceView : LinearLayout {
|
class DisabledSourceView : LinearLayout {
|
||||||
private val _root: LinearLayout;
|
private val _root: LinearLayout;
|
||||||
@@ -37,7 +38,7 @@ class DisabledSourceView : LinearLayout {
|
|||||||
|
|
||||||
_textSource.text = client.name;
|
_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.text = context.getString(R.string.update_available_exclamation)
|
||||||
_textSourceSubtitle.setTextColor(context.getColor(R.color.light_blue_400))
|
_textSourceSubtitle.setTextColor(context.getColor(R.color.light_blue_400))
|
||||||
_textSourceSubtitle.typeface = resources.getFont(R.font.inter_regular)
|
_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.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
|
|
||||||
class EnabledSourceViewHolder : ViewHolder {
|
class EnabledSourceViewHolder : ViewHolder {
|
||||||
private val _imageSource: ImageView;
|
private val _imageSource: ImageView;
|
||||||
@@ -61,7 +62,7 @@ class EnabledSourceViewHolder : ViewHolder {
|
|||||||
|
|
||||||
_textSource.text = client.name
|
_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.text = itemView.context.getString(R.string.update_available_exclamation)
|
||||||
_textSourceSubtitle.setTextColor(itemView.context.getColor(R.color.light_blue_400))
|
_textSourceSubtitle.setTextColor(itemView.context.getColor(R.color.light_blue_400))
|
||||||
_textSourceSubtitle.typeface = itemView.resources.getFont(R.font.inter_regular)
|
_textSourceSubtitle.typeface = itemView.resources.getFont(R.font.inter_regular)
|
||||||
|
|||||||
+4
@@ -39,6 +39,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
|||||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||||
val onAddToClicked = Event1<IPlatformContent>();
|
val onAddToClicked = Event1<IPlatformContent>();
|
||||||
val onAddToQueueClicked = Event1<IPlatformContent>();
|
val onAddToQueueClicked = Event1<IPlatformContent>();
|
||||||
|
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
|
||||||
val onLongPress = Event1<IPlatformContent>();
|
val onLongPress = Event1<IPlatformContent>();
|
||||||
|
|
||||||
private var _taskLoadContent = TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>(
|
private var _taskLoadContent = TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>(
|
||||||
@@ -95,6 +96,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
|||||||
this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit);
|
this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit);
|
||||||
this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit);
|
this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit);
|
||||||
this.onAddToQueueClicked.subscribe(this@PreviewContentListAdapter.onAddToQueueClicked::emit);
|
this.onAddToQueueClicked.subscribe(this@PreviewContentListAdapter.onAddToQueueClicked::emit);
|
||||||
|
this.onAddToWatchLaterClicked.subscribe(this@PreviewContentListAdapter.onAddToWatchLaterClicked::emit);
|
||||||
};
|
};
|
||||||
private fun createLockedViewHolder(viewGroup: ViewGroup): PreviewLockedViewHolder = PreviewLockedViewHolder(viewGroup, _feedStyle).apply {
|
private fun createLockedViewHolder(viewGroup: ViewGroup): PreviewLockedViewHolder = PreviewLockedViewHolder(viewGroup, _feedStyle).apply {
|
||||||
this.onLockedUrlClicked.subscribe(this@PreviewContentListAdapter.onUrlClicked::emit);
|
this.onLockedUrlClicked.subscribe(this@PreviewContentListAdapter.onUrlClicked::emit);
|
||||||
@@ -106,6 +108,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
|||||||
this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit);
|
this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit);
|
||||||
this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit);
|
this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit);
|
||||||
this.onAddToQueueClicked.subscribe(this@PreviewContentListAdapter.onAddToQueueClicked::emit);
|
this.onAddToQueueClicked.subscribe(this@PreviewContentListAdapter.onAddToQueueClicked::emit);
|
||||||
|
this.onAddToWatchLaterClicked.subscribe(this@PreviewContentListAdapter.onAddToWatchLaterClicked::emit);
|
||||||
this.onLongPress.subscribe(this@PreviewContentListAdapter.onLongPress::emit);
|
this.onLongPress.subscribe(this@PreviewContentListAdapter.onLongPress::emit);
|
||||||
};
|
};
|
||||||
private fun createPlaylistViewHolder(viewGroup: ViewGroup): PreviewPlaylistViewHolder = PreviewPlaylistViewHolder(viewGroup, _feedStyle).apply {
|
private fun createPlaylistViewHolder(viewGroup: ViewGroup): PreviewPlaylistViewHolder = PreviewPlaylistViewHolder(viewGroup, _feedStyle).apply {
|
||||||
@@ -161,6 +164,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
|||||||
onChannelClicked.clear();
|
onChannelClicked.clear();
|
||||||
onAddToClicked.clear();
|
onAddToClicked.clear();
|
||||||
onAddToQueueClicked.clear();
|
onAddToQueueClicked.clear();
|
||||||
|
onAddToWatchLaterClicked.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun previewContentDetails(viewHolder: ContentPreviewViewHolder, videoDetails: IPlatformContentDetails?) {
|
private fun previewContentDetails(viewHolder: ContentPreviewViewHolder, videoDetails: IPlatformContentDetails?) {
|
||||||
|
|||||||
+2
@@ -19,6 +19,7 @@ class PreviewNestedVideoViewHolder : ContentPreviewViewHolder {
|
|||||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||||
val onAddToClicked = Event1<IPlatformVideo>();
|
val onAddToClicked = Event1<IPlatformVideo>();
|
||||||
val onAddToQueueClicked = Event1<IPlatformVideo>();
|
val onAddToQueueClicked = Event1<IPlatformVideo>();
|
||||||
|
val onAddToWatchLaterClicked = Event1<IPlatformVideo>();
|
||||||
|
|
||||||
override val content: IPlatformContent? get() = view.content;
|
override val content: IPlatformContent? get() = view.content;
|
||||||
private val view: PreviewNestedVideoView get() = itemView as PreviewNestedVideoView;
|
private val view: PreviewNestedVideoView get() = itemView as PreviewNestedVideoView;
|
||||||
@@ -31,6 +32,7 @@ class PreviewNestedVideoViewHolder : ContentPreviewViewHolder {
|
|||||||
view.onChannelClicked.subscribe(onChannelClicked::emit);
|
view.onChannelClicked.subscribe(onChannelClicked::emit);
|
||||||
view.onAddToClicked.subscribe(onAddToClicked::emit);
|
view.onAddToClicked.subscribe(onAddToClicked::emit);
|
||||||
view.onAddToQueueClicked.subscribe(onAddToQueueClicked::emit);
|
view.onAddToQueueClicked.subscribe(onAddToQueueClicked::emit);
|
||||||
|
view.onAddToWatchLaterClicked.subscribe(onAddToWatchLaterClicked::emit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user