mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 6d9c1e17b5 | |||
| 507ad105c0 | |||
| 40a283017e | |||
| be14597670 | |||
| 837609abb9 | |||
| d64cd98b43 | |||
| 0081ff1483 | |||
| f78ca6c7ed | |||
| cfc7cbcaa4 | |||
| e533eb7778 | |||
| 7c1d0a7f88 | |||
| 01ef471708 | |||
| 2fd0a9a41d | |||
| 635749dfe4 | |||
| c4bd5626f3 | |||
| 568a0f6329 | |||
| 7ee67b5cd0 | |||
| fc94c6903c | |||
| a0af8805e7 | |||
| 9b64cde17d | |||
| f6931bcf8c | |||
| a4ff47d863 | |||
| 982d251126 | |||
| 8820a0ecc0 | |||
| b99a713ffc | |||
| dfc8c4b740 | |||
| c3df9e5259 | |||
| b9c7e0a8ca | |||
| 2c7f02a24d | |||
| 5cc8488d94 | |||
| 6f7304f59c | |||
| ea4fea4401 | |||
| 9b48664de4 | |||
| 8964dc68f0 | |||
| 4711b8055b | |||
| 84e3373fa7 | |||
| fdd7e32dd8 | |||
| e57119ebbd | |||
| ed29dd8365 | |||
| 196cacb452 | |||
| c025913fc8 | |||
| 48b2c68e72 | |||
| 689766a6ac | |||
| 9306024d17 | |||
| 195163840b | |||
| 788c54bf8f | |||
| 031aabd523 | |||
| 85db4cc4e6 | |||
| 745aad385b | |||
| ba87261f9f | |||
| 7d091382c0 | |||
| 781d0797e7 |
+6
-3
@@ -1,9 +1,6 @@
|
|||||||
[submodule "dep/polycentricandroid"]
|
[submodule "dep/polycentricandroid"]
|
||||||
path = dep/polycentricandroid
|
path = dep/polycentricandroid
|
||||||
url = ../polycentricandroid.git
|
url = ../polycentricandroid.git
|
||||||
[submodule "app/src/playstore/assets/sources/peertube"]
|
|
||||||
path = app/src/playstore/assets/sources/peertube
|
|
||||||
url = ../plugins/peertube.git
|
|
||||||
[submodule "app/src/stable/assets/sources/kick"]
|
[submodule "app/src/stable/assets/sources/kick"]
|
||||||
path = app/src/stable/assets/sources/kick
|
path = app/src/stable/assets/sources/kick
|
||||||
url = ../plugins/kick.git
|
url = ../plugins/kick.git
|
||||||
@@ -61,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
|
||||||
|
|||||||
+10
-10
@@ -151,7 +151,7 @@ dependencies {
|
|||||||
//Core
|
//Core
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'com.google.android.material:material:1.10.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
|
||||||
//Images
|
//Images
|
||||||
@@ -172,15 +172,15 @@ dependencies {
|
|||||||
implementation("com.caoccao.javet:javet-android:3.0.2")
|
implementation("com.caoccao.javet:javet-android:3.0.2")
|
||||||
|
|
||||||
//Exoplayer
|
//Exoplayer
|
||||||
implementation 'androidx.media3:media3-exoplayer:1.2.0'
|
implementation 'androidx.media3:media3-exoplayer:1.2.1'
|
||||||
implementation 'androidx.media3:media3-exoplayer-dash:1.2.0'
|
implementation 'androidx.media3:media3-exoplayer-dash:1.2.1'
|
||||||
implementation 'androidx.media3:media3-ui:1.2.0'
|
implementation 'androidx.media3:media3-ui:1.2.1'
|
||||||
implementation 'androidx.media3:media3-exoplayer-hls:1.2.0'
|
implementation 'androidx.media3:media3-exoplayer-hls:1.2.1'
|
||||||
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.0'
|
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.1'
|
||||||
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.0'
|
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.1'
|
||||||
implementation 'androidx.media3:media3-transformer:1.2.0'
|
implementation 'androidx.media3:media3-transformer:1.2.1'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.5'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
|
||||||
implementation 'androidx.media:media:1.7.0'
|
implementation 'androidx.media:media:1.7.0'
|
||||||
|
|
||||||
//Other
|
//Other
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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", {
|
||||||
|
|||||||
@@ -385,8 +385,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 +402,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 +421,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>
|
||||||
@@ -535,6 +543,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 +554,7 @@
|
|||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
|
searchTestMethods: "",
|
||||||
page: "Plugin",
|
page: "Plugin",
|
||||||
pastPluginUrls: [],
|
pastPluginUrls: [],
|
||||||
settings: {},
|
settings: {},
|
||||||
@@ -570,6 +580,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: "",
|
||||||
@@ -860,6 +873,53 @@
|
|||||||
"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) {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -809,7 +814,36 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var polycentricEnabled: Boolean = true;
|
var polycentricEnabled: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.info, FieldForm.GROUP, -1, 19)
|
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
|
||||||
|
var gestureControls = GestureControls();
|
||||||
|
@Serializable
|
||||||
|
class GestureControls {
|
||||||
|
@FormField(R.string.volume_slider, FieldForm.TOGGLE, R.string.volume_slider_descr, 1)
|
||||||
|
var volumeSlider: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.brightness_slider, FieldForm.TOGGLE, R.string.brightness_slider_descr, 2)
|
||||||
|
var brightnessSlider: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.toggle_full_screen, FieldForm.TOGGLE, R.string.toggle_full_screen_descr, 3)
|
||||||
|
var toggleFullscreen: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.system_brightness, FieldForm.TOGGLE, R.string.system_brightness_descr, 4)
|
||||||
|
var useSystemBrightness: Boolean = false;
|
||||||
|
|
||||||
|
@FormField(R.string.system_volume, FieldForm.TOGGLE, R.string.system_volume_descr, 5)
|
||||||
|
var useSystemVolume: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.restore_system_brightness, FieldForm.TOGGLE, R.string.restore_system_brightness_descr, 6)
|
||||||
|
var restoreSystemBrightness: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.zoom_option, FieldForm.TOGGLE, R.string.zoom_option_descr, 7)
|
||||||
|
var zoom: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.pan_option, FieldForm.TOGGLE, R.string.pan_option_descr, 8)
|
||||||
|
var pan: Boolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.info, FieldForm.GROUP, -1, 20)
|
||||||
var info = Info();
|
var info = Info();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Info {
|
class Info {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import com.futo.platformplayer.dialogs.MigrateDialog
|
|||||||
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.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.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
@@ -304,12 +305,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 +344,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();
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
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
|
||||||
@@ -38,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
|
||||||
@@ -68,7 +74,7 @@ class UISlideOverlays {
|
|||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) {
|
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
|
|
||||||
val originalNotif = subscription.doNotifications;
|
val originalNotif = subscription.doNotifications;
|
||||||
@@ -77,20 +83,48 @@ class UISlideOverlays {
|
|||||||
val originalVideo = subscription.doFetchVideos;
|
val originalVideo = subscription.doFetchVideos;
|
||||||
val originalPosts = subscription.doFetchPosts;
|
val originalPosts = subscription.doFetchPosts;
|
||||||
|
|
||||||
|
val menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, listOf());
|
||||||
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
|
||||||
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
|
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
|
||||||
val capabilities = plugin.getChannelCapabilities();
|
val capabilities = plugin.getChannelCapabilities();
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
|
||||||
var menu: SlideUpMenuOverlay? = null;
|
|
||||||
|
|
||||||
|
|
||||||
items.addAll(listOf(
|
items.addAll(listOf(
|
||||||
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()),
|
||||||
@@ -119,7 +153,7 @@ class UISlideOverlays {
|
|||||||
}, false)*/
|
}, false)*/
|
||||||
).filterNotNull());
|
).filterNotNull());
|
||||||
|
|
||||||
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
|
menu.setItems(items);
|
||||||
|
|
||||||
if(subscription.doNotifications)
|
if(subscription.doNotifications)
|
||||||
menu.selectOption(null, "notifications", true, true);
|
menu.selectOption(null, "notifications", true, true);
|
||||||
@@ -174,6 +208,8 @@ class UISlideOverlays {
|
|||||||
menu.show();
|
menu.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {
|
fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {
|
||||||
@@ -474,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;
|
||||||
@@ -647,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");
|
||||||
@@ -687,7 +736,7 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showAddToOverlay(video: IPlatformVideo, container: ViewGroup): SlideUpMenuOverlay {
|
fun showAddToOverlay(video: IPlatformVideo, container: ViewGroup, slideUpMenuOverlayUpdated: (SlideUpMenuOverlay) -> Unit): SlideUpMenuOverlay {
|
||||||
|
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
|
|
||||||
@@ -718,6 +767,13 @@ class UISlideOverlays {
|
|||||||
);
|
);
|
||||||
|
|
||||||
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
val playlistItems = arrayListOf<SlideUpMenuItem>();
|
||||||
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
|
||||||
|
slideUpMenuOverlayUpdated(showCreatePlaylistOverlay(container) {
|
||||||
|
val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
|
||||||
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
|
});
|
||||||
|
}, false))
|
||||||
|
|
||||||
for (playlist in allPlaylists) {
|
for (playlist in allPlaylists) {
|
||||||
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
|
||||||
{
|
{
|
||||||
@@ -732,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import android.widget.ScrollView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
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.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
@@ -37,8 +38,10 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private lateinit var _sourceHeader: SourceHeaderView;
|
private lateinit var _sourceHeader: SourceHeaderView;
|
||||||
|
|
||||||
|
|
||||||
private lateinit var _sourcePermissions: LinearLayout;
|
private lateinit var _sourcePermissions: LinearLayout;
|
||||||
private lateinit var _sourceWarnings: LinearLayout;
|
private lateinit var _sourceWarnings: LinearLayout;
|
||||||
|
private lateinit var _sourceWarningsContainer: LinearLayout;
|
||||||
|
|
||||||
private lateinit var _container: ScrollView;
|
private lateinit var _container: ScrollView;
|
||||||
private lateinit var _loader: ImageView;
|
private lateinit var _loader: ImageView;
|
||||||
@@ -79,6 +82,7 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
_sourcePermissions = findViewById(R.id.source_permissions);
|
_sourcePermissions = findViewById(R.id.source_permissions);
|
||||||
_sourceWarnings = findViewById(R.id.source_warnings);
|
_sourceWarnings = findViewById(R.id.source_warnings);
|
||||||
|
_sourceWarningsContainer = findViewById(R.id.container_source_warnings);
|
||||||
|
|
||||||
_container = findViewById(R.id.configContainer);
|
_container = findViewById(R.id.configContainer);
|
||||||
_loader = findViewById(R.id.loader);
|
_loader = findViewById(R.id.loader);
|
||||||
@@ -203,21 +207,28 @@ class AddSourceActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val pastelRed = ContextCompat.getColor(this, R.color.pastel_red);
|
val pastelRed = ContextCompat.getColor(this, R.color.pastel_red);
|
||||||
|
|
||||||
for(warning in config.getWarnings(script))
|
val warnings = config.getWarnings(script);
|
||||||
|
for(warning in warnings)
|
||||||
_sourceWarnings.addView(
|
_sourceWarnings.addView(
|
||||||
SourceInfoView(this,
|
SourceInfoView(this,
|
||||||
R.drawable.ic_security_pred,
|
R.drawable.ic_security_pred,
|
||||||
warning.first,
|
warning.first,
|
||||||
warning.second)
|
warning.second)
|
||||||
.withDescriptionColor(pastelRed));
|
.withDescriptionColor(pastelRed));
|
||||||
|
_sourceWarningsContainer.isVisible = warnings.isNotEmpty();
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun install(config: SourcePluginConfig, script: String) {
|
fun install(config: SourcePluginConfig, script: String) {
|
||||||
|
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)
|
StatePlatform.instance.clearUpdateAvailable(config)
|
||||||
|
if(isNew)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
StatePlatform.instance.enableClient(listOf(config.id));
|
||||||
|
}
|
||||||
backToSources();
|
backToSources();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
lateinit var _buttonBack: ImageButton;
|
lateinit var _buttonBack: ImageButton;
|
||||||
|
|
||||||
lateinit var _buttonQR: BigButton;
|
lateinit var _buttonQR: BigButton;
|
||||||
|
lateinit var _buttonBrowse: BigButton;
|
||||||
lateinit var _buttonURL: BigButton;
|
lateinit var _buttonURL: BigButton;
|
||||||
lateinit var _buttonPlugins: BigButton;
|
lateinit var _buttonPlugins: BigButton;
|
||||||
|
|
||||||
@@ -56,6 +57,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
_buttonBack = findViewById(R.id.button_back);
|
_buttonBack = findViewById(R.id.button_back);
|
||||||
|
|
||||||
_buttonQR = findViewById(R.id.option_qr);
|
_buttonQR = findViewById(R.id.option_qr);
|
||||||
|
_buttonBrowse = findViewById(R.id.option_browse);
|
||||||
_buttonURL = findViewById(R.id.option_url);
|
_buttonURL = findViewById(R.id.option_url);
|
||||||
_buttonPlugins = findViewById(R.id.option_plugins);
|
_buttonPlugins = findViewById(R.id.option_plugins);
|
||||||
|
|
||||||
@@ -74,6 +76,9 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
||||||
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||||
}
|
}
|
||||||
|
_buttonBrowse.onClick.subscribe {
|
||||||
|
startActivity(MainActivity.getTabIntent(this, "BROWSE_PLUGINS"));
|
||||||
|
}
|
||||||
|
|
||||||
_buttonURL.onClick.subscribe {
|
_buttonURL.onClick.subscribe {
|
||||||
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
|
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
@@ -535,13 +543,28 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
navigate(_fragMainSources);
|
navigate(_fragMainSources);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
"BROWSE_PLUGINS" -> {
|
||||||
|
navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
|
||||||
|
Pair("grayjay") { req ->
|
||||||
|
StateApp.instance.contextOrNull?.let {
|
||||||
|
if(it is MainActivity) {
|
||||||
|
runBlocking {
|
||||||
|
it.handleUrlAll(req.url.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (targetData != null) {
|
if (targetData != null) {
|
||||||
handleUrlAll(targetData)
|
runBlocking {
|
||||||
|
handleUrlAll(targetData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
@@ -549,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" -> {
|
||||||
@@ -585,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",
|
||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
@@ -633,23 +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;
|
||||||
}
|
}
|
||||||
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)");
|
||||||
@@ -660,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") {
|
||||||
@@ -678,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")) {
|
||||||
@@ -695,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
|
||||||
@@ -712,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) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -802,11 +865,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if(_fragBotBarMenu.onBackPressed())
|
if(_fragBotBarMenu.onBackPressed())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED &&
|
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed())
|
||||||
_fragVideoDetail.onBackPressed())
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
||||||
if(!fragCurrent.onBackPressed())
|
if(!fragCurrent.onBackPressed())
|
||||||
closeSegment();
|
closeSegment();
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -12,14 +12,13 @@ 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.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
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.Synchronization
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -71,6 +70,13 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
try {
|
try {
|
||||||
processHandle = ProcessHandle.create();
|
processHandle = ProcessHandle.create();
|
||||||
Store.instance.addProcessSecret(processHandle.processSecret);
|
Store.instance.addProcessSecret(processHandle.processSecret);
|
||||||
|
|
||||||
|
try {
|
||||||
|
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
|
}
|
||||||
|
|
||||||
processHandle.addServer("https://srv1-stg.polycentric.io");
|
processHandle.addServer("https://srv1-stg.polycentric.io");
|
||||||
processHandle.setUsername(username);
|
processHandle.setUsername(username);
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
|
|||||||
+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(
|
||||||
|
|||||||
+5
-2
@@ -19,8 +19,9 @@ class PolycentricPlatformComment : IPlatformComment {
|
|||||||
|
|
||||||
val eventPointer: Pointer;
|
val eventPointer: Pointer;
|
||||||
val reference: Reference;
|
val reference: Reference;
|
||||||
|
val parentReference: Reference?;
|
||||||
|
|
||||||
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, replyCount: Int? = null) {
|
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, parentReference: Reference?, replyCount: Int? = null) {
|
||||||
this.contextUrl = contextUrl;
|
this.contextUrl = contextUrl;
|
||||||
this.author = author;
|
this.author = author;
|
||||||
this.message = msg;
|
this.message = msg;
|
||||||
@@ -29,6 +30,7 @@ class PolycentricPlatformComment : IPlatformComment {
|
|||||||
this.replyCount = replyCount;
|
this.replyCount = replyCount;
|
||||||
this.eventPointer = eventPointer;
|
this.eventPointer = eventPointer;
|
||||||
this.reference = eventPointer.toReference();
|
this.reference = eventPointer.toReference();
|
||||||
|
this.parentReference = parentReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
|
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
|
||||||
@@ -36,10 +38,11 @@ class PolycentricPlatformComment : IPlatformComment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
|
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
|
||||||
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, replyCount);
|
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, parentReference, replyCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val TAG = "PolycentricPlatformComment"
|
||||||
val MAX_COMMENT_SIZE = 2000
|
val MAX_COMMENT_SIZE = 2000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -58,6 +59,7 @@ import kotlinx.serialization.json.Json
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
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 +75,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 +94,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 +157,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 +182,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 +225,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 +273,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 +281,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 +311,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 +319,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 +335,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 +347,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 +367,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 +394,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 +494,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 +503,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 +514,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 +528,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 +546,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 +555,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 +568,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 +578,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 +690,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 +724,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 +773,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -45,7 +45,8 @@ 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
|
||||||
) : IV8PluginConfig {
|
) : IV8PluginConfig {
|
||||||
|
|
||||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||||
|
|||||||
+32
-2
@@ -2,9 +2,13 @@ package com.futo.platformplayer.api.media.platforms.js
|
|||||||
|
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
|
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
|
||||||
@@ -55,7 +59,16 @@ class SourcePluginDescriptor {
|
|||||||
onCaptchaChanged.emit();
|
onCaptchaChanged.emit();
|
||||||
}
|
}
|
||||||
fun getCaptchaData(): SourceCaptchaData? {
|
fun getCaptchaData(): SourceCaptchaData? {
|
||||||
return SourceCaptchaData.fromEncrypted(captchaEncrypted);
|
try {
|
||||||
|
return SourceCaptchaData.fromEncrypted(captchaEncrypted);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("SourcePluginDescriptor", "Captcha decode failed, disabling auth.", ex);
|
||||||
|
StateAnnouncement.instance.registerAnnouncement("CAP_BROKEN_" + config.id,
|
||||||
|
"Captcha corrupted for plugin [${config.name}]",
|
||||||
|
"Something went wrong in the stored captcha, you'll have to login again", AnnouncementType.SESSION);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateAuth(str: SourceAuth?) {
|
fun updateAuth(str: SourceAuth?) {
|
||||||
@@ -63,12 +76,24 @@ class SourcePluginDescriptor {
|
|||||||
onAuthChanged.emit();
|
onAuthChanged.emit();
|
||||||
}
|
}
|
||||||
fun getAuth(): SourceAuth? {
|
fun getAuth(): SourceAuth? {
|
||||||
return SourceAuth.fromEncrypted(authEncrypted);
|
try {
|
||||||
|
return SourceAuth.fromEncrypted(authEncrypted);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("SourcePluginDescriptor", "Authentication decode failed, disabling auth.", ex);
|
||||||
|
StateAnnouncement.instance.registerAnnouncement("AUTH_BROKEN_" + config.id,
|
||||||
|
"Authentication corrupted for plugin [${config.name}]",
|
||||||
|
"Something went wrong in the stored authentication, you'll have to login again", AnnouncementType.SESSION);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class AppPluginSettings {
|
class AppPluginSettings {
|
||||||
|
|
||||||
|
@FormField(R.string.check_for_updates_setting, FieldForm.TOGGLE, R.string.check_for_updates_setting_description, 0)
|
||||||
|
var checkForUpdates: Boolean = true;
|
||||||
|
|
||||||
@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();
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -106,6 +131,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);
|
||||||
@@ -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.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.futo.platformplayer.casting.models.FCastSetSpeedMessage
|
|||||||
import com.futo.platformplayer.casting.models.FCastSetVolumeMessage
|
import com.futo.platformplayer.casting.models.FCastSetVolumeMessage
|
||||||
import com.futo.platformplayer.casting.models.FCastVersionMessage
|
import com.futo.platformplayer.casting.models.FCastVersionMessage
|
||||||
import com.futo.platformplayer.casting.models.FCastVolumeUpdateMessage
|
import com.futo.platformplayer.casting.models.FCastVolumeUpdateMessage
|
||||||
|
import com.futo.platformplayer.ensureNotMainThread
|
||||||
import com.futo.platformplayer.getConnectedSocket
|
import com.futo.platformplayer.getConnectedSocket
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
@@ -251,7 +252,7 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
|
|
||||||
val thread = _thread
|
val thread = _thread
|
||||||
val pingThread = _pingThread
|
val pingThread = _pingThread
|
||||||
if (thread == null || !thread.isAlive || pingThread == null || !pingThread.isAlive) {
|
if (_started && (thread == null || !thread.isAlive || pingThread == null || !pingThread.isAlive)) {
|
||||||
Log.i(TAG, "(Re)starting thread because the thread has died")
|
Log.i(TAG, "(Re)starting thread because the thread has died")
|
||||||
|
|
||||||
_scopeIO?.let {
|
_scopeIO?.let {
|
||||||
@@ -298,6 +299,8 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
_socket?.close()
|
_socket?.close()
|
||||||
|
_inputStream?.close()
|
||||||
|
_outputStream?.close()
|
||||||
if (connectedSocket != null) {
|
if (connectedSocket != null) {
|
||||||
Logger.i(TAG, "Using connected socket.");
|
Logger.i(TAG, "Using connected socket.");
|
||||||
_socket = connectedSocket
|
_socket = connectedSocket
|
||||||
@@ -311,7 +314,9 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
_outputStream = _socket?.outputStream;
|
_outputStream = _socket?.outputStream;
|
||||||
_inputStream = _socket?.inputStream;
|
_inputStream = _socket?.inputStream;
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
_socket?.close();
|
_socket?.close()
|
||||||
|
_inputStream?.close()
|
||||||
|
_outputStream?.close()
|
||||||
Logger.i(TAG, "Failed to connect to FastCast.", e);
|
Logger.i(TAG, "Failed to connect to FastCast.", e);
|
||||||
|
|
||||||
connectionState = CastConnectionState.CONNECTING;
|
connectionState = CastConnectionState.CONNECTING;
|
||||||
@@ -333,7 +338,10 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
|
|
||||||
var headerBytesRead = 0
|
var headerBytesRead = 0
|
||||||
while (headerBytesRead < 4) {
|
while (headerBytesRead < 4) {
|
||||||
headerBytesRead += inputStream.read(buffer, headerBytesRead, 4 - headerBytesRead)
|
val read = inputStream.read(buffer, headerBytesRead, 4 - headerBytesRead)
|
||||||
|
if (read == -1)
|
||||||
|
throw Exception("Stream closed")
|
||||||
|
headerBytesRead += read
|
||||||
}
|
}
|
||||||
|
|
||||||
val size = ((buffer[3].toLong() shl 24) or (buffer[2].toLong() shl 16) or (buffer[1].toLong() shl 8) or buffer[0].toLong()).toInt();
|
val size = ((buffer[3].toLong() shl 24) or (buffer[2].toLong() shl 16) or (buffer[1].toLong() shl 8) or buffer[0].toLong()).toInt();
|
||||||
@@ -345,7 +353,10 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
||||||
var bytesRead = 0
|
var bytesRead = 0
|
||||||
while (bytesRead < size) {
|
while (bytesRead < size) {
|
||||||
bytesRead += inputStream.read(buffer, bytesRead, size - bytesRead)
|
val read = inputStream.read(buffer, bytesRead, size - bytesRead)
|
||||||
|
if (read == -1)
|
||||||
|
throw Exception("Stream closed")
|
||||||
|
bytesRead += read
|
||||||
}
|
}
|
||||||
|
|
||||||
val messageBytes = buffer.sliceArray(IntRange(0, size));
|
val messageBytes = buffer.sliceArray(IntRange(0, size));
|
||||||
@@ -374,7 +385,8 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
_socket?.close()
|
_socket?.close()
|
||||||
_socket = null
|
_inputStream?.close()
|
||||||
|
_outputStream?.close()
|
||||||
Logger.i(TAG, "Socket disconnected.");
|
Logger.i(TAG, "Socket disconnected.");
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to close socket.", e)
|
Logger.e(TAG, "Failed to close socket.", e)
|
||||||
@@ -399,6 +411,8 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
_socket?.close()
|
_socket?.close()
|
||||||
|
_inputStream?.close()
|
||||||
|
_outputStream?.close()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.w(TAG, "Failed to close socket.", e)
|
Log.w(TAG, "Failed to close socket.", e)
|
||||||
}
|
}
|
||||||
@@ -477,6 +491,8 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun send(opcode: Opcode, message: String? = null) {
|
private fun send(opcode: Opcode, message: String? = null) {
|
||||||
|
ensureNotMainThread()
|
||||||
|
|
||||||
synchronized (_outputStreamLock) {
|
synchronized (_outputStreamLock) {
|
||||||
try {
|
try {
|
||||||
val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
|
val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
|
||||||
@@ -536,6 +552,8 @@ class FCastCastingDevice : CastingDevice {
|
|||||||
|
|
||||||
scopeIO.launch {
|
scopeIO.launch {
|
||||||
socket.close();
|
socket.close();
|
||||||
|
_inputStream?.close()
|
||||||
|
_outputStream?.close()
|
||||||
connectionState = CastConnectionState.DISCONNECTED;
|
connectionState = CastConnectionState.DISCONNECTED;
|
||||||
scopeIO.cancel();
|
scopeIO.cancel();
|
||||||
Logger.i(TAG, "Cancelled scopeIO with open socket.")
|
Logger.i(TAG, "Cancelled scopeIO with open socket.")
|
||||||
|
|||||||
@@ -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,6 +104,17 @@ 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")
|
//@HttpGET("/dependencies/vue.js", "application/javascript")
|
||||||
//val depVue = StateAssets.readAsset(context, "devportal/dependencies/vue.js", true);
|
//val depVue = StateAssets.readAsset(context, "devportal/dependencies/vue.js", true);
|
||||||
@@ -190,6 +215,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());
|
||||||
}
|
}
|
||||||
@@ -440,6 +476,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;
|
||||||
|
|||||||
@@ -123,7 +123,8 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
|||||||
msg = comment,
|
msg = comment,
|
||||||
rating = RatingLikeDislikes(0, 0),
|
rating = RatingLikeDislikes(0, 0),
|
||||||
date = OffsetDateTime.now(),
|
date = OffsetDateTime.now(),
|
||||||
eventPointer = eventPointer
|
eventPointer = eventPointer,
|
||||||
|
parentReference = ref
|
||||||
));
|
));
|
||||||
|
|
||||||
dismiss();
|
dismiss();
|
||||||
|
|||||||
@@ -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}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ import com.caoccao.javet.annotations.V8Function
|
|||||||
import com.caoccao.javet.annotations.V8Property
|
import com.caoccao.javet.annotations.V8Property
|
||||||
import com.caoccao.javet.enums.V8ConversionMode
|
import com.caoccao.javet.enums.V8ConversionMode
|
||||||
import com.caoccao.javet.enums.V8ProxyMode
|
import com.caoccao.javet.enums.V8ProxyMode
|
||||||
|
import com.caoccao.javet.values.reference.V8ValueObject
|
||||||
import com.futo.platformplayer.engine.V8Plugin
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
import com.futo.platformplayer.engine.internal.V8BindObject
|
import com.futo.platformplayer.engine.internal.V8BindObject
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
@@ -65,7 +68,7 @@ class PackageDOMParser : V8Package {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@V8Property
|
@V8Property
|
||||||
fun attributes(): Map<String, String> = _element.attributes().dataset();
|
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
|
||||||
@V8Property
|
@V8Property
|
||||||
fun innerHTML(): String = _element.html();
|
fun innerHTML(): String = _element.html();
|
||||||
@V8Property
|
@V8Property
|
||||||
@@ -138,10 +141,32 @@ class PackageDOMParser : V8Package {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun toNodeTree(): SerializedNode {
|
||||||
|
return SerializedNode(
|
||||||
|
childNodes().map { it.toNodeTree() },
|
||||||
|
_element.tagName(),
|
||||||
|
_element.text(),
|
||||||
|
attributes()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@V8Function
|
||||||
|
fun toNodeTreeJson(): String {
|
||||||
|
return Json.encodeToString(SerializedNode.serializer(), toNodeTree());
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(parser: PackageDOMParser, str: String): DOMNode {
|
fun parse(parser: PackageDOMParser, str: String): DOMNode {
|
||||||
return DOMNode(parser, Jsoup.parse(str));
|
return DOMNode(parser, Jsoup.parse(str));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SerializedNode(
|
||||||
|
val children: List<SerializedNode>,
|
||||||
|
val name: String,
|
||||||
|
val value: String,
|
||||||
|
val attributes: Map<String, String>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
+11
-4
@@ -27,6 +27,7 @@ import com.futo.platformplayer.constructs.Event2
|
|||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
|
import com.futo.platformplayer.exceptions.ChannelException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
||||||
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
|
||||||
@@ -59,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");
|
||||||
|
|
||||||
@@ -102,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() });
|
||||||
@@ -156,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,8 +340,11 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
context?.let {
|
context?.let {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
|
val channel = if(kv.value is ChannelException) (kv.value as ChannelException).channelNameOrUrl else null;
|
||||||
if(jsVideoPager != null)
|
if(jsVideoPager != null)
|
||||||
UIDialogs.toast(it, "Plugin ${jsVideoPager.getPluginConfig().name} failed:\n${kv.value.message}", false);
|
UIDialogs.toast(it, "Plugin ${jsVideoPager.getPluginConfig().name} failed:\n" +
|
||||||
|
(if(!channel.isNullOrEmpty()) "(${channel}) " else "") +
|
||||||
|
"${kv.value.message}", false);
|
||||||
else
|
else
|
||||||
UIDialogs.toast(it, kv.value.message ?: "", false);
|
UIDialogs.toast(it, kv.value.message ?: "", false);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
|||||||
+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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+31
-6
@@ -22,15 +22,17 @@ class BrowserFragment : MainFragment() {
|
|||||||
override val hasBottomBar: Boolean get() = true;
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
private var _webview: WebView? = null;
|
private var _webview: WebView? = null;
|
||||||
|
private val _webviewWithoutHandling = object: WebViewClient() {
|
||||||
|
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
val view = inflater.inflate(R.layout.fragment_browser, container, false);
|
val view = inflater.inflate(R.layout.fragment_browser, container, false);
|
||||||
_webview = view.findViewById<WebView?>(R.id.webview).apply {
|
_webview = view.findViewById<WebView?>(R.id.webview).apply {
|
||||||
this.webViewClient = object: WebViewClient() {
|
this.webViewClient = _webviewWithoutHandling;
|
||||||
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.settings.javaScriptEnabled = true;
|
this.settings.javaScriptEnabled = true;
|
||||||
CookieManager.getInstance().setAcceptCookie(true);
|
CookieManager.getInstance().setAcceptCookie(true);
|
||||||
this.settings.domStorageEnabled = true;
|
this.settings.domStorageEnabled = true;
|
||||||
@@ -41,8 +43,26 @@ class BrowserFragment : MainFragment() {
|
|||||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
super.onShownWithView(parameter, isBack)
|
super.onShownWithView(parameter, isBack)
|
||||||
|
|
||||||
if(parameter is String)
|
if(parameter is String) {
|
||||||
|
_webview?.webViewClient = _webviewWithoutHandling;
|
||||||
_webview?.loadUrl(parameter);
|
_webview?.loadUrl(parameter);
|
||||||
|
}
|
||||||
|
else if(parameter is NavigateOptions) {
|
||||||
|
if(parameter.urlHandlers != null && parameter.urlHandlers.isNotEmpty())
|
||||||
|
_webview?.webViewClient = object: WebViewClient() {
|
||||||
|
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
||||||
|
val schema = request?.url?.scheme;
|
||||||
|
if(schema != null && parameter.urlHandlers.containsKey(schema)) {
|
||||||
|
parameter.urlHandlers[schema]?.invoke(request);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
else
|
||||||
|
_webview?.webViewClient = _webviewWithoutHandling;
|
||||||
|
_webview?.loadUrl(parameter.url);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
_webview?.loadUrl("about:blank");
|
_webview?.loadUrl("about:blank");
|
||||||
}
|
}
|
||||||
@@ -59,4 +79,9 @@ class BrowserFragment : MainFragment() {
|
|||||||
companion object {
|
companion object {
|
||||||
fun newInstance() = BrowserFragment().apply {}
|
fun newInstance() = BrowserFragment().apply {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NavigateOptions(
|
||||||
|
val url: String,
|
||||||
|
val urlHandlers: Map<String, (WebResourceRequest)->Unit>? = null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
+8
-1
@@ -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
|
||||||
@@ -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) {
|
||||||
|
|||||||
+28
@@ -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
|
||||||
@@ -117,6 +119,8 @@ class CommentsFragment : MainFragment() {
|
|||||||
val holder = CommentWithReferenceViewHolder(viewGroup, _cache);
|
val holder = CommentWithReferenceViewHolder(viewGroup, _cache);
|
||||||
holder.onDelete.subscribe(::onDelete);
|
holder.onDelete.subscribe(::onDelete);
|
||||||
holder.onRepliesClick.subscribe(::onRepliesClick);
|
holder.onRepliesClick.subscribe(::onRepliesClick);
|
||||||
|
holder.onClick.subscribe(::onClick);
|
||||||
|
holder.onAuthorClick.subscribe(::onAuthorClick);
|
||||||
return@InsertedViewAdapterWithLoader holder;
|
return@InsertedViewAdapterWithLoader holder;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -200,6 +204,30 @@ class CommentsFragment : MainFragment() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onClick(c: IPlatformComment) {
|
||||||
|
if (c !is PolycentricPlatformComment) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val parentRef = c.parentReference
|
||||||
|
if (parentRef != null && _repliesOverlay.handleParentClick(c.contextUrl, parentRef)) {
|
||||||
|
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;
|
||||||
var metadata = "";
|
var metadata = "";
|
||||||
|
|||||||
+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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-4
@@ -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);
|
||||||
@@ -154,8 +154,14 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onSearch.subscribe(this) {
|
onSearch.subscribe(this) {
|
||||||
if(it.isHttpUrl())
|
if(it.isHttpUrl()) {
|
||||||
navigate<VideoDetailFragment>(it);
|
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
|
||||||
|
navigate<PlaylistFragment>(it);
|
||||||
|
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
||||||
|
navigate<ChannelFragment>(it);
|
||||||
|
else
|
||||||
|
navigate<VideoDetailFragment>(it);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
setQuery(it, true);
|
setQuery(it, true);
|
||||||
};
|
};
|
||||||
@@ -164,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) {
|
||||||
|
|||||||
+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);
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-1
@@ -8,6 +8,7 @@ import android.view.ViewGroup
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
@@ -17,15 +18,23 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
|||||||
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.SearchType
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
|
import com.futo.platformplayer.views.NoResultsView
|
||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
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 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
|
||||||
|
|
||||||
@@ -147,6 +156,42 @@ class HomeFragment : MainFragment() {
|
|||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getEmptyPagerView(): View? {
|
||||||
|
val dp10 = 10.dp(resources);
|
||||||
|
val dp30 = 30.dp(resources);
|
||||||
|
|
||||||
|
val pluginsExist = StatePlatform.instance.getAvailableClients().isNotEmpty();
|
||||||
|
if(StatePlatform.instance.getEnabledClients().isEmpty())
|
||||||
|
//Initial setup
|
||||||
|
return NoResultsView(context, "No enabled sources", if(pluginsExist)
|
||||||
|
"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,
|
||||||
|
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(
|
||||||
|
Pair("grayjay") { req ->
|
||||||
|
StateApp.instance.contextOrNull?.let {
|
||||||
|
if(it is MainActivity) {
|
||||||
|
runBlocking {
|
||||||
|
it.handleUrlAll(req.url.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)));
|
||||||
|
}.withMargin(dp10, dp30),
|
||||||
|
if(pluginsExist) BigButton(context, "Sources", "Go to the sources tab", R.drawable.ic_creators) {
|
||||||
|
fragment.navigate<SourcesFragment>();
|
||||||
|
}.withMargin(dp10, dp30) else null).filterNotNull()
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return NoResultsView(context, "Nothing to see here", "The enabled sources do not have any results.", R.drawable.ic_help,
|
||||||
|
listOf(BigButton(context, "Sources", "Go to the sources tab", R.drawable.ic_creators) {
|
||||||
|
fragment.navigate<SourcesFragment>();
|
||||||
|
}.withMargin(dp10, dp30))
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
override fun reload() {
|
override fun reload() {
|
||||||
loadResults();
|
loadResults();
|
||||||
}
|
}
|
||||||
@@ -161,13 +206,15 @@ class HomeFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
private fun loadedResult(pager : IPager<IPlatformContent>) {
|
private fun loadedResult(pager : IPager<IPlatformContent>) {
|
||||||
if (pager is EmptyPager<IPlatformContent>) {
|
if (pager is EmptyPager<IPlatformContent>) {
|
||||||
StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), context.getString(R.string.no_home_available), context.getString(R.string.no_home_page_is_available_please_check_if_you_are_connected_to_the_internet_and_refresh), AnnouncementType.SESSION);
|
//StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), context.getString(R.string.no_home_available), context.getString(R.string.no_home_page_is_available_please_check_if_you_are_connected_to_the_internet_and_refresh), AnnouncementType.SESSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "Got new home pager ${pager}");
|
Logger.i(TAG, "Got new home pager ${pager}");
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setPager(pager);
|
setPager(pager);
|
||||||
|
if(pager.getResults().isEmpty() && !pager.hasMorePages())
|
||||||
|
setEmptyPager(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-40
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -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
|
||||||
@@ -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);
|
||||||
@@ -315,7 +318,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
_rating.visibility = View.GONE;
|
_rating.visibility = View.GONE;
|
||||||
|
|
||||||
val ref = Models.referenceFromBuffer((_post?.url ?: _postOverview?.url)?.toByteArray() ?: return)
|
val ref = Models.referenceFromBuffer((_post?.url ?: _postOverview?.url)?.toByteArray() ?: return)
|
||||||
val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.toByteArray()
|
val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
||||||
val version = _version;
|
val version = _version;
|
||||||
|
|
||||||
_rating.onLikeDislikeUpdated.remove(this);
|
_rating.onLikeDislikeUpdated.remove(this);
|
||||||
@@ -473,6 +476,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCommentType(true);
|
updateCommentType(true);
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPostOverview(value: IPlatformPost) {
|
fun setPostOverview(value: IPlatformPost) {
|
||||||
@@ -663,7 +667,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
Logger.i(TAG, "fetchPolycentricComments")
|
Logger.i(TAG, "fetchPolycentricComments")
|
||||||
val post = _post;
|
val post = _post;
|
||||||
val ref = (_post?.url ?: _postOverview?.url)?.toByteArray()?.let { Models.referenceFromBuffer(it) }
|
val ref = (_post?.url ?: _postOverview?.url)?.toByteArray()?.let { Models.referenceFromBuffer(it) }
|
||||||
val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.toByteArray()
|
val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
||||||
|
|
||||||
if (ref == null) {
|
if (ref == null) {
|
||||||
Logger.w(TAG, "Failed to fetch polycentric comments because url was not set null")
|
Logger.w(TAG, "Failed to fetch polycentric comments because url was not set null")
|
||||||
|
|||||||
+45
-13
@@ -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
|
||||||
@@ -107,17 +108,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 +141,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)
|
||||||
@@ -295,17 +315,24 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isEmbedded = StatePlugins.instance.getEmbeddedSources(context).any { it.key == config.id };
|
||||||
|
|
||||||
val clientIfExists = if(config.id != StateDeveloper.DEV_ID)
|
val clientIfExists = if(config.id != StateDeveloper.DEV_ID)
|
||||||
StatePlugins.instance.getPlugin(config.id);
|
StatePlugins.instance.getPlugin(config.id);
|
||||||
else null;
|
else null;
|
||||||
groups.add(
|
groups.add(
|
||||||
BigButtonGroup(c, context.getString(R.string.management),
|
BigButtonGroup(c, context.getString(R.string.management),
|
||||||
BigButton(c, context.getString(R.string.uninstall), context.getString(R.string.removes_the_plugin_from_the_app), R.drawable.ic_block) {
|
if(!isEmbedded) BigButton(c, context.getString(R.string.uninstall), context.getString(R.string.removes_the_plugin_from_the_app), R.drawable.ic_block) {
|
||||||
uninstallSource();
|
uninstallSource();
|
||||||
}.withBackground(R.drawable.background_big_button_red).apply {
|
}.withBackground(R.drawable.background_big_button_red).apply {
|
||||||
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
};
|
};
|
||||||
|
} else BigButton(c, context.getString(R.string.uninstall), "Cannot uninstall embedded plugins", R.drawable.ic_block, {}).apply {
|
||||||
|
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
|
};
|
||||||
|
this.alpha = 0.5f
|
||||||
},
|
},
|
||||||
if(clientIfExists?.captchaEncrypted != null)
|
if(clientIfExists?.captchaEncrypted != null)
|
||||||
BigButton(c, context.getString(R.string.delete_captcha), context.getString(R.string.deletes_stored_captcha_answer_for_this_plugin), R.drawable.ic_block) {
|
BigButton(c, context.getString(R.string.delete_captcha), context.getString(R.string.deletes_stored_captcha_answer_for_this_plugin), R.drawable.ic_block) {
|
||||||
@@ -325,7 +352,6 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
_sourceButtons.addView(group);
|
_sourceButtons.addView(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
val isEmbedded = StatePlugins.instance.getEmbeddedSources(context).any { it.key == config.id };
|
|
||||||
val advancedButtons = BigButtonGroup(c, "Advanced",
|
val advancedButtons = BigButtonGroup(c, "Advanced",
|
||||||
BigButton(c, "Edit Code", "Modify the source of this plugin", R.drawable.ic_code) {
|
BigButton(c, "Edit Code", "Modify the source of this plugin", R.drawable.ic_code) {
|
||||||
|
|
||||||
@@ -333,9 +359,15 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
this.alpha = 0.5f;
|
this.alpha = 0.5f;
|
||||||
},
|
},
|
||||||
if(isEmbedded) BigButton(c, "Reinstall", "Modify the source of this plugin", R.drawable.ic_refresh) {
|
if(isEmbedded) BigButton(c, "Reinstall", "Modify the source of this plugin", R.drawable.ic_refresh) {
|
||||||
StatePlugins.instance.updateEmbeddedPlugins(context, listOf(config.id), true);
|
val embeddedConfig = StatePlugins.instance.getEmbeddedPluginConfigFromID(context, config.id);
|
||||||
reloadSource(config.id);
|
|
||||||
UIDialogs.toast(context, "Embedded plugin reinstalled, may require refresh");
|
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow, "Are you sure you want to downgrade (${config.version}=>${embeddedConfig?.version})?",
|
||||||
|
"This will revert the plugin back to the originally embedded version.\nVersion change: ${config.version}=>${embeddedConfig?.version}", null,
|
||||||
|
0, UIDialogs.Action("Cancel", {}), UIDialogs.Action("Reinstall", {
|
||||||
|
StatePlugins.instance.updateEmbeddedPlugins(context, listOf(config.id), true);
|
||||||
|
reloadSource(config.id);
|
||||||
|
UIDialogs.toast(context, "Embedded plugin reinstalled, may require refresh");
|
||||||
|
}, UIDialogs.ActionStyle.DANGEROUS));
|
||||||
}.apply {
|
}.apply {
|
||||||
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
this.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||||
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
|
|||||||
+10
-7
@@ -8,6 +8,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -25,6 +26,7 @@ import com.futo.platformplayer.views.adapters.DisabledSourceView
|
|||||||
import com.futo.platformplayer.views.adapters.EnabledSourceAdapter
|
import com.futo.platformplayer.views.adapters.EnabledSourceAdapter
|
||||||
import com.futo.platformplayer.views.adapters.EnabledSourceViewHolder
|
import com.futo.platformplayer.views.adapters.EnabledSourceViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.ItemMoveCallback
|
import com.futo.platformplayer.views.adapters.ItemMoveCallback
|
||||||
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import com.futo.platformplayer.views.sources.SourceUnderConstructionView
|
import com.futo.platformplayer.views.sources.SourceUnderConstructionView
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
@@ -86,6 +88,14 @@ class SourcesFragment : MainFragment() {
|
|||||||
_containerDisabledViews = findViewById(R.id.container_disabled_views);
|
_containerDisabledViews = findViewById(R.id.container_disabled_views);
|
||||||
_containerConstruction = findViewById(R.id.container_construction);
|
_containerConstruction = findViewById(R.id.container_construction);
|
||||||
|
|
||||||
|
if(StatePlatform.instance.getAvailableClients().isEmpty()) {
|
||||||
|
findViewById<LinearLayout>(R.id.no_sources).isVisible = true;
|
||||||
|
findViewById<LinearLayout>(R.id.plugin_disclaimer).isVisible = false;
|
||||||
|
}
|
||||||
|
findViewById<BigButton>(R.id.button_add_sources).onClick.subscribe {
|
||||||
|
fragment.startActivity(Intent(context, AddSourceOptionsActivity::class.java));
|
||||||
|
};
|
||||||
|
|
||||||
for(inConstructSource in StatePlugins.instance.getSourcesUnderConstruction(context))
|
for(inConstructSource in StatePlugins.instance.getSourcesUnderConstruction(context))
|
||||||
_containerConstruction.addView(SourceUnderConstructionView(context, inConstructSource.key, inConstructSource.value));
|
_containerConstruction.addView(SourceUnderConstructionView(context, inConstructSource.key, inConstructSource.value));
|
||||||
|
|
||||||
@@ -111,8 +121,6 @@ class SourcesFragment : MainFragment() {
|
|||||||
|
|
||||||
adapterSourcesEnabled.notifyItemMoved(fromPosition, toPosition);
|
adapterSourcesEnabled.notifyItemMoved(fromPosition, toPosition);
|
||||||
onEnabledChanged(enabledSources);
|
onEnabledChanged(enabledSources);
|
||||||
if(toPosition == 0)
|
|
||||||
onPrimaryChanged(enabledSources.first());
|
|
||||||
|
|
||||||
StatePlatform.instance.setPlatformOrder(enabledSources.map { it.name });
|
StatePlatform.instance.setPlatformOrder(enabledSources.map { it.name });
|
||||||
};
|
};
|
||||||
@@ -133,8 +141,6 @@ class SourcesFragment : MainFragment() {
|
|||||||
|
|
||||||
updateContainerVisibility();
|
updateContainerVisibility();
|
||||||
onEnabledChanged(enabledSources);
|
onEnabledChanged(enabledSources);
|
||||||
if(index == 0)
|
|
||||||
onPrimaryChanged(enabledSources.first());
|
|
||||||
|
|
||||||
if(enabledSources.size <= 1)
|
if(enabledSources.size <= 1)
|
||||||
setCanRemove(false);
|
setCanRemove(false);
|
||||||
@@ -221,9 +227,6 @@ class SourcesFragment : MainFragment() {
|
|||||||
_adapterSourcesEnabled.canRemove = canRemove;
|
_adapterSourcesEnabled.canRemove = canRemove;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPrimaryChanged(client: IPlatformClient) {
|
|
||||||
StatePlatform.instance.selectPrimaryClient(client.id);
|
|
||||||
}
|
|
||||||
private fun onEnabledChanged(clients: List<IPlatformClient>) {
|
private fun onEnabledChanged(clients: List<IPlatformClient>) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
StatePlatform.instance.selectClients(*clients.map { it.id }.toTypedArray());
|
StatePlatform.instance.selectClients(*clients.map { it.id }.toTypedArray());
|
||||||
|
|||||||
+17
-3
@@ -25,12 +25,14 @@ 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
|
||||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.NoResultsView
|
import com.futo.platformplayer.views.NoResultsView
|
||||||
|
import com.futo.platformplayer.views.ToastView
|
||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||||
@@ -44,6 +46,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.nio.channels.Channel
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
@@ -195,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);
|
||||||
}
|
}
|
||||||
@@ -302,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); }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,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;
|
||||||
@@ -440,14 +448,17 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
Logger.e(TAG, "Channel [${channel}] failed", ex);
|
Logger.e(TAG, "Channel [${channel}] failed", ex);
|
||||||
if (toShow is PluginException)
|
if (toShow is PluginException)
|
||||||
UIDialogs.appToast(
|
UIDialogs.appToast(ToastView.Toast(
|
||||||
context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", toShow.config.name).replace("{message}", toShow.message ?: "")
|
toShow.message +
|
||||||
|
(if(channel != null) "\nChannel: " + channel else ""), false, null,
|
||||||
|
"Plugin ${toShow.config.name} failed")
|
||||||
);
|
);
|
||||||
else
|
else
|
||||||
UIDialogs.appToast(ex.message ?: "");
|
UIDialogs.appToast(ex.message ?: "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
val failedChannels = exs.filterIsInstance<ChannelException>().map { it.channelNameOrUrl }.distinct().toList();
|
||||||
val failedPlugins = exs.filter { it is PluginException || (it is ChannelException && it.cause is PluginException) }
|
val failedPlugins = exs.filter { it is PluginException || (it is ChannelException && it.cause is PluginException) }
|
||||||
.map { if(it is ChannelException) (it.cause as PluginException) else if(it is PluginException) it else null }
|
.map { if(it is ChannelException) (it.cause as PluginException) else if(it is PluginException) it else null }
|
||||||
.filter { it != null }
|
.filter { it != null }
|
||||||
@@ -456,6 +467,9 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
.toList();
|
.toList();
|
||||||
for(distinctPluginFail in failedPlugins)
|
for(distinctPluginFail in failedPlugins)
|
||||||
UIDialogs.appToast(context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: ""));
|
UIDialogs.appToast(context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: ""));
|
||||||
|
if(failedChannels.isNotEmpty())
|
||||||
|
UIDialogs.appToast(ToastView.Toast(failedChannels.take(3).map { "- ${it}" }.joinToString("\n") +
|
||||||
|
(if(failedChannels.size >= 3) "\nAnd ${failedChannels.size - 3} more" else ""), false, null, "Failed Channels"));
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to handle exceptions", e)
|
Logger.e(TAG, "Failed to handle exceptions", e)
|
||||||
|
|||||||
+24
-11
@@ -67,7 +67,7 @@ class TutorialFragment : MainFragment() {
|
|||||||
|
|
||||||
addView(createHeader("Initial setup"))
|
addView(createHeader("Initial setup"))
|
||||||
initialSetupVideos.forEach {
|
initialSetupVideos.forEach {
|
||||||
addView(createTutorialPill(R.drawable.ic_movie, it.name).apply {
|
addView(createTutorialPill(R.drawable.ic_movie, it.name, it.description).apply {
|
||||||
onClick.subscribe {
|
onClick.subscribe {
|
||||||
fragment.navigate<VideoDetailFragment>(it)
|
fragment.navigate<VideoDetailFragment>(it)
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ class TutorialFragment : MainFragment() {
|
|||||||
|
|
||||||
addView(createHeader("Features"))
|
addView(createHeader("Features"))
|
||||||
featuresVideos.forEach {
|
featuresVideos.forEach {
|
||||||
addView(createTutorialPill(R.drawable.ic_movie, it.name).apply {
|
addView(createTutorialPill(R.drawable.ic_movie, it.name, it.description).apply {
|
||||||
onClick.subscribe {
|
onClick.subscribe {
|
||||||
fragment.navigate<VideoDetailFragment>(it)
|
fragment.navigate<VideoDetailFragment>(it)
|
||||||
}
|
}
|
||||||
@@ -95,10 +95,11 @@ class TutorialFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createTutorialPill(iconPrefix: Int, t: String): WidePillButton {
|
private fun createTutorialPill(iconPrefix: Int, t: String, d: String): WidePillButton {
|
||||||
return WidePillButton(context).apply {
|
return WidePillButton(context).apply {
|
||||||
setIconPrefix(iconPrefix)
|
setIconPrefix(iconPrefix)
|
||||||
setText(t)
|
setText(t)
|
||||||
|
setDescription(d)
|
||||||
setIconSuffix(R.drawable.ic_play_notif)
|
setIconSuffix(R.drawable.ic_play_notif)
|
||||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
|
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
|
||||||
setMargins(15.dp(resources), 0, 15.dp(resources), 12.dp(resources))
|
setMargins(15.dp(resources), 0, 15.dp(resources), 12.dp(resources))
|
||||||
@@ -107,9 +108,9 @@ class TutorialFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TutorialVideoSourceDescriptor(url: String, duration: Long) : VideoUnMuxedSourceDescriptor() {
|
class TutorialVideoSourceDescriptor(url: String, duration: Long, width: Int, height: Int) : VideoUnMuxedSourceDescriptor() {
|
||||||
override val videoSources: Array<IVideoSource> = arrayOf(
|
override val videoSources: Array<IVideoSource> = arrayOf(
|
||||||
VideoUrlSource("1080p", url, 1920, 1080, duration, "video/mp4")
|
VideoUrlSource("Original", url, width, height, duration, "video/mp4")
|
||||||
)
|
)
|
||||||
override val audioSources: Array<IAudioSource> = arrayOf()
|
override val audioSources: Array<IAudioSource> = arrayOf()
|
||||||
}
|
}
|
||||||
@@ -120,7 +121,9 @@ class TutorialFragment : MainFragment() {
|
|||||||
override val description: String,
|
override val description: String,
|
||||||
thumbnailUrl: String,
|
thumbnailUrl: String,
|
||||||
videoUrl: String,
|
videoUrl: String,
|
||||||
override val duration: Long
|
override val duration: Long,
|
||||||
|
width: Int = 1920,
|
||||||
|
height: Int = 1080
|
||||||
) : IPlatformVideoDetails {
|
) : IPlatformVideoDetails {
|
||||||
override val id: PlatformID = PlatformID("tutorial", uuid)
|
override val id: PlatformID = PlatformID("tutorial", uuid)
|
||||||
override val contentType: ContentType = ContentType.MEDIA
|
override val contentType: ContentType = ContentType.MEDIA
|
||||||
@@ -137,7 +140,7 @@ class TutorialFragment : MainFragment() {
|
|||||||
override val isLive: Boolean = false
|
override val isLive: Boolean = false
|
||||||
override val rating: IRating = RatingLikes(-1)
|
override val rating: IRating = RatingLikes(-1)
|
||||||
override val viewCount: Long = -1
|
override val viewCount: Long = -1
|
||||||
override val video: IVideoSourceDescriptor = TutorialVideoSourceDescriptor(videoUrl, duration)
|
override val video: IVideoSourceDescriptor = TutorialVideoSourceDescriptor(videoUrl, duration, width, height)
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
|
||||||
return EmptyPager()
|
return EmptyPager()
|
||||||
}
|
}
|
||||||
@@ -163,7 +166,7 @@ class TutorialFragment : MainFragment() {
|
|||||||
TutorialVideo(
|
TutorialVideo(
|
||||||
uuid = "3b99ebfe-2640-4643-bfe0-a0cf04261fc5",
|
uuid = "3b99ebfe-2640-4643-bfe0-a0cf04261fc5",
|
||||||
name = "Getting started",
|
name = "Getting started",
|
||||||
description = "Learn how to get started with Grayjay.",
|
description = "Learn how to get started with Grayjay. How do you install plugins?",
|
||||||
thumbnailUrl = "https://releases.grayjay.app/tutorials/getting-started.jpg",
|
thumbnailUrl = "https://releases.grayjay.app/tutorials/getting-started.jpg",
|
||||||
videoUrl = "https://releases.grayjay.app/tutorials/getting-started.mp4",
|
videoUrl = "https://releases.grayjay.app/tutorials/getting-started.mp4",
|
||||||
duration = 50
|
duration = 50
|
||||||
@@ -171,7 +174,7 @@ class TutorialFragment : MainFragment() {
|
|||||||
TutorialVideo(
|
TutorialVideo(
|
||||||
uuid = "793aa009-516c-4581-b82f-a8efdfef4c27",
|
uuid = "793aa009-516c-4581-b82f-a8efdfef4c27",
|
||||||
name = "Is Grayjay free?",
|
name = "Is Grayjay free?",
|
||||||
description = "Learn how Grayjay is monetized.",
|
description = "Learn how Grayjay is monetized. How do we make money?",
|
||||||
thumbnailUrl = "https://releases.grayjay.app/tutorials/pay.jpg",
|
thumbnailUrl = "https://releases.grayjay.app/tutorials/pay.jpg",
|
||||||
videoUrl = "https://releases.grayjay.app/tutorials/pay.mp4",
|
videoUrl = "https://releases.grayjay.app/tutorials/pay.mp4",
|
||||||
duration = 52
|
duration = 52
|
||||||
@@ -182,7 +185,7 @@ class TutorialFragment : MainFragment() {
|
|||||||
TutorialVideo(
|
TutorialVideo(
|
||||||
uuid = "d2238d88-4252-4a91-a12d-b90c049bb7cf",
|
uuid = "d2238d88-4252-4a91-a12d-b90c049bb7cf",
|
||||||
name = "Searching",
|
name = "Searching",
|
||||||
description = "Learn about searching in Grayjay.",
|
description = "Learn about searching in Grayjay. How can I find channels, videos or playlists?",
|
||||||
thumbnailUrl = "https://releases.grayjay.app/tutorials/search.jpg",
|
thumbnailUrl = "https://releases.grayjay.app/tutorials/search.jpg",
|
||||||
videoUrl = "https://releases.grayjay.app/tutorials/search.mp4",
|
videoUrl = "https://releases.grayjay.app/tutorials/search.mp4",
|
||||||
duration = 39
|
duration = 39
|
||||||
@@ -198,10 +201,20 @@ class TutorialFragment : MainFragment() {
|
|||||||
TutorialVideo(
|
TutorialVideo(
|
||||||
uuid = "94d36959-e3fc-4c24-a988-89147067a179",
|
uuid = "94d36959-e3fc-4c24-a988-89147067a179",
|
||||||
name = "Casting",
|
name = "Casting",
|
||||||
description = "Learn about casting in Grayjay.",
|
description = "Learn about casting in Grayjay. How do I show video on my TV?",
|
||||||
thumbnailUrl = "https://releases.grayjay.app/tutorials/how-to-cast.jpg",
|
thumbnailUrl = "https://releases.grayjay.app/tutorials/how-to-cast.jpg",
|
||||||
videoUrl = "https://releases.grayjay.app/tutorials/how-to-cast.mp4",
|
videoUrl = "https://releases.grayjay.app/tutorials/how-to-cast.mp4",
|
||||||
duration = 79
|
duration = 79
|
||||||
|
),
|
||||||
|
TutorialVideo(
|
||||||
|
uuid = "5128c2e3-852b-4281-869b-efea2ec82a0e",
|
||||||
|
name = "Monetization",
|
||||||
|
description = "How can I monetize as a creator?",
|
||||||
|
thumbnailUrl = "https://releases.grayjay.app/tutorials/monetization.jpg",
|
||||||
|
videoUrl = "https://releases.grayjay.app/tutorials/monetization.mp4",
|
||||||
|
duration = 47,
|
||||||
|
1080,
|
||||||
|
1920
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -169,14 +169,14 @@ class VideoDetailFragment : MainFragment {
|
|||||||
_view!!.transitionToStart();
|
_view!!.transitionToStart();
|
||||||
}
|
}
|
||||||
fun maximizeVideoDetail(instant: Boolean = false) {
|
fun maximizeVideoDetail(instant: Boolean = false) {
|
||||||
if(_maximizeProgress > 0.9f && state != State.MAXIMIZED) {
|
if((_maximizeProgress > 0.9f || instant) && state != State.MAXIMIZED) {
|
||||||
state = State.MAXIMIZED;
|
state = State.MAXIMIZED;
|
||||||
onMaximized.emit();
|
onMaximized.emit();
|
||||||
}
|
}
|
||||||
_view?.let {
|
_view?.let {
|
||||||
if(!instant)
|
if(!instant) {
|
||||||
it.transitionToEnd();
|
it.transitionToEnd();
|
||||||
else {
|
} else {
|
||||||
it.progress = 1f;
|
it.progress = 1f;
|
||||||
onTransitioning.emit(true);
|
onTransitioning.emit(true);
|
||||||
}
|
}
|
||||||
@@ -424,6 +424,7 @@ class VideoDetailFragment : MainFragment {
|
|||||||
changeOrientation(OrientationManager.Orientation.PORTRAIT);
|
changeOrientation(OrientationManager.Orientation.PORTRAIT);
|
||||||
}
|
}
|
||||||
isFullscreen = fullscreen;
|
isFullscreen = fullscreen;
|
||||||
|
_view?.allowMotion = !fullscreen;
|
||||||
}
|
}
|
||||||
private fun changeOrientation(orientation: OrientationManager.Orientation) {
|
private fun changeOrientation(orientation: OrientationManager.Orientation) {
|
||||||
Logger.i(TAG, "Orientation Change:" + orientation.name);
|
Logger.i(TAG, "Orientation Change:" + orientation.name);
|
||||||
|
|||||||
+55
-20
@@ -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
|
||||||
@@ -145,7 +147,6 @@ import com.futo.polycentric.core.Opinion
|
|||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -245,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;
|
||||||
@@ -350,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);
|
||||||
@@ -374,7 +377,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
|
|
||||||
_buttonSubscribe.onSubscribed.subscribe {
|
_buttonSubscribe.onSubscribed.subscribe {
|
||||||
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||||
};
|
};
|
||||||
|
|
||||||
_container_content_liveChat.onRaidNow.subscribe {
|
_container_content_liveChat.onRaidNow.subscribe {
|
||||||
@@ -399,6 +402,10 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
_monetization.onUrlTap.subscribe {
|
||||||
|
fragment.navigate<BrowserFragment>(it);
|
||||||
|
onMinimize.emit();
|
||||||
|
}
|
||||||
|
|
||||||
_player.attachPlayer();
|
_player.attachPlayer();
|
||||||
|
|
||||||
@@ -621,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);
|
||||||
@@ -641,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 = "";
|
||||||
@@ -762,7 +784,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
fun updateMoreButtons() {
|
fun updateMoreButtons() {
|
||||||
val buttons = listOf(RoundButton(context, R.drawable.ic_add, context.getString(R.string.add), TAG_ADD) {
|
val buttons = listOf(RoundButton(context, R.drawable.ic_add, context.getString(R.string.add), TAG_ADD) {
|
||||||
(video ?: _searchVideo)?.let {
|
(video ?: _searchVideo)?.let {
|
||||||
_slideUpOverlay = UISlideOverlays.showAddToOverlay(it, _overlayContainer);
|
_slideUpOverlay = UISlideOverlays.showAddToOverlay(it, _overlayContainer) {
|
||||||
|
_slideUpOverlay = it
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
if(video?.isLive ?: false)
|
if(video?.isLive ?: false)
|
||||||
@@ -858,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;
|
||||||
}
|
}
|
||||||
@@ -1004,6 +1028,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
fun setVideo(url: String, resumeSeconds: Long = 0, playWhenReady: Boolean = true) {
|
fun setVideo(url: String, resumeSeconds: Long = 0, playWhenReady: Boolean = true) {
|
||||||
Logger.i(TAG, "setVideo url=$url resumeSeconds=$resumeSeconds playWhenReady=$playWhenReady")
|
Logger.i(TAG, "setVideo url=$url resumeSeconds=$resumeSeconds playWhenReady=$playWhenReady")
|
||||||
|
|
||||||
|
if(this.video?.url == url)
|
||||||
|
return;
|
||||||
|
|
||||||
_searchVideo = null;
|
_searchVideo = null;
|
||||||
video = null;
|
video = null;
|
||||||
_playbackTracker = null;
|
_playbackTracker = null;
|
||||||
@@ -1031,9 +1058,12 @@ 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(!bypassSameVideoCheck && this.video?.url == video.url)
|
||||||
|
return;
|
||||||
|
|
||||||
val cachedVideo = StateDownloads.instance.getCachedVideo(video.id);
|
val cachedVideo = StateDownloads.instance.getCachedVideo(video.id);
|
||||||
if(cachedVideo != null) {
|
if(cachedVideo != null) {
|
||||||
setVideoDetails(cachedVideo, true);
|
setVideoDetails(cachedVideo, true);
|
||||||
@@ -1132,6 +1162,9 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
||||||
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
||||||
|
|
||||||
|
if(newVideo && this.video?.url == videoDetail.url)
|
||||||
|
return;
|
||||||
|
|
||||||
if (newVideo) {
|
if (newVideo) {
|
||||||
_lastVideoSource = null;
|
_lastVideoSource = null;
|
||||||
_lastAudioSource = null;
|
_lastAudioSource = null;
|
||||||
@@ -1220,7 +1253,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||||
val extraBytesRef = video.id.value?.toByteArray()
|
val extraBytesRef = video.id.value?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
||||||
_addCommentView.setContext(video.url, ref)
|
_addCommentView.setContext(video.url, ref)
|
||||||
_player.setMetadata(video.name, video.author.name);
|
_player.setMetadata(video.name, video.author.name);
|
||||||
|
|
||||||
@@ -1372,13 +1405,15 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
val toResume = _videoResumePositionMilliseconds;
|
val toResume = _videoResumePositionMilliseconds;
|
||||||
_videoResumePositionMilliseconds = 0;
|
_videoResumePositionMilliseconds = 0;
|
||||||
loadCurrentVideo(toResume);
|
loadCurrentVideo(toResume);
|
||||||
_player.setGestureSoundFactor(1.0f);
|
if (!Settings.instance.gestureControls.useSystemVolume) {
|
||||||
|
_player.setGestureSoundFactor(1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
updateQueueState();
|
updateQueueState();
|
||||||
|
|
||||||
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());
|
||||||
@@ -1651,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1661,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
|
||||||
@@ -1968,14 +2003,14 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
Logger.i(TAG, "fetchPolycentricComments")
|
Logger.i(TAG, "fetchPolycentricComments")
|
||||||
val video = video;
|
val video = video;
|
||||||
val idValue = video?.id?.value
|
val idValue = video?.id?.value
|
||||||
if (idValue == null) {
|
if (video?.url?.isEmpty() != false) {
|
||||||
Logger.w(TAG, "Failed to fetch polycentric comments because id was null")
|
Logger.w(TAG, "Failed to fetch polycentric comments because url was null")
|
||||||
_commentsList.clear()
|
_commentsList.clear()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||||
val extraBytesRef = video.id.value?.toByteArray()
|
val extraBytesRef = idValue?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
||||||
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); };
|
_commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); };
|
||||||
}
|
}
|
||||||
private fun fetchVideo() {
|
private fun fetchVideo() {
|
||||||
@@ -2196,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;
|
||||||
}
|
}
|
||||||
@@ -2240,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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2349,7 +2384,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
else if(isOverlayed) {
|
else if(isOverlayed) {
|
||||||
_playerProgress.layoutParams = _playerProgress.layoutParams.apply {
|
_playerProgress.layoutParams = _playerProgress.layoutParams.apply {
|
||||||
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -6f, resources.displayMetrics).toInt();
|
(this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -2f, resources.displayMetrics).toInt();
|
||||||
};
|
};
|
||||||
_playerProgress.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
_playerProgress.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
||||||
}
|
}
|
||||||
@@ -2550,7 +2585,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
setVideoDetails(videoDetail);
|
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 {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+9
-8
@@ -13,17 +13,17 @@ import android.view.inputmethod.InputMethodManager
|
|||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.stores.SearchHistoryStorage
|
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
|
||||||
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.fragment.mainactivity.main.*
|
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.SuggestionsFragmentData
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.SearchType
|
import com.futo.platformplayer.models.SearchType
|
||||||
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.SearchHistoryStorage
|
||||||
|
|
||||||
class SearchTopBarFragment : TopFragment() {
|
class SearchTopBarFragment : TopFragment() {
|
||||||
private val TAG = "SearchTopBarFragment"
|
private val TAG = "SearchTopBarFragment"
|
||||||
@@ -54,11 +54,12 @@ class SearchTopBarFragment : TopFragment() {
|
|||||||
|
|
||||||
private val _searchDoneListener = object : TextView.OnEditorActionListener {
|
private val _searchDoneListener = object : TextView.OnEditorActionListener {
|
||||||
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
||||||
if (actionId != EditorInfo.IME_ACTION_DONE)
|
val isEnterPress = event?.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN
|
||||||
|
if (actionId != EditorInfo.IME_ACTION_DONE && !isEnterPress)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
onDone();
|
onDone()
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import kotlinx.coroutines.withContext
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.Random
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class StateAnnouncement {
|
class StateAnnouncement {
|
||||||
@@ -252,41 +251,6 @@ class StateAnnouncement {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun registerDidYouKnow() {
|
|
||||||
val random = Random();
|
|
||||||
val message: String? = when (random.nextInt(4 * 18 + 1)) {
|
|
||||||
0 -> "You can login to different platforms and unify your content experience. Check it out in the source settings!"
|
|
||||||
1 -> "Importing your playlists and subscriptions from other platforms to Grayjay is quick and easy. Check it out in the source settings!"
|
|
||||||
2 -> "Want to cast to a big screen? Try out FCast (https://fcast.org/)."
|
|
||||||
3 -> "Explore Grayjay's gesture controls. When in full-screen swipe on the left to change brightness, swipe on the right to change volume."
|
|
||||||
4 -> "Explore Grayjay's gesture controls. Swipe up in the center of a video to toggle full-screen."
|
|
||||||
5 -> "Grayjay's multi-platform search lets you find content from various sources."
|
|
||||||
6 -> "Grayjay's multi-platform search filters will unify filters across platforms. If your expected filters are not there, try toggling some platforms off in the search filters."
|
|
||||||
7 -> "You can share playlists with friends on the playlist page and make full-backups in the settings page."
|
|
||||||
8 -> "Discover Grayjay's offline playback feature. Save content for when you're on the go!"
|
|
||||||
9 -> "Paid content from your favorite creators gets seamlessly integrated into your Grayjay feed. Login to a platform to seamlessly see content you paid for."
|
|
||||||
10 -> "Explore Grayjay's plugin features! Login, import playlists, and tweak plugin settings for a tailored experience."
|
|
||||||
11 -> "Directly engage with content by liking, disliking, or leaving comments on the Polycentric network."
|
|
||||||
12 -> "With Grayjay's rotation lock, you can watch videos in your preferred orientation regardless of device settings. Check it out during playback!"
|
|
||||||
13 -> "Grayjay supports background play. Listen to your favorite content even while multitasking!"
|
|
||||||
14 -> "Use Grayjay's quality selection to adjust video resolution. Save data or watch in high definition – it's up to you."
|
|
||||||
15 -> "Customize your Grayjay experience by changing playback speed. Watch content at your own pace."
|
|
||||||
16 -> "Save time by adding videos to your 'Watch Later' list. Perfect for catching up on content during your free time."
|
|
||||||
17 -> "On Grayjay, your playlists, subscriptions, and settings are stored offline for privacy and quick access."
|
|
||||||
18 -> "Explore and engage with live content using Grayjay's live stream feature."
|
|
||||||
else -> null
|
|
||||||
};
|
|
||||||
|
|
||||||
if (message != null) {
|
|
||||||
registerAnnouncement(
|
|
||||||
"did-you-know?",
|
|
||||||
"Did you know?",
|
|
||||||
message,
|
|
||||||
AnnouncementType.SESSION_RECURRING
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerDefaultHandlerAnnouncement() {
|
fun registerDefaultHandlerAnnouncement() {
|
||||||
registerAnnouncement(
|
registerAnnouncement(
|
||||||
"default-url-handler",
|
"default-url-handler",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.app.Activity
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.graphics.Color
|
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.Network
|
import android.net.Network
|
||||||
@@ -14,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
|
||||||
@@ -42,7 +42,6 @@ import com.futo.platformplayer.stores.v2.ManagedStore
|
|||||||
import com.futo.platformplayer.views.ToastView
|
import com.futo.platformplayer.views.ToastView
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
@@ -55,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))
|
||||||
@@ -331,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()));
|
||||||
}
|
}
|
||||||
@@ -427,8 +429,6 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StateAnnouncement.instance.registerAnnouncement("fa4647d3-36fa-4c8c-832d-85b00fc72dca", "Disclaimer", "This is an early alpha build of the application, expect bugs and unfinished features.", AnnouncementType.DELETABLE, OffsetDateTime.now())
|
|
||||||
|
|
||||||
if(SettingsDev.instance.developerMode && SettingsDev.instance.devServerSettings.devServerOnBoot)
|
if(SettingsDev.instance.developerMode && SettingsDev.instance.devServerSettings.devServerOnBoot)
|
||||||
StateDeveloper.instance.runServer();
|
StateDeveloper.instance.runServer();
|
||||||
|
|
||||||
@@ -477,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));
|
||||||
@@ -557,7 +561,6 @@ class StateApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StateAnnouncement.instance.registerDefaultHandlerAnnouncement();
|
StateAnnouncement.instance.registerDefaultHandlerAnnouncement();
|
||||||
StateAnnouncement.instance.registerDidYouKnow();
|
|
||||||
Logger.i(TAG, "MainApp Started: Finished");
|
Logger.i(TAG, "MainApp Started: Finished");
|
||||||
|
|
||||||
StatePlaylists.instance.toMigrateCheck();
|
StatePlaylists.instance.toMigrateCheck();
|
||||||
@@ -580,24 +583,9 @@ class StateApp {
|
|||||||
null,
|
null,
|
||||||
"Plugin updates available"
|
"Plugin updates available"
|
||||||
));
|
));
|
||||||
|
|
||||||
StateAnnouncement.instance.registerAnnouncement(
|
|
||||||
"plugin-update",
|
|
||||||
"Plugin updates available",
|
|
||||||
"There are ${updateAvailable.size} plugin updates available.",
|
|
||||||
AnnouncementType.SESSION_RECURRING
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
UIDialogs.appToast("This is a test", false);
|
|
||||||
UIDialogs.appToast("This is a test 2", false);
|
|
||||||
UIDialogs.appToastError("This is a test 3 (Error)", false);
|
|
||||||
UIDialogs.appToast(ToastView.Toast("This is a test 4, with title", false, Color.WHITE, "Test title"));
|
|
||||||
UIDialogs.appToast("This is a test 5 Long text\nWith enters\nasdh asfh fds h rwe h fxh sdfh sdf h dsfh sdf hasdfhsdhg ads as", true);
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mainAppStartedWithExternalFiles(context: Context) {
|
fun mainAppStartedWithExternalFiles(context: Context) {
|
||||||
@@ -659,7 +647,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
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -94,11 +95,6 @@ class StatePlatform {
|
|||||||
private val _liveEventClientPool = PlatformMultiClientPool("LiveEvents", 1); //Used exclusively for live events
|
private val _liveEventClientPool = PlatformMultiClientPool("LiveEvents", 1); //Used exclusively for live events
|
||||||
|
|
||||||
|
|
||||||
private val _primaryClientPersistent = FragmentedStorage.get<StringStorage>("primaryClient");
|
|
||||||
private var _primaryClientObj : IPlatformClient? = null;
|
|
||||||
val primaryClient : IPlatformClient get() = _primaryClientObj ?: throw IllegalStateException("PlatformState not yet initialized");
|
|
||||||
|
|
||||||
|
|
||||||
private val _icons : HashMap<String, ImageVariable> = HashMap();
|
private val _icons : HashMap<String, ImageVariable> = HashMap();
|
||||||
|
|
||||||
val hasClients: Boolean get() = _availableClients.size > 0;
|
val hasClients: Boolean get() = _availableClients.size > 0;
|
||||||
@@ -171,8 +167,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();
|
||||||
@@ -207,20 +208,6 @@ class StatePlatform {
|
|||||||
.filter { id -> _availableClients.any { it.id == id } }
|
.filter { id -> _availableClients.any { it.id == id } }
|
||||||
.toTypedArray();
|
.toTypedArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val primary = _primaryClientPersistent.value;
|
|
||||||
if(primary.isEmpty() || primary == StateDeveloper.DEV_ID) {
|
|
||||||
selectPrimaryClient(enabled.firstOrNull() ?: _availableClients.first().id);
|
|
||||||
} else if(!_availableClients.any { it.id == primary }) {
|
|
||||||
selectPrimaryClient(_availableClients.firstOrNull()?.id!!);
|
|
||||||
} else {
|
|
||||||
selectPrimaryClient(primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!enabled.any { it == primaryClient.id }) {
|
|
||||||
enabled = enabled.concat(primaryClient.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
selectClients(*enabled);
|
selectClients(*enabled);
|
||||||
};
|
};
|
||||||
@@ -323,8 +310,6 @@ class StatePlatform {
|
|||||||
newClient.initialize();
|
newClient.initialize();
|
||||||
_enabledClients.add(newClient);
|
_enabledClients.add(newClient);
|
||||||
}
|
}
|
||||||
if (_primaryClientObj == client)
|
|
||||||
_primaryClientObj = newClient;
|
|
||||||
|
|
||||||
_availableClients.removeIf { it.id == id };
|
_availableClients.removeIf { it.id == id };
|
||||||
_availableClients.add(newClient);
|
_availableClients.add(newClient);
|
||||||
@@ -333,6 +318,11 @@ class StatePlatform {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun enableClient(ids: List<String>) {
|
||||||
|
val currentClients = getEnabledClients().map { it.id };
|
||||||
|
selectClients(*(currentClients + ids).distinct().toTypedArray());
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Selects the enabled clients, meaning all clients that data is actively requested from.
|
* Selects the enabled clients, meaning all clients that data is actively requested from.
|
||||||
* If a client is disabled, NO requests are made to said client
|
* If a client is disabled, NO requests are made to said client
|
||||||
@@ -365,17 +355,6 @@ class StatePlatform {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Selects the primary client, meaning the first target for requests.
|
|
||||||
* At the moment, since multi-client requests are not yet implemented, this is the goto client.
|
|
||||||
*/
|
|
||||||
fun selectPrimaryClient(id: String) {
|
|
||||||
synchronized(_clientsLock) {
|
|
||||||
_primaryClientObj = getClient(id);
|
|
||||||
_primaryClientPersistent.setAndSave(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getHome(): IPager<IPlatformContent> {
|
fun getHome(): IPager<IPlatformContent> {
|
||||||
Logger.i(TAG, "Platform - getHome");
|
Logger.i(TAG, "Platform - getHome");
|
||||||
var clientIdsOngoing = mutableListOf<String>();
|
var clientIdsOngoing = mutableListOf<String>();
|
||||||
@@ -448,14 +427,12 @@ class StatePlatform {
|
|||||||
toAwait.map { PlaceholderPager(5, { PlatformContentPlaceholder(it.first.id) }) });
|
toAwait.map { PlaceholderPager(5, { PlatformContentPlaceholder(it.first.id) }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getHomePrimary(): IPager<IPlatformContent> {
|
|
||||||
return primaryClient.getHome();
|
|
||||||
}
|
|
||||||
|
|
||||||
//Search
|
//Search
|
||||||
fun searchSuggestions(query: String): Array<String> {
|
fun searchSuggestions(query: String): Array<String> {
|
||||||
Logger.i(TAG, "Platform - searchSuggestions");
|
Logger.i(TAG, "Platform - searchSuggestions");
|
||||||
return primaryClient.searchSuggestions(query);
|
//TODO: hasSearchSuggestions
|
||||||
|
return getEnabledClients().firstOrNull()?.searchSuggestions(query) ?: arrayOf();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search(query: String, type: String? = null, sort: String? = null, filters: Map<String, List<String>> = mapOf(), clientIds: List<String>? = null): IPager<IPlatformContent> {
|
fun search(query: String, type: String? = null, sort: String? = null, filters: Map<String, List<String>> = mapOf(), clientIds: List<String>? = null): IPager<IPlatformContent> {
|
||||||
@@ -552,12 +529,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();
|
||||||
@@ -567,7 +555,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])) {
|
||||||
@@ -688,8 +676,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);
|
||||||
@@ -809,6 +800,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);
|
||||||
@@ -841,6 +836,7 @@ class StatePlatform {
|
|||||||
return urls;
|
return urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasEnabledPlaylistClient(url: String) : Boolean = getEnabledClients().any { it.isPlaylistUrl(url) };
|
||||||
fun getPlaylistClientOrNull(url: String): IPlatformClient? = getEnabledClients().find { it.isPlaylistUrl(url) }
|
fun getPlaylistClientOrNull(url: String): IPlatformClient? = getEnabledClients().find { it.isPlaylistUrl(url) }
|
||||||
fun getPlaylistClient(url: String): IPlatformClient = getEnabledClients().find { it.isPlaylistUrl(url) }
|
fun getPlaylistClient(url: String): IPlatformClient = getEnabledClients().find { it.isPlaylistUrl(url) }
|
||||||
?: throw NoPlatformClientException("No client enabled that supports this playlist url (${url})");
|
?: throw NoPlatformClientException("No client enabled that supports this playlist url (${url})");
|
||||||
@@ -886,7 +882,6 @@ class StatePlatform {
|
|||||||
synchronized(_clientsLock) {
|
synchronized(_clientsLock) {
|
||||||
val enabledExisting = _enabledClients.filter { it is DevJSClient };
|
val enabledExisting = _enabledClients.filter { it is DevJSClient };
|
||||||
val isEnabled = !enabledExisting.isEmpty()
|
val isEnabled = !enabledExisting.isEmpty()
|
||||||
val isPrimary = _primaryClientObj is DevJSClient;
|
|
||||||
|
|
||||||
for (enabled in enabledExisting) {
|
for (enabled in enabledExisting) {
|
||||||
enabled.disable();
|
enabled.disable();
|
||||||
@@ -901,11 +896,7 @@ class StatePlatform {
|
|||||||
devId = newClient.devID;
|
devId = newClient.devID;
|
||||||
try {
|
try {
|
||||||
StateDeveloper.instance.initializeDev(devId!!);
|
StateDeveloper.instance.initializeDev(devId!!);
|
||||||
if (isPrimary) {
|
if (isEnabled) {
|
||||||
_primaryClientObj = newClient;
|
|
||||||
_enabledClients.add(0, newClient);
|
|
||||||
newClient.initialize();
|
|
||||||
} else if (isEnabled) {
|
|
||||||
_enabledClients.add(newClient);
|
_enabledClients.add(newClient);
|
||||||
newClient.initialize();
|
newClient.initialize();
|
||||||
}
|
}
|
||||||
@@ -944,7 +935,7 @@ class StatePlatform {
|
|||||||
suspend fun checkForUpdates(): List<SourcePluginConfig> = withContext(Dispatchers.IO) {
|
suspend fun checkForUpdates(): List<SourcePluginConfig> = withContext(Dispatchers.IO) {
|
||||||
var configs = mutableListOf<SourcePluginConfig>()
|
var configs = mutableListOf<SourcePluginConfig>()
|
||||||
val updatesAvailableFor = hashSetOf<String>()
|
val updatesAvailableFor = hashSetOf<String>()
|
||||||
for (availableClient in getAvailableClients()) {
|
for (availableClient in getAvailableClients().filter { it is JSClient && it.descriptor.appSettings.checkForUpdates }) {
|
||||||
if (availableClient !is JSClient) {
|
if (availableClient !is JSClient) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -361,6 +362,12 @@ class StatePlayer {
|
|||||||
if (queueShuffle) {
|
if (queueShuffle) {
|
||||||
removeFromShuffledQueue(video);
|
removeFromShuffledQueue(video);
|
||||||
}
|
}
|
||||||
|
if(currentVideo != null) {
|
||||||
|
val newPos = _queue.indexOfFirst { it.url == currentVideo?.url };
|
||||||
|
if(newPos >= 0)
|
||||||
|
_queuePosition = newPos;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onQueueChanged.emit(shouldSwapCurrentItem);
|
onQueueChanged.emit(shouldSwapCurrentItem);
|
||||||
@@ -407,6 +414,12 @@ class StatePlayer {
|
|||||||
if(_queue.size == 1) {
|
if(_queue.size == 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if(_queue.size <= _queuePosition && currentVideo != null) {
|
||||||
|
//Out of sync position
|
||||||
|
val newPos = _queue.indexOfFirst { it.url == currentVideo?.url }
|
||||||
|
if(newPos != -1)
|
||||||
|
_queuePosition = newPos;
|
||||||
|
}
|
||||||
|
|
||||||
val shuffledQueue = _queueShuffled;
|
val shuffledQueue = _queueShuffled;
|
||||||
val queue = if (queueShuffle && shuffledQueue != null) {
|
val queue = if (queueShuffle && shuffledQueue != null) {
|
||||||
@@ -421,6 +434,8 @@ class StatePlayer {
|
|||||||
}
|
}
|
||||||
//Standard Behavior
|
//Standard Behavior
|
||||||
if(_queuePosition - 1 >= 0) {
|
if(_queuePosition - 1 >= 0) {
|
||||||
|
if(queue.size <= _queuePosition)
|
||||||
|
return null;
|
||||||
return queue[_queuePosition - 1];
|
return queue[_queuePosition - 1];
|
||||||
}
|
}
|
||||||
//Repeat Behavior (End of queue)
|
//Repeat Behavior (End of queue)
|
||||||
@@ -619,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,11 +11,14 @@ 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.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import com.futo.platformplayer.stores.v2.ReconstructStore
|
import com.futo.platformplayer.stores.v2.ReconstructStore
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
@@ -31,10 +34,14 @@ 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..
|
||||||
|
|
||||||
val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists")
|
val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists")
|
||||||
.withRestore(PlaylistBackup())
|
.withRestore(PlaylistBackup())
|
||||||
.load();
|
.load();
|
||||||
@@ -48,28 +55,44 @@ class StatePlaylists {
|
|||||||
}
|
}
|
||||||
fun getWatchLater() : List<SerializedPlatformVideo> {
|
fun getWatchLater() : List<SerializedPlatformVideo> {
|
||||||
synchronized(_watchlistStore) {
|
synchronized(_watchlistStore) {
|
||||||
return _watchlistStore.getItems();
|
val order = _watchlistOrderStore.getAllValues();
|
||||||
|
return _watchlistStore.getItems().sortedBy { order.indexOf(it.url) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun updateWatchLater(updated: List<SerializedPlatformVideo>) {
|
fun updateWatchLater(updated: List<SerializedPlatformVideo>) {
|
||||||
synchronized(_watchlistStore) {
|
synchronized(_watchlistStore) {
|
||||||
_watchlistStore.deleteAll();
|
_watchlistStore.deleteAll();
|
||||||
_watchlistStore.saveAllAsync(updated);
|
_watchlistStore.saveAllAsync(updated);
|
||||||
|
_watchlistOrderStore.set(*updated.map { it.url }.toTypedArray());
|
||||||
|
_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) {
|
||||||
_watchlistStore.delete(video);
|
_watchlistStore.delete(video);
|
||||||
|
_watchlistOrderStore.set(*_watchlistOrderStore.values.filter { it != video.url }.toTypedArray());
|
||||||
|
_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) {
|
||||||
_watchlistStore.saveAsync(video);
|
_watchlistStore.saveAsync(video);
|
||||||
|
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values) .toTypedArray());
|
||||||
|
_watchlistOrderStore.save();
|
||||||
}
|
}
|
||||||
onWatchLaterChanged.emit();
|
onWatchLaterChanged.emit();
|
||||||
|
|
||||||
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLastPlayedPlaylist() : Playlist? {
|
fun getLastPlayedPlaylist() : Playlist? {
|
||||||
@@ -119,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) {
|
||||||
@@ -131,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 {
|
||||||
@@ -145,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);
|
||||||
}
|
}
|
||||||
@@ -176,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}");
|
||||||
@@ -185,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,8 +134,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) {
|
||||||
@@ -160,6 +163,13 @@ class StatePlugins {
|
|||||||
val configJson = StateAssets.readAsset(context, assetConfigPath) ?: return null;
|
val configJson = StateAssets.readAsset(context, assetConfigPath) ?: return null;
|
||||||
return SourcePluginConfig.fromJson(configJson, "");
|
return SourcePluginConfig.fromJson(configJson, "");
|
||||||
}
|
}
|
||||||
|
fun getEmbeddedPluginConfigFromID(context: Context, pluginId: String): SourcePluginConfig? {
|
||||||
|
val embedded = getEmbeddedSources(context);
|
||||||
|
if(!embedded.containsKey(pluginId))
|
||||||
|
return null;
|
||||||
|
return getEmbeddedPluginConfig(context, embedded[pluginId]!!);
|
||||||
|
}
|
||||||
|
|
||||||
fun installEmbeddedPlugin(context: Context, assetConfigPath: String, id: String? = null): Boolean {
|
fun installEmbeddedPlugin(context: Context, assetConfigPath: String, id: String? = null): Boolean {
|
||||||
try {
|
try {
|
||||||
val configJson = StateAssets.readAsset(context, assetConfigPath) ?:
|
val configJson = StateAssets.readAsset(context, assetConfigPath) ?:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -48,6 +49,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
|
import userpackage.Protocol.Reference
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
@@ -66,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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,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?) {
|
||||||
@@ -287,7 +326,8 @@ class StatePolycentric {
|
|||||||
rating = RatingLikeDislikes(0, 0),
|
rating = RatingLikeDislikes(0, 0),
|
||||||
date = if (ev.unixMilliseconds != null) Instant.ofEpochMilli(ev.unixMilliseconds!!).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
date = if (ev.unixMilliseconds != null) Instant.ofEpochMilli(ev.unixMilliseconds!!).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
||||||
replyCount = 0,
|
replyCount = 0,
|
||||||
eventPointer = se.toPointer()
|
eventPointer = se.toPointer(),
|
||||||
|
parentReference = se.event.references.getOrNull(0)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,6 +368,77 @@ class StatePolycentric {
|
|||||||
return LikesDislikesReplies(likes, dislikes, replyCount)
|
return LikesDislikesReplies(likes, dislikes, replyCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getComment(contextUrl: String, reference: Reference): PolycentricPlatformComment {
|
||||||
|
ensureEnabled()
|
||||||
|
|
||||||
|
if (reference.referenceType != 2L) {
|
||||||
|
throw Exception("Not a pointer")
|
||||||
|
}
|
||||||
|
|
||||||
|
val pointer = Protocol.Pointer.parseFrom(reference.reference)
|
||||||
|
val events = ApiMethods.getEvents(PolycentricCache.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder()
|
||||||
|
.addRangesForProcesses(Protocol.RangesForProcess.newBuilder()
|
||||||
|
.setProcess(pointer.process)
|
||||||
|
.addRanges(Protocol.Range.newBuilder()
|
||||||
|
.setLow(pointer.logicalClock)
|
||||||
|
.setHigh(pointer.logicalClock)
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
|
||||||
|
val sev = SignedEvent.fromProto(events.getEvents(0))
|
||||||
|
val ev = sev.event
|
||||||
|
|
||||||
|
if (ev.contentType != ContentType.POST.value) {
|
||||||
|
throw Exception("This is not a comment")
|
||||||
|
}
|
||||||
|
|
||||||
|
val post = Protocol.Post.parseFrom(ev.content);
|
||||||
|
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
|
||||||
|
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
||||||
|
|
||||||
|
val profileEvents = ApiMethods.getQueryLatest(
|
||||||
|
PolycentricCache.SERVER,
|
||||||
|
ev.system.toProto(),
|
||||||
|
listOf(
|
||||||
|
ContentType.AVATAR.value,
|
||||||
|
ContentType.USERNAME.value
|
||||||
|
)
|
||||||
|
).eventsList.map { e -> SignedEvent.fromProto(e) }.groupBy { e -> e.event.contentType }
|
||||||
|
.map { (_, events) -> events.maxBy { x -> x.event.unixMilliseconds ?: 0 } };
|
||||||
|
|
||||||
|
val nameEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.USERNAME.value };
|
||||||
|
val avatarEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.AVATAR.value };
|
||||||
|
val imageBundle = if (avatarEvent != null) {
|
||||||
|
val lwwElementValue = avatarEvent.event.lwwElement?.value;
|
||||||
|
if (lwwElementValue != null) {
|
||||||
|
Protocol.ImageBundle.parseFrom(lwwElementValue)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val ldr = getLikesDislikesReplies(reference)
|
||||||
|
return PolycentricPlatformComment(
|
||||||
|
contextUrl = contextUrl,
|
||||||
|
author = PlatformAuthorLink(
|
||||||
|
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||||
|
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
||||||
|
url = systemLinkUrl,
|
||||||
|
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
||||||
|
subscribers = null
|
||||||
|
),
|
||||||
|
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||||
|
rating = RatingLikeDislikes(ldr.likes, ldr.dislikes),
|
||||||
|
date = if (ev.unixMilliseconds != null) Instant.ofEpochMilli(ev.unixMilliseconds!!).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
||||||
|
replyCount = ldr.replyCount.toInt(),
|
||||||
|
eventPointer = sev.toPointer(),
|
||||||
|
parentReference = sev.event.references.getOrNull(0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
|
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return EmptyPager()
|
return EmptyPager()
|
||||||
@@ -453,7 +564,8 @@ class StatePolycentric {
|
|||||||
rating = RatingLikeDislikes(likes, dislikes),
|
rating = RatingLikeDislikes(likes, dislikes),
|
||||||
date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
||||||
replyCount = replies.toInt(),
|
replyCount = replies.toInt(),
|
||||||
eventPointer = sev.toPointer()
|
eventPointer = sev.toPointer(),
|
||||||
|
parentReference = sev.event.references.getOrNull(0)
|
||||||
);
|
);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
return@mapNotNull null;
|
return@mapNotNull null;
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+34
-8
@@ -13,6 +13,7 @@ import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
|||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
||||||
|
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||||
import com.futo.platformplayer.exceptions.ChannelException
|
import com.futo.platformplayer.exceptions.ChannelException
|
||||||
import com.futo.platformplayer.findNonRuntimeException
|
import com.futo.platformplayer.findNonRuntimeException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
|
||||||
@@ -23,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
|
||||||
@@ -47,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)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +73,6 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
val cachedChannels = mutableListOf<String>()
|
val cachedChannels = mutableListOf<String>()
|
||||||
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
|
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
|
||||||
|
|
||||||
|
|
||||||
val taskResults = arrayListOf<SubscriptionTaskResult>();
|
val taskResults = arrayListOf<SubscriptionTaskResult>();
|
||||||
val timeTotal = measureTimeMillis {
|
val timeTotal = measureTimeMillis {
|
||||||
for(task in forkTasks) {
|
for(task in forkTasks) {
|
||||||
@@ -126,7 +129,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
val pager = MultiChronoContentPager(groupedPagers, allowFailure, 15);
|
val pager = MultiChronoContentPager(groupedPagers, allowFailure, 15);
|
||||||
pager.initialize();
|
pager.initialize();
|
||||||
|
|
||||||
return Result(DedupContentPager(pager), exs);
|
return Result(DedupContentPager(pager, StatePlatform.instance.getEnabledClients().map { it.id }), exs);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun executeSubscriptionTasks(tasks: List<SubscriptionTask>, failedPlugins: MutableList<String>, cachedChannels: MutableList<String>): List<ForkJoinTask<SubscriptionTaskResult>> {
|
fun executeSubscriptionTasks(tasks: List<SubscriptionTask>, failedPlugins: MutableList<String>, cachedChannels: MutableList<String>): List<ForkJoinTask<SubscriptionTaskResult>> {
|
||||||
@@ -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)) {
|
||||||
@@ -200,7 +225,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
else {
|
else {
|
||||||
Logger.i(StateSubscriptions.TAG, "Channel ${task.sub.channel.name} failed, substituting with cache");
|
Logger.i(StateSubscriptions.TAG, "Channel ${task.sub.channel.name} failed, substituting with cache");
|
||||||
pager = StateCache.instance.getChannelCachePager(task.sub.channel.url);
|
pager = StateCache.instance.getChannelCachePager(task.sub.channel.url);
|
||||||
taskEx = ex;
|
taskEx = channelEx;
|
||||||
return@submit SubscriptionTaskResult(task, pager, taskEx);
|
return@submit SubscriptionTaskResult(task, pager, taskEx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
+17
-2
@@ -53,8 +53,10 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
|||||||
hideLikesDislikesReplies()
|
hideLikesDislikesReplies()
|
||||||
}
|
}
|
||||||
|
|
||||||
var onRepliesClick = Event1<IPlatformComment>();
|
val onRepliesClick = Event1<IPlatformComment>();
|
||||||
var onDelete = Event1<IPlatformComment>();
|
val onDelete = Event1<IPlatformComment>();
|
||||||
|
val onClick = Event1<IPlatformComment>();
|
||||||
|
val onAuthorClick = Event1<IPlatformComment>();
|
||||||
var comment: IPlatformComment? = null
|
var comment: IPlatformComment? = null
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
@@ -98,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);
|
||||||
@@ -108,6 +118,11 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
|||||||
onDelete.emit(c);
|
onDelete.emit(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_layoutComment.setOnClickListener {
|
||||||
|
val c = comment ?: return@setOnClickListener;
|
||||||
|
onClick.emit(c);
|
||||||
|
}
|
||||||
|
|
||||||
_textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context);
|
_textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -61,6 +61,7 @@ open class PreviewVideoView : LinearLayout {
|
|||||||
protected val _layoutDownloaded: FrameLayout;
|
protected val _layoutDownloaded: FrameLayout;
|
||||||
|
|
||||||
protected val _button_add_to_queue : View;
|
protected val _button_add_to_queue : View;
|
||||||
|
protected val _button_add_to_watch_later : View;
|
||||||
protected val _button_add_to : View;
|
protected val _button_add_to : View;
|
||||||
|
|
||||||
protected val _exoPlayer: PlayerManager?;
|
protected val _exoPlayer: PlayerManager?;
|
||||||
@@ -80,6 +81,7 @@ open class PreviewVideoView : LinearLayout {
|
|||||||
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>();
|
||||||
|
|
||||||
var currentVideo: IPlatformVideo? = null
|
var currentVideo: IPlatformVideo? = null
|
||||||
private set
|
private set
|
||||||
@@ -104,6 +106,7 @@ open class PreviewVideoView : LinearLayout {
|
|||||||
_containerDuration = findViewById(R.id.thumbnail_duration_container);
|
_containerDuration = findViewById(R.id.thumbnail_duration_container);
|
||||||
_containerLive = findViewById(R.id.thumbnail_live_container);
|
_containerLive = findViewById(R.id.thumbnail_live_container);
|
||||||
_button_add_to_queue = findViewById(R.id.button_add_to_queue);
|
_button_add_to_queue = findViewById(R.id.button_add_to_queue);
|
||||||
|
_button_add_to_watch_later = findViewById(R.id.button_add_to_watch_later);
|
||||||
_button_add_to = findViewById(R.id.button_add_to);
|
_button_add_to = findViewById(R.id.button_add_to);
|
||||||
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
|
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
|
||||||
_layoutDownloaded = findViewById(R.id.layout_downloaded);
|
_layoutDownloaded = findViewById(R.id.layout_downloaded);
|
||||||
@@ -124,7 +127,7 @@ open class PreviewVideoView : LinearLayout {
|
|||||||
_textVideoMetadata.setOnClickListener { currentVideo?.let { onChannelClicked.emit(it.author) } };
|
_textVideoMetadata.setOnClickListener { currentVideo?.let { onChannelClicked.emit(it.author) } };
|
||||||
_button_add_to.setOnClickListener { currentVideo?.let { onAddToClicked.emit(it) } };
|
_button_add_to.setOnClickListener { currentVideo?.let { onAddToClicked.emit(it) } };
|
||||||
_button_add_to_queue.setOnClickListener { currentVideo?.let { onAddToQueueClicked.emit(it) } };
|
_button_add_to_queue.setOnClickListener { currentVideo?.let { onAddToQueueClicked.emit(it) } };
|
||||||
|
_button_add_to_watch_later.setOnClickListener { currentVideo?.let { onAddToWatchLaterClicked.emit(it); } }
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun inflate(feedStyle: FeedStyle) {
|
protected open fun inflate(feedStyle: FeedStyle) {
|
||||||
|
|||||||
+2
@@ -18,6 +18,7 @@ class PreviewVideoViewHolder : 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>();
|
||||||
val onLongPress = Event1<IPlatformVideo>();
|
val onLongPress = Event1<IPlatformVideo>();
|
||||||
|
|
||||||
//val context: Context;
|
//val context: Context;
|
||||||
@@ -34,6 +35,7 @@ class PreviewVideoViewHolder : 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);
|
||||||
view.onLongPress.subscribe(onLongPress::emit);
|
view.onLongPress.subscribe(onLongPress::emit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import android.animation.Animator
|
|||||||
import android.animation.AnimatorSet
|
import android.animation.AnimatorSet
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Matrix
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.media.AudioManager
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.util.Log
|
||||||
import android.view.GestureDetector
|
import android.view.GestureDetector
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
|
import android.view.ScaleGestureDetector
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
@@ -19,8 +23,11 @@ import androidx.core.animation.doOnEnd
|
|||||||
import androidx.core.animation.doOnStart
|
import androidx.core.animation.doOnStart
|
||||||
import androidx.core.view.GestureDetectorCompat
|
import androidx.core.view.GestureDetectorCompat
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
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.constructs.Event2
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.views.others.CircularProgressBar
|
import com.futo.platformplayer.views.others.CircularProgressBar
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
@@ -33,6 +40,7 @@ import kotlinx.coroutines.ensureActive
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
class GestureControlView : LinearLayout {
|
class GestureControlView : LinearLayout {
|
||||||
private val _scope = CoroutineScope(Dispatchers.Main);
|
private val _scope = CoroutineScope(Dispatchers.Main);
|
||||||
private val _imageFastForward: ImageView;
|
private val _imageFastForward: ImageView;
|
||||||
@@ -59,6 +67,8 @@ class GestureControlView : LinearLayout {
|
|||||||
private val _progressSound: CircularProgressBar;
|
private val _progressSound: CircularProgressBar;
|
||||||
private var _animatorSound: ObjectAnimator? = null;
|
private var _animatorSound: ObjectAnimator? = null;
|
||||||
private var _brightnessFactor = 1.0f;
|
private var _brightnessFactor = 1.0f;
|
||||||
|
private var _originalBrightnessFactor = 1.0f;
|
||||||
|
private var _originalBrightnessMode: Int = 0;
|
||||||
private var _adjustingBrightness: Boolean = false;
|
private var _adjustingBrightness: Boolean = false;
|
||||||
private val _layoutControlsBrightness: FrameLayout;
|
private val _layoutControlsBrightness: FrameLayout;
|
||||||
private val _progressBrightness: CircularProgressBar;
|
private val _progressBrightness: CircularProgressBar;
|
||||||
@@ -70,10 +80,27 @@ class GestureControlView : LinearLayout {
|
|||||||
private var _fullScreenFactorUp = 1.0f;
|
private var _fullScreenFactorUp = 1.0f;
|
||||||
private var _fullScreenFactorDown = 1.0f;
|
private var _fullScreenFactorDown = 1.0f;
|
||||||
|
|
||||||
|
private var _scaleGestureDetector: ScaleGestureDetector
|
||||||
|
private var _scaleFactor = 1.0f
|
||||||
|
private var _translationX = 0.0f
|
||||||
|
private var _translationY = 0.0f
|
||||||
|
private val _layoutControlsZoom: FrameLayout
|
||||||
|
private val _textZoom: TextView
|
||||||
|
private var _isZooming = false
|
||||||
|
private var _isPanning = false
|
||||||
|
private var _isZoomPanEnabled = false
|
||||||
|
private var _surfaceView: View? = null
|
||||||
|
private var _layoutIndicatorFill: FrameLayout;
|
||||||
|
private var _layoutIndicatorFit: FrameLayout;
|
||||||
|
|
||||||
private val _gestureController: GestureDetectorCompat;
|
private val _gestureController: GestureDetectorCompat;
|
||||||
|
|
||||||
|
val isUserGesturing get() = _rewinding || _skipping || _adjustingBrightness || _adjustingSound || _adjustingFullscreenUp || _adjustingFullscreenDown || _isPanning || _isZooming;
|
||||||
|
|
||||||
val onSeek = Event1<Long>();
|
val onSeek = Event1<Long>();
|
||||||
val onBrightnessAdjusted = Event1<Float>();
|
val onBrightnessAdjusted = Event1<Float>();
|
||||||
|
val onPan = Event2<Float, Float>();
|
||||||
|
val onZoom = Event1<Float>();
|
||||||
val onSoundAdjusted = Event1<Float>();
|
val onSoundAdjusted = Event1<Float>();
|
||||||
val onToggleFullscreen = Event0();
|
val onToggleFullscreen = Event0();
|
||||||
|
|
||||||
@@ -91,8 +118,48 @@ class GestureControlView : LinearLayout {
|
|||||||
_layoutControlsSound = findViewById(R.id.layout_controls_sound);
|
_layoutControlsSound = findViewById(R.id.layout_controls_sound);
|
||||||
_progressSound = findViewById(R.id.progress_sound);
|
_progressSound = findViewById(R.id.progress_sound);
|
||||||
_layoutControlsBrightness = findViewById(R.id.layout_controls_brightness);
|
_layoutControlsBrightness = findViewById(R.id.layout_controls_brightness);
|
||||||
|
_layoutControlsZoom = findViewById(R.id.layout_controls_zoom)
|
||||||
|
_textZoom = findViewById(R.id.text_zoom)
|
||||||
_progressBrightness = findViewById(R.id.progress_brightness);
|
_progressBrightness = findViewById(R.id.progress_brightness);
|
||||||
_layoutControlsFullscreen = findViewById(R.id.layout_controls_fullscreen);
|
_layoutControlsFullscreen = findViewById(R.id.layout_controls_fullscreen);
|
||||||
|
_layoutIndicatorFill = findViewById(R.id.layout_indicator_fill);
|
||||||
|
_layoutIndicatorFit = findViewById(R.id.layout_indicator_fit);
|
||||||
|
|
||||||
|
_scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||||
|
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||||
|
if (!_isZoomPanEnabled || !_isFullScreen || !Settings.instance.gestureControls.zoom) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val newScaleFactor = (_scaleFactor * detector.scaleFactor).coerceAtLeast(1.0f).coerceAtMost(10.0f)
|
||||||
|
val scaleFactorChange = newScaleFactor / _scaleFactor
|
||||||
|
_scaleFactor = newScaleFactor
|
||||||
|
onZoom.emit(_scaleFactor)
|
||||||
|
|
||||||
|
val sx = detector.focusX
|
||||||
|
val sy = detector.focusY
|
||||||
|
|
||||||
|
val tx = _translationX + width / 2.0f
|
||||||
|
val ty = _translationY + height / 2.0f
|
||||||
|
|
||||||
|
val matrix = Matrix()
|
||||||
|
matrix.postTranslate(-sx, -sy)
|
||||||
|
matrix.postScale(scaleFactorChange, scaleFactorChange)
|
||||||
|
matrix.postTranslate(sx, sy)
|
||||||
|
|
||||||
|
val point = floatArrayOf(tx, ty)
|
||||||
|
matrix.mapPoints(point)
|
||||||
|
pan(point[0] - width / 2.0f, point[1] - height / 2.0f)
|
||||||
|
|
||||||
|
_layoutControlsZoom.visibility = View.VISIBLE
|
||||||
|
_textZoom.text = "${String.format("%.1f", _scaleFactor)}x"
|
||||||
|
_isZooming = true
|
||||||
|
|
||||||
|
updateSnappingVisibility()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
_gestureController = GestureDetectorCompat(context, object : GestureDetector.OnGestureListener {
|
_gestureController = GestureDetectorCompat(context, object : GestureDetector.OnGestureListener {
|
||||||
override fun onDown(p0: MotionEvent): Boolean { return false; }
|
override fun onDown(p0: MotionEvent): Boolean { return false; }
|
||||||
@@ -103,41 +170,48 @@ class GestureControlView : LinearLayout {
|
|||||||
if(p0 == null)
|
if(p0 == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
val minDistance = Math.min(width, height)
|
if (!_isPanning && p1.pointerCount == 1) {
|
||||||
if (_isFullScreen && _adjustingBrightness) {
|
val minDistance = Math.min(width, height)
|
||||||
val adjustAmount = (distanceY * 2) / minDistance;
|
if (_isFullScreen && _adjustingBrightness) {
|
||||||
_brightnessFactor = (_brightnessFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
val adjustAmount = (distanceY * 2) / minDistance;
|
||||||
_progressBrightness.progress = _brightnessFactor;
|
_brightnessFactor = (_brightnessFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||||
onBrightnessAdjusted.emit(_brightnessFactor);
|
_progressBrightness.progress = _brightnessFactor;
|
||||||
} else if (_isFullScreen && _adjustingSound) {
|
onBrightnessAdjusted.emit(_brightnessFactor);
|
||||||
val adjustAmount = (distanceY * 2) / minDistance;
|
} else if (_isFullScreen && _adjustingSound) {
|
||||||
_soundFactor = (_soundFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
val adjustAmount = (distanceY * 2) / minDistance;
|
||||||
_progressSound.progress = _soundFactor;
|
_soundFactor = (_soundFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||||
onSoundAdjusted.emit(_soundFactor);
|
_progressSound.progress = _soundFactor;
|
||||||
} else if (_adjustingFullscreenUp) {
|
onSoundAdjusted.emit(_soundFactor);
|
||||||
val adjustAmount = (distanceY * 2) / minDistance;
|
} else if (_adjustingFullscreenUp) {
|
||||||
_fullScreenFactorUp = (_fullScreenFactorUp + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
val adjustAmount = (distanceY * 2) / minDistance;
|
||||||
_layoutControlsFullscreen.alpha = _fullScreenFactorUp;
|
_fullScreenFactorUp = (_fullScreenFactorUp + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||||
} else if (_adjustingFullscreenDown) {
|
_layoutControlsFullscreen.alpha = _fullScreenFactorUp;
|
||||||
val adjustAmount = (-distanceY * 2) / minDistance;
|
} else if (_adjustingFullscreenDown) {
|
||||||
_fullScreenFactorDown = (_fullScreenFactorDown + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
val adjustAmount = (-distanceY * 2) / minDistance;
|
||||||
_layoutControlsFullscreen.alpha = _fullScreenFactorDown;
|
_fullScreenFactorDown = (_fullScreenFactorDown + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||||
} else {
|
_layoutControlsFullscreen.alpha = _fullScreenFactorDown;
|
||||||
val rx = (p0.x + p1.x) / (2 * width);
|
} else if (p0.pointerCount == 1) {
|
||||||
val ry = (p0.y + p1.y) / (2 * height);
|
val rx = (p0.x + p1.x) / (2 * width);
|
||||||
if (ry > 0.1 && ry < 0.9) {
|
val ry = (p0.y + p1.y) / (2 * height);
|
||||||
if (_isFullScreen && rx < 0.2) {
|
if (ry > 0.1 && ry < 0.9) {
|
||||||
startAdjustingBrightness();
|
if (Settings.instance.gestureControls.brightnessSlider && _isFullScreen && rx < 0.2) {
|
||||||
} else if (_isFullScreen && rx > 0.8) {
|
startAdjustingBrightness();
|
||||||
startAdjustingSound();
|
} else if (Settings.instance.gestureControls.volumeSlider && _isFullScreen && rx > 0.8) {
|
||||||
} else if (fullScreenGestureEnabled && rx in 0.3..0.7) {
|
startAdjustingSound();
|
||||||
if (_isFullScreen) {
|
} else if (Settings.instance.gestureControls.toggleFullscreen && fullScreenGestureEnabled && rx in 0.3..0.7) {
|
||||||
startAdjustingFullscreenDown();
|
if (_isFullScreen) {
|
||||||
} else {
|
startAdjustingFullscreenDown();
|
||||||
startAdjustingFullscreenUp();
|
} else {
|
||||||
|
startAdjustingFullscreenUp();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (_isZoomPanEnabled && _isFullScreen && !_isZooming && Settings.instance.gestureControls.pan) {
|
||||||
|
_isPanning = true
|
||||||
|
stopAllGestures()
|
||||||
|
updateSnappingVisibility()
|
||||||
|
pan(_translationX - distanceX, _translationY - distanceY)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -178,6 +252,55 @@ class GestureControlView : LinearLayout {
|
|||||||
isClickable = true
|
isClickable = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateSnappingVisibility() {
|
||||||
|
if (willSnapFill()) {
|
||||||
|
_layoutIndicatorFill.visibility = View.VISIBLE
|
||||||
|
_layoutIndicatorFit.visibility = View.GONE
|
||||||
|
} else if (willSnapFit()) {
|
||||||
|
_layoutIndicatorFill.visibility = View.GONE
|
||||||
|
_layoutIndicatorFit.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
_surfaceView?.let {
|
||||||
|
val lp = _layoutIndicatorFit.layoutParams
|
||||||
|
lp.width = it.width
|
||||||
|
lp.height = it.height
|
||||||
|
_layoutIndicatorFit.layoutParams = lp
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_layoutIndicatorFill.visibility = View.GONE
|
||||||
|
_layoutIndicatorFit.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setZoomPanEnabled(view: View) {
|
||||||
|
_isZoomPanEnabled = true
|
||||||
|
_surfaceView = view
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetZoomPan() {
|
||||||
|
_scaleFactor = 1.0f
|
||||||
|
onZoom.emit(_scaleFactor)
|
||||||
|
_translationX = 0f
|
||||||
|
_translationY = 0f
|
||||||
|
onPan.emit(_translationX, _translationY)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pan(translationX: Float, translationY: Float) {
|
||||||
|
val xc = width / 2.0f
|
||||||
|
val yc = height / 2.0f
|
||||||
|
|
||||||
|
val xmin = xc - width / 2.0f * _scaleFactor
|
||||||
|
val xmax = xc + width / 2.0f * _scaleFactor - width
|
||||||
|
|
||||||
|
val ymin = yc - height / 2.0f * _scaleFactor
|
||||||
|
val ymax = yc + height / 2.0f * _scaleFactor - height
|
||||||
|
|
||||||
|
_translationX = translationX.coerceAtLeast(xmin).coerceAtMost(xmax)
|
||||||
|
_translationY = translationY.coerceAtLeast(ymin).coerceAtMost(ymax)
|
||||||
|
|
||||||
|
onPan.emit(_translationX, _translationY)
|
||||||
|
}
|
||||||
|
|
||||||
fun setupTouchArea(layoutControls: ViewGroup? = null, background: View? = null) {
|
fun setupTouchArea(layoutControls: ViewGroup? = null, background: View? = null) {
|
||||||
_layoutControls = layoutControls;
|
_layoutControls = layoutControls;
|
||||||
_background = background;
|
_background = background;
|
||||||
@@ -223,12 +346,67 @@ class GestureControlView : LinearLayout {
|
|||||||
stopAdjustingFullscreenDown();
|
stopAdjustingFullscreenDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((_isPanning || _isZooming) && ev.action == MotionEvent.ACTION_UP) {
|
||||||
|
val surfaceView = _surfaceView
|
||||||
|
if (surfaceView != null && willSnapFill()) {
|
||||||
|
_scaleFactor = calculateZoomScaleFactor()
|
||||||
|
onZoom.emit(_scaleFactor)
|
||||||
|
|
||||||
|
_translationX = 0f
|
||||||
|
_translationY = 0f
|
||||||
|
onPan.emit(_translationX, _translationY)
|
||||||
|
} else if (willSnapFit()) {
|
||||||
|
_scaleFactor = 1f
|
||||||
|
onZoom.emit(_scaleFactor)
|
||||||
|
|
||||||
|
_translationX = 0f
|
||||||
|
_translationY = 0f
|
||||||
|
onPan.emit(_translationX, _translationY)
|
||||||
|
}
|
||||||
|
|
||||||
|
_layoutControlsZoom.visibility = View.GONE
|
||||||
|
_layoutIndicatorFill.visibility = View.GONE
|
||||||
|
_layoutIndicatorFit.visibility = View.GONE
|
||||||
|
_isZooming = false
|
||||||
|
_isPanning = false
|
||||||
|
}
|
||||||
|
|
||||||
startHideJobIfNecessary();
|
startHideJobIfNecessary();
|
||||||
|
|
||||||
_gestureController.onTouchEvent(ev)
|
_gestureController.onTouchEvent(ev)
|
||||||
|
_scaleGestureDetector.onTouchEvent(ev)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun calculateZoomScaleFactor(): Float {
|
||||||
|
val w = _surfaceView?.width?.toFloat() ?: return 1.0f;
|
||||||
|
val h = _surfaceView?.height?.toFloat() ?: return 1.0f;
|
||||||
|
if (w == 0.0f || h == 0.0f) {
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(width / w, height / h)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _snapTranslationTolerance = 0.1f;
|
||||||
|
private val _snapZoomTolerance = 0.1f;
|
||||||
|
|
||||||
|
private fun willSnapFill(): Boolean {
|
||||||
|
val surfaceView = _surfaceView
|
||||||
|
if (surfaceView != null) {
|
||||||
|
val zoomScaleFactor = calculateZoomScaleFactor()
|
||||||
|
if (Math.abs(_scaleFactor - zoomScaleFactor) < _snapZoomTolerance && Math.abs(_translationX) / width < _snapTranslationTolerance && Math.abs(_translationY) / height < _snapTranslationTolerance) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun willSnapFit(): Boolean {
|
||||||
|
return Math.abs(_scaleFactor - 1.0f) < _snapZoomTolerance && Math.abs(_translationX) / width < _snapTranslationTolerance && Math.abs(_translationY) / height < _snapTranslationTolerance
|
||||||
|
}
|
||||||
|
|
||||||
fun cancelHideJob() {
|
fun cancelHideJob() {
|
||||||
_jobHideControls?.cancel();
|
_jobHideControls?.cancel();
|
||||||
_jobHideControls = null;
|
_jobHideControls = null;
|
||||||
@@ -558,11 +736,56 @@ class GestureControlView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setFullscreen(isFullScreen: Boolean) {
|
fun setFullscreen(isFullScreen: Boolean) {
|
||||||
|
resetZoomPan()
|
||||||
|
|
||||||
if (isFullScreen) {
|
if (isFullScreen) {
|
||||||
|
if (Settings.instance.gestureControls.useSystemBrightness) {
|
||||||
|
try {
|
||||||
|
_originalBrightnessMode = android.provider.Settings.System.getInt(context.contentResolver, android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE)
|
||||||
|
|
||||||
|
val brightness = android.provider.Settings.System.getInt(context.contentResolver, android.provider.Settings.System.SCREEN_BRIGHTNESS)
|
||||||
|
_brightnessFactor = brightness / 255.0f;
|
||||||
|
Log.i(TAG, "Starting brightness brightness: $brightness, _brightnessFactor: $_brightnessFactor, _originalBrightnessMode: $_originalBrightnessMode")
|
||||||
|
|
||||||
|
_originalBrightnessFactor = _brightnessFactor
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Settings.instance.gestureControls.useSystemBrightness = false
|
||||||
|
Settings.instance.save()
|
||||||
|
UIDialogs.toast(context, "useSystemBrightness disabled due to an error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Settings.instance.gestureControls.useSystemBrightness) {
|
||||||
|
_brightnessFactor = 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.instance.gestureControls.useSystemVolume) {
|
||||||
|
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||||
|
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
||||||
|
_soundFactor = currentVolume.toFloat() / maxVolume.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
onBrightnessAdjusted.emit(_brightnessFactor);
|
onBrightnessAdjusted.emit(_brightnessFactor);
|
||||||
onSoundAdjusted.emit(_soundFactor);
|
onSoundAdjusted.emit(_soundFactor);
|
||||||
} else {
|
} else {
|
||||||
onBrightnessAdjusted.emit(1.0f);
|
if (Settings.instance.gestureControls.useSystemBrightness) {
|
||||||
|
if (Settings.instance.gestureControls.restoreSystemBrightness) {
|
||||||
|
onBrightnessAdjusted.emit(_originalBrightnessFactor)
|
||||||
|
|
||||||
|
if (android.provider.Settings.System.canWrite(context)) {
|
||||||
|
Log.i(TAG, "Restoring system brightness mode _originalBrightnessMode: $_originalBrightnessMode")
|
||||||
|
|
||||||
|
android.provider.Settings.System.putInt(
|
||||||
|
context.contentResolver,
|
||||||
|
android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE,
|
||||||
|
_originalBrightnessMode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onBrightnessAdjusted.emit(1.0f);
|
||||||
|
}
|
||||||
//onSoundAdjusted.emit(1.0f);
|
//onSoundAdjusted.emit(1.0f);
|
||||||
stopAdjustingBrightness();
|
stopAdjustingBrightness();
|
||||||
stopAdjustingSound();
|
stopAdjustingSound();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
||||||
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 NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
||||||
constructor(context: Context) : super(context) {}
|
constructor(context: Context) : super(context) {}
|
||||||
@@ -40,32 +41,34 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
|||||||
if (text is Spannable) {
|
if (text is Spannable) {
|
||||||
val links = text.getSpans(offset, offset, URLSpan::class.java)
|
val links = text.getSpans(offset, offset, URLSpan::class.java)
|
||||||
if (links.isNotEmpty()) {
|
if (links.isNotEmpty()) {
|
||||||
for (link in links) {
|
runBlocking {
|
||||||
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." };
|
for (link in links) {
|
||||||
|
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." };
|
||||||
|
|
||||||
val c = context;
|
val c = context;
|
||||||
if (c is MainActivity) {
|
if (c is MainActivity) {
|
||||||
if (c.handleUrl(link.url)) {
|
if (c.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;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ import com.futo.platformplayer.casting.AirPlayCastingDevice
|
|||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
|
import com.futo.platformplayer.formatDuration
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.toHumanTime
|
|
||||||
import com.futo.platformplayer.views.behavior.GestureControlView
|
import com.futo.platformplayer.views.behavior.GestureControlView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -252,8 +252,8 @@ class CastView : ConstraintLayout {
|
|||||||
.load(video.thumbnails.getHQThumbnail())
|
.load(video.thumbnails.getHQThumbnail())
|
||||||
.placeholder(R.drawable.placeholder_video_thumbnail)
|
.placeholder(R.drawable.placeholder_video_thumbnail)
|
||||||
.into(_thumbnail);
|
.into(_thumbnail);
|
||||||
_textPosition.text = position.toHumanTime(false);
|
_textPosition.text = (position * 1000).formatDuration();
|
||||||
_textDuration.text = video.duration.toHumanTime(false);
|
_textDuration.text = (video.duration * 1000).formatDuration();
|
||||||
_timeBar.setPosition(position);
|
_timeBar.setPosition(position);
|
||||||
_timeBar.setDuration(video.duration);
|
_timeBar.setDuration(video.duration);
|
||||||
}
|
}
|
||||||
@@ -261,7 +261,7 @@ class CastView : ConstraintLayout {
|
|||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
fun setTime(ms: Long) {
|
fun setTime(ms: Long) {
|
||||||
updateCurrentChapter(ms);
|
updateCurrentChapter(ms);
|
||||||
_textPosition.text = ms.toHumanTime(true);
|
_textPosition.text = ms.formatDuration();
|
||||||
_timeBar.setPosition(ms / 1000);
|
_timeBar.setPosition(ms / 1000);
|
||||||
StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), ms);
|
StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), ms);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,16 @@ import com.futo.platformplayer.R
|
|||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.models.PlaylistDownloaded
|
import com.futo.platformplayer.models.PlaylistDownloaded
|
||||||
|
|
||||||
class PlaylistDownloadItem(context: Context, val playlist: PlaylistDownloaded): LinearLayout(context) {
|
class PlaylistDownloadItem(context: Context, playlistName: String, playlistThumbnail: String?, val obj: Any): LinearLayout(context) {
|
||||||
init { inflate(context, R.layout.list_downloaded_playlist, this) }
|
init { inflate(context, R.layout.list_downloaded_playlist, this) }
|
||||||
|
|
||||||
var imageView: ImageView = findViewById(R.id.downloaded_playlist_image);
|
var imageView: ImageView = findViewById(R.id.downloaded_playlist_image);
|
||||||
var imageText: TextView = findViewById(R.id.downloaded_playlist_name);
|
var imageText: TextView = findViewById(R.id.downloaded_playlist_name);
|
||||||
|
|
||||||
init {
|
init {
|
||||||
imageText.text = playlist.playlist.name;
|
imageText.text = playlistName;
|
||||||
Glide.with(imageView)
|
Glide.with(imageView)
|
||||||
.load(playlist.playlist.videos.firstOrNull()?.thumbnails?.getHQThumbnail())
|
.load(playlistThumbnail)
|
||||||
.crossfade()
|
.crossfade()
|
||||||
.into(imageView);
|
.into(imageView);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user