Compare commits

...

79 Commits

Author SHA1 Message Date
Kelvin 7729681829 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-02-16 14:58:28 +01:00
Kelvin b12d04b27d Attempted fix for double controls 2024-02-16 14:58:17 +01:00
Koen e6608b9a5c Updated PolycentricAndroid. 2024-02-16 14:07:27 +01:00
Koen 2d503dfaf6 Added scroll to top. Full scrollable parent comment and Polycentric process secret backup and automatic database recovery. 2024-02-16 13:56:14 +01:00
Kelvin 08934ef8de Modify subscription groups in sub settings 2024-02-14 23:25:58 +01:00
Kelvin 62d927739a Sharing from overview options, notification channel names 2024-02-14 20:15:12 +01:00
Kelvin c8db8f58e8 Refs 2024-02-14 19:19:24 +01:00
Kelvin 0fc966a77d Subscription watched filter 2024-02-14 19:18:35 +01:00
Kelvin 9f6c6c8cf3 Fix support, fix membership urls 2024-01-23 23:51:21 +01:00
Kelvin 43a6ff138c Fix queue looping 2024-01-22 20:54:40 +01:00
Kelvin 269a3460e7 Fix live stream retrying 2024-01-22 15:52:51 +01:00
Koen 18150e9e15 Fixed bottom menu button visibility. 2024-01-19 20:29:00 +01:00
Koen 362c7f5b2c Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-19 20:24:35 +01:00
Koen 2adb8ad7f9 Fixed brightness not working. 2024-01-19 20:24:23 +01:00
Kelvin 6b5d4e7507 Fix nullable 2024-01-19 19:44:52 +01:00
Kelvin 49c82726f0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-19 17:28:45 +01:00
Kelvin c8ddcda384 Refs, Dev portal improvements and on-device testing, Fix crashes on disabling v8 race conditions, edgecase where history could be null, issue on starting Grayjay with an url 2024-01-19 17:28:35 +01:00
Koen b75217f789 Possible fix for AudioNoisyReceiver popping up 'App is not responding'. 2024-01-19 17:02:24 +01:00
Koen 8ba8e535bd Added check for updates button on exception activity. 2024-01-19 16:13:42 +01:00
Koen e4c574db6b Fixed crash in updateAllButtonVisibility. 2024-01-19 15:38:22 +01:00
Koen fae73293d7 Fixed crash where it would fail to unregister audio noisy receiver. Fixed crash where system brightness setting does not exist. 2024-01-19 15:17:11 +01:00
Koen 3bd0aac4f8 Implemented system brightness in an alternative way. 2024-01-19 14:09:56 +01:00
Kelvin 26b822e04b Text edit 2024-01-17 17:23:38 +01:00
Kelvin 96b9b8843c Fix wrong visibility for no sources ui 2024-01-17 16:57:35 +01:00
Kelvin 6d9c1e17b5 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-17 15:31:11 +01:00
Kelvin 507ad105c0 Hide warnings if empty, enable newly installed plugins, new browse plugin url 2024-01-17 15:31:05 +01:00
Koen 40a283017e Fixed issue where adding new playlist would require two back swipes to minimize video. 2024-01-17 15:26:09 +01:00
Kelvin be14597670 Merge 2024-01-17 13:28:54 +01:00
Kelvin 837609abb9 Remove primary client, remove play store default source, add additional flows for adding sources 2024-01-17 13:26:17 +01:00
Koen d64cd98b43 Removed most announcements. 2024-01-17 13:08:25 +01:00
Koen 0081ff1483 Removed playstore pre-installed PeerTube. 2024-01-17 12:54:45 +01:00
Kelvin f78ca6c7ed Toggle to disable update check for individual sources 2024-01-17 12:34:58 +01:00
Kelvin cfc7cbcaa4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-17 12:15:26 +01:00
Kelvin e533eb7778 Video zoom increase tolerances 2024-01-17 12:15:17 +01:00
Koen 7c1d0a7f88 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-17 11:02:13 +01:00
Koen 01ef471708 Better handling of null or empty id/url for Polycentric comments/likes. 2024-01-17 11:02:03 +01:00
Kelvin 2fd0a9a41d Fix scroll downloaded playlists 2024-01-16 22:31:45 +01:00
Kelvin 635749dfe4 Open channel/playlist urls through search 2024-01-16 21:42:43 +01:00
Kelvin c4bd5626f3 Fix usable motionlayout on portrait fullscreen 2024-01-16 21:21:26 +01:00
Kelvin 568a0f6329 Catch any failed auth/captcha decodes 2024-01-16 21:11:11 +01:00
Kelvin 7ee67b5cd0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-16 20:11:27 +01:00
Kelvin fc94c6903c Additional warnings for reinstall, disable uninstall if doesn't make sense, reduce maxheight to 40% of videoview 2024-01-16 20:11:23 +01:00
Koen a0af8805e7 Removed accidentally cmomitted code. 2024-01-16 20:09:02 +01:00
Koen 9b64cde17d Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-16 20:08:15 +01:00
Koen f6931bcf8c Added isUserGesturing boolean to gesture control view. 2024-01-16 20:07:26 +01:00
Kelvin a4ff47d863 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-16 16:46:40 +01:00
Kelvin 982d251126 Prevent video reload if same video, refs 2024-01-16 16:46:30 +01:00
Koen 8820a0ecc0 Fixed slide subscription overlay not closing on back gesture in video detail view. Fixed bottom margin for minimized view progress bar. 2024-01-16 13:25:12 +01:00
Koen b99a713ffc Added 'Add to new playlist' to add button when watching video. 2024-01-16 13:04:44 +01:00
Koen dfc8c4b740 Added snapping when only panning. 2024-01-16 12:24:07 +01:00
Koen c3df9e5259 Changed tolerances on zooming/panning snapping. 2024-01-16 12:20:04 +01:00
Koen b9c7e0a8ca Made snapping only work when panned close to center. 2024-01-16 12:05:04 +01:00
Koen 2c7f02a24d Added zoom pan snapping. 2024-01-16 11:01:00 +01:00
Koen 5cc8488d94 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-15 18:07:08 +01:00
Koen 6f7304f59c Added ability to click on a comment to view where the comment was placed and the ability to navigate upwards in the replies overlay by clicking the parent comment. 2024-01-15 18:06:57 +01:00
Kelvin ea4fea4401 Deduplication priorities fixed, playpause button change and wakelock on interruption fixed 2024-01-13 00:51:00 +01:00
Kelvin 9b48664de4 Resolving wakelock on play interuptions 2024-01-12 23:22:49 +01:00
Kelvin 8964dc68f0 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-12 21:34:49 +01:00
Kelvin 4711b8055b Empty home and install plugin flows. BrowserFragment optional url handling 2024-01-12 21:34:39 +01:00
Koen 84e3373fa7 Added settings to enable/disable zoom/pan gestures. 2024-01-11 15:48:58 +01:00
Koen fdd7e32dd8 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-11 12:59:18 +01:00
Koen e57119ebbd Added setting for restore system brightness and finished zoom pan controls. 2024-01-11 12:59:10 +01:00
Kelvin ed29dd8365 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-10 17:53:52 +01:00
Kelvin 196cacb452 Open playlist urls support, indexOutOfBound fix for queue when deleting items 2024-01-10 17:53:45 +01:00
Koen c025913fc8 Fixed tutorial video aspect ratio. 2024-01-10 13:13:25 +01:00
Koen 48b2c68e72 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-10 13:07:33 +01:00
Koen 689766a6ac Added monetization tutorial and added tutorial descriptions. 2024-01-10 13:07:22 +01:00
Kelvin 9306024d17 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-01-09 22:25:49 +01:00
Kelvin 195163840b DOMParser toNodeTree, Better subscription errors, Watch later ordering 2024-01-09 22:25:41 +01:00
Koen 788c54bf8f Fixed durations for castview. 2024-01-09 13:50:51 +01:00
Koen 031aabd523 Proper implementation of editor action. 2024-01-09 13:37:56 +01:00
Koen 85db4cc4e6 Theoretical fix for double controls. 2024-01-08 14:19:21 +01:00
Koen 745aad385b Gesture controls can individually be enabled/disabled and can be toggled to use system or non-system values. 2024-01-08 13:56:11 +01:00
Koen ba87261f9f Added settings to enable/disable gestures. 2024-01-08 12:30:04 +01:00
Koen 7d091382c0 Do not restart threads when started is false. 2024-01-07 21:56:56 +01:00
Koen 781d0797e7 More casting fixes. 2024-01-07 18:18:15 +01:00
Koen ec12a06b88 Reverted disconnect based on pong timer. 2024-01-07 17:05:35 +01:00
Koen bf3e8867c3 Synchronized writes. 2024-01-07 14:19:03 +01:00
Koen 176814a715 Crashfix on unreliable casting connection. Made casting more robust with intermittent TCP connections. 2024-01-07 13:41:43 +01:00
91 changed files with 2025 additions and 552 deletions
-3
View File
@@ -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
+14 -14
View File
@@ -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
@@ -162,25 +162,25 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
//HTTP //HTTP
implementation "com.squareup.okhttp3:okhttp:4.11.0" implementation "com.squareup.okhttp3:okhttp:4.12.0"
//JSON //JSON
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" //Used for structured json implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" //Used for structured json
implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject) implementation 'com.google.code.gson:gson:2.10.1' //Used for complex/anonymous cases like during development conversions (eg. V8RemoteObject)
//JS //JS
implementation("com.caoccao.javet:javet-android:3.0.2") implementation("com.caoccao.javet:javet-android:3.0.3")
//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.7'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.5' implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
implementation 'androidx.media:media:1.7.0' implementation 'androidx.media:media:1.7.0'
//Other //Other
@@ -189,7 +189,7 @@ dependencies {
implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.arthenica:ffmpeg-kit-full:5.1' implementation 'com.arthenica:ffmpeg-kit-full:5.1'
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0' implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.21'
implementation 'com.github.dhaval2404:imagepicker:2.1' implementation 'com.github.dhaval2404:imagepicker:2.1'
implementation 'com.google.zxing:core:3.4.1' implementation 'com.google.zxing:core:3.4.1'
implementation 'com.journeyapps:zxing-android-embedded:4.3.0' implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
@@ -214,7 +214,7 @@ dependencies {
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
testImplementation "org.jetbrains.kotlin:kotlin-test:1.8.22" testImplementation "org.jetbrains.kotlin:kotlin-test:1.9.21"
testImplementation "org.xmlunit:xmlunit-core:2.9.1" testImplementation "org.xmlunit:xmlunit-core:2.9.1"
testImplementation "org.mockito:mockito-core:5.4.0" testImplementation "org.mockito:mockito-core:5.4.0"
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+1
View File
@@ -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", {
+53 -2
View File
@@ -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>
@@ -416,6 +416,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>
@@ -545,6 +548,7 @@
new Vue({ new Vue({
el: '#app', el: '#app',
data: { data: {
searchTestMethods: "",
page: "Plugin", page: "Plugin",
pastPluginUrls: [], pastPluginUrls: [],
settings: {}, settings: {},
@@ -860,6 +864,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) {
}, },
@@ -809,7 +809,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 {
@@ -304,12 +304,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) {
@@ -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) {
@@ -647,9 +683,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 +731,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 +762,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), "",
{ {
@@ -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
@@ -141,7 +142,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}")
@@ -535,13 +538,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 +567,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" -> {
@@ -633,23 +651,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)");
@@ -802,11 +835,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();
} }
@@ -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);
@@ -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) {
@@ -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
} }
} }
@@ -2,6 +2,9 @@ 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
@@ -55,7 +58,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 +75,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, 1)
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
@@ -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
@@ -89,6 +90,8 @@ class FCastCastingDevice : CastingDevice {
private var _version: Long = 1; private var _version: Long = 1;
private var _thread: Thread? = null private var _thread: Thread? = null
private var _pingThread: Thread? = null private var _pingThread: Thread? = null
private var _lastPongTime = -1L
private var _outputStreamLock = Object()
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() { constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
this.name = name; this.name = name;
@@ -208,7 +211,13 @@ class FCastCastingDevice : CastingDevice {
private fun invokeInIOScopeIfRequired(action: () -> Unit): Boolean { private fun invokeInIOScopeIfRequired(action: () -> Unit): Boolean {
if(Looper.getMainLooper().thread == Thread.currentThread()) { if(Looper.getMainLooper().thread == Thread.currentThread()) {
_scopeIO?.launch { action(); } _scopeIO?.launch {
try {
action();
} catch (e: Throwable) {
Logger.e(TAG, "Failed to invoke in IO scope.", e)
}
}
return true; return true;
} }
@@ -243,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 {
@@ -290,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
@@ -303,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;
@@ -313,32 +326,37 @@ class FCastCastingDevice : CastingDevice {
localAddress = _socket?.localAddress; localAddress = _socket?.localAddress;
connectionState = CastConnectionState.CONNECTED; connectionState = CastConnectionState.CONNECTED;
_lastPongTime = -1L
val buffer = ByteArray(4096); val buffer = ByteArray(4096);
Logger.i(TAG, "Started receiving."); Logger.i(TAG, "Started receiving.");
var exceptionOccurred = false; while (_scopeIO?.isActive == true) {
while (_scopeIO?.isActive == true && !exceptionOccurred) {
try { try {
val inputStream = _inputStream ?: break; val inputStream = _inputStream ?: break;
Log.d(TAG, "Receiving next packet..."); Log.d(TAG, "Receiving next packet...");
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();
if (size > buffer.size) { if (size > buffer.size) {
Logger.w(TAG, "Skipping packet that is too large $size bytes.") Logger.w(TAG, "Packets larger than $size bytes are not supported.")
inputStream.skip(size.toLong()); break
continue;
} }
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));
@@ -353,19 +371,22 @@ class FCastCastingDevice : CastingDevice {
try { try {
handleMessage(Opcode.find(opcode), json); handleMessage(Opcode.find(opcode), json);
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(TAG, "Failed to handle message.", e); Logger.w(TAG, "Failed to handle message.", e)
break
} }
} 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);
exceptionOccurred = true; break
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Exception while receiving.", e); Logger.e(TAG, "Exception while receiving.", e);
exceptionOccurred = true; break
} }
} }
try { try {
_socket?.close(); _socket?.close()
_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)
@@ -386,10 +407,28 @@ class FCastCastingDevice : CastingDevice {
try { try {
send(Opcode.Ping) send(Opcode.Ping)
} catch (e: Throwable) { } catch (e: Throwable) {
Log.w(TAG, "Failed to send ping."); Log.w(TAG, "Failed to send ping.")
try {
_socket?.close()
_inputStream?.close()
_outputStream?.close()
} catch (e: Throwable) {
Log.w(TAG, "Failed to close socket.", e)
}
} }
Thread.sleep(5000); /*if (_lastPongTime != -1L && System.currentTimeMillis() - _lastPongTime > 6000) {
Logger.w(TAG, "Closing socket due to last pong time being larger than 6 seconds.")
try {
_socket?.close()
} catch (e: Throwable) {
Log.w(TAG, "Failed to close socket.", e)
}
}*/
Thread.sleep(2000)
} }
Logger.i(TAG, "Stopped ping loop."); Logger.i(TAG, "Stopped ping loop.");
@@ -446,39 +485,44 @@ class FCastCastingDevice : CastingDevice {
Logger.i(TAG, "Remote version received: $version") Logger.i(TAG, "Remote version received: $version")
} }
Opcode.Ping -> send(Opcode.Pong) Opcode.Ping -> send(Opcode.Pong)
Opcode.Pong -> _lastPongTime = System.currentTimeMillis()
else -> { } else -> { }
} }
} }
private fun send(opcode: Opcode, message: String? = null) { private fun send(opcode: Opcode, message: String? = null) {
try { ensureNotMainThread()
val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
val size = 1 + data.size synchronized (_outputStreamLock) {
val outputStream = _outputStream try {
if (outputStream == null) { val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
Log.w(TAG, "Failed to send $size bytes, output stream is null.") val size = 1 + data.size
return val outputStream = _outputStream
if (outputStream == null) {
Log.w(TAG, "Failed to send $size bytes, output stream is null.")
return
}
val serializedSizeLE = ByteArray(4)
serializedSizeLE[0] = (size and 0xff).toByte()
serializedSizeLE[1] = (size shr 8 and 0xff).toByte()
serializedSizeLE[2] = (size shr 16 and 0xff).toByte()
serializedSizeLE[3] = (size shr 24 and 0xff).toByte()
outputStream.write(serializedSizeLE)
val opcodeBytes = ByteArray(1)
opcodeBytes[0] = opcode.value
outputStream.write(opcodeBytes)
if (data.isNotEmpty()) {
outputStream.write(data)
}
Log.d(TAG, "Sent $size bytes: (opcode: $opcode, body: $message).")
} catch (e: Throwable) {
Log.i(TAG, "Failed to send message.", e)
throw e
} }
val serializedSizeLE = ByteArray(4)
serializedSizeLE[0] = (size and 0xff).toByte()
serializedSizeLE[1] = (size shr 8 and 0xff).toByte()
serializedSizeLE[2] = (size shr 16 and 0xff).toByte()
serializedSizeLE[3] = (size shr 24 and 0xff).toByte()
outputStream.write(serializedSizeLE)
val opcodeBytes = ByteArray(1)
opcodeBytes[0] = opcode.value
outputStream.write(opcodeBytes)
if (data.isNotEmpty()) {
outputStream.write(data)
}
Log.d(TAG, "Sent $size bytes: (opcode: $opcode, body: $message).")
} catch (e: Throwable) {
Log.i(TAG, "Failed to send message.", e)
throw e
} }
} }
@@ -508,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.")
@@ -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();
@@ -190,6 +204,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 +465,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();
@@ -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
@@ -10,6 +11,7 @@ import com.caoccao.javet.values.primitive.V8ValueBoolean
import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.primitive.V8ValueInteger
import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.primitive.V8ValueString
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
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.internal.JSHttpClient import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
@@ -173,8 +175,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,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>
);
} }
} }
@@ -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
@@ -336,8 +337,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) {
@@ -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())
} }
} }
@@ -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
)
} }
@@ -117,6 +117,7 @@ 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);
return@InsertedViewAdapterWithLoader holder; return@InsertedViewAdapterWithLoader holder;
} }
); );
@@ -200,6 +201,17 @@ 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 onRepliesClick(c: IPlatformComment) { private fun onRepliesClick(c: IPlatformComment) {
val replyCount = c.replyCount ?: 0; val replyCount = c.replyCount ?: 0;
var metadata = ""; var metadata = "";
@@ -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);
}; };
@@ -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);
} }
} }
@@ -315,7 +315,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);
@@ -663,7 +663,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")
@@ -295,17 +295,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 +332,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 +339,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);
@@ -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());
@@ -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)
@@ -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
) )
) )
} }
@@ -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);
@@ -145,7 +145,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
@@ -374,7 +373,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 +398,10 @@ class VideoDetailView : ConstraintLayout {
} }
} }
}; };
_monetization.onUrlTap.subscribe {
fragment.navigate<BrowserFragment>(it);
onMinimize.emit();
}
_player.attachPlayer(); _player.attachPlayer();
@@ -762,7 +765,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 +863,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 +1009,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 +1039,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 +1143,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 +1234,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 +1386,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 +1667,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 +1677,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 +1984,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() {
@@ -2240,7 +2256,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 +2365,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 +2566,7 @@ class VideoDetailView : ConstraintLayout {
} }
else else
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
setVideoDetails(videoDetail); setVideoDetails(videoDetail, false);
_liveTryJob = null; _liveTryJob = null;
} }
} }
@@ -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
} }
}; };
@@ -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);
}; };
@@ -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);
}; };
@@ -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
@@ -427,8 +426,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 +474,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 +558,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 +580,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 +644,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.")
@@ -1,5 +1,6 @@
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
@@ -49,6 +50,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 +96,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> {
@@ -841,6 +818,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 +864,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 +878,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 +917,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
} }
@@ -361,6 +361,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 +413,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 +433,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)
@@ -16,6 +16,7 @@ import com.futo.platformplayer.exceptions.ReconstructionException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
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
@@ -35,6 +36,8 @@ class StatePlaylists {
= SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails); = SerializedPlatformVideo.fromVideo(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,26 +51,32 @@ 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();
} }
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();
} }
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();
} }
@@ -160,6 +160,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;
@@ -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));
@@ -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
@@ -69,7 +70,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 +126,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>> {
@@ -200,7 +200,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);
} }
} }
@@ -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);
@@ -55,6 +55,7 @@ class CommentWithReferenceViewHolder : ViewHolder {
var onRepliesClick = Event1<IPlatformComment>(); var onRepliesClick = Event1<IPlatformComment>();
var onDelete = Event1<IPlatformComment>(); var onDelete = Event1<IPlatformComment>();
var onClick = Event1<IPlatformComment>();
var comment: IPlatformComment? = null var comment: IPlatformComment? = null
private set; private set;
@@ -108,6 +109,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,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);
} }
@@ -1,6 +1,7 @@
package com.futo.platformplayer.views.overlays package com.futo.platformplayer.views.overlays
import android.content.Context import android.content.Context
import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
@@ -8,11 +9,15 @@ import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fixHtmlLinks import com.futo.platformplayer.fixHtmlLinks
import com.futo.platformplayer.logging.Logger
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
import com.futo.platformplayer.toHumanNowDiffString import com.futo.platformplayer.toHumanNowDiffString
@@ -20,6 +25,13 @@ import com.futo.platformplayer.views.behavior.NonScrollingTextView
import com.futo.platformplayer.views.comments.AddCommentView import com.futo.platformplayer.views.comments.AddCommentView
import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.segments.CommentsList import com.futo.platformplayer.views.segments.CommentsList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import userpackage.Protocol import userpackage.Protocol
class RepliesOverlay : LinearLayout { class RepliesOverlay : LinearLayout {
@@ -34,10 +46,16 @@ class RepliesOverlay : LinearLayout {
private val _creatorThumbnail: CreatorThumbnail; private val _creatorThumbnail: CreatorThumbnail;
private val _layoutParentComment: ConstraintLayout; private val _layoutParentComment: ConstraintLayout;
private var _readonly = false; private var _readonly = false;
private var _loading = true;
private var _parentComment: IPlatformComment? = null;
private var _onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null; private var _onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null;
private val _loaderOverlay: LoaderOverlay
private val _client = ManagedHttpClient()
private val _layoutItems: LinearLayout
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.overlay_replies, this) inflate(context, R.layout.overlay_replies, this)
_layoutItems = findViewById(R.id.layout_items)
_topbar = findViewById(R.id.topbar); _topbar = findViewById(R.id.topbar);
_commentsList = findViewById(R.id.comments_list); _commentsList = findViewById(R.id.comments_list);
_addCommentView = findViewById(R.id.add_comment_view); _addCommentView = findViewById(R.id.add_comment_view);
@@ -46,6 +64,11 @@ class RepliesOverlay : LinearLayout {
_textAuthor = findViewById(R.id.text_author) _textAuthor = findViewById(R.id.text_author)
_creatorThumbnail = findViewById(R.id.image_thumbnail) _creatorThumbnail = findViewById(R.id.image_thumbnail)
_layoutParentComment = findViewById(R.id.layout_parent_comment) _layoutParentComment = findViewById(R.id.layout_parent_comment)
_loaderOverlay = findViewById(R.id.loader_overlay)
setLoading(false);
_layoutItems.removeView(_layoutParentComment)
_commentsList.setPrependedView(_layoutParentComment)
_addCommentView.onCommentAdded.subscribe { _addCommentView.onCommentAdded.subscribe {
_commentsList.addComment(it); _commentsList.addComment(it);
@@ -72,11 +95,21 @@ class RepliesOverlay : LinearLayout {
} }
}; };
_layoutParentComment.setOnClickListener {
val p = _parentComment
if (p !is PolycentricPlatformComment) {
return@setOnClickListener
}
val ref = p.parentReference ?: return@setOnClickListener
handleParentClick(p.contextUrl, ref)
}
_topbar.onClose.subscribe(this, onClose::emit); _topbar.onClose.subscribe(this, onClose::emit);
_topbar.setInfo(context.getString(R.string.Replies), ""); _topbar.setInfo(context.getString(R.string.Replies), "");
} }
fun load(readonly: Boolean, metadata: String, contextUrl: String?, ref: Protocol.Reference?, parentComment: IPlatformComment? = null, loader: suspend () -> IPager<IPlatformComment>, onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null) { fun load(readonly: Boolean, metadata: String, contextUrl: String?, ref: Protocol.Reference?, parentComment: IPlatformComment? = null, loader: suspend () -> IPager<IPlatformComment>, onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null, onParentClick: ((comment: IPlatformComment) -> Unit)? = null) {
_readonly = readonly; _readonly = readonly;
if (readonly) { if (readonly) {
_addCommentView.visibility = View.GONE; _addCommentView.visibility = View.GONE;
@@ -109,6 +142,136 @@ class RepliesOverlay : LinearLayout {
_topbar.setInfo(context.getString(R.string.Replies), metadata); _topbar.setInfo(context.getString(R.string.Replies), metadata);
_commentsList.load(readonly, loader); _commentsList.load(readonly, loader);
_onCommentAdded = onCommentAdded; _onCommentAdded = onCommentAdded;
_parentComment = parentComment;
}
fun handleParentClick(contextUrl: String, ref: Protocol.Reference): Boolean {
val ctx = context
if (ctx !is MainActivity) {
return false
}
return when (ref.referenceType) {
2L -> {
setLoading(true)
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try {
val parentComment = StatePolycentric.instance.getComment(contextUrl, ref)
val replyCount = parentComment.replyCount ?: 0;
var metadata = "";
if (replyCount > 0) {
metadata += "$replyCount " + context.getString(R.string.replies);
}
withContext(Dispatchers.Main) {
setLoading(false)
load(false, metadata, parentComment.contextUrl, parentComment.reference, parentComment,
{ StatePolycentric.instance.getCommentPager(contextUrl, ref) })
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
setLoading(false)
}
Logger.e(TAG, "Failed to load parent comment.", e)
UIDialogs.toast("Failed to load comment")
}
}
true
}
3L -> {
StateApp.instance.scopeOrNull?.launch {
try {
val url = referenceToUrl(_client, ref) ?: return@launch
withContext(Dispatchers.Main) {
ctx.handleUrl(url)
onClose.emit()
}
} catch (e: Throwable) {
Logger.i(TAG, "Failed to open ref.", e)
}
}
false
}
else -> false
}
}
private fun referenceToUrl(client: ManagedHttpClient, parentRef: Protocol.Reference): String? {
val refBytes = parentRef.reference?.toByteArray() ?: return null
val ref = refBytes.decodeToString()
try {
Uri.parse(ref)
return ref
} catch (e: Throwable) {
try {
return oldReferenceToUrl(client, ref)
} catch (f: Throwable) {
Logger.i(TAG, "Failed to handle URL.", f)
}
}
return null
}
private fun oldReferenceToUrl(client: ManagedHttpClient, reference: String): String? {
return when {
reference.startsWith("video_episode:") -> {
val response = client.get("https://content.api.nebula.app/video_episodes/$reference")
if (!response.isOk) {
throw Exception("Failed to resolve nebula video (${response.code}).")
}
val respString = response.body?.string()
val jsonElement = respString?.let { Json.parseToJsonElement(it) }
return jsonElement?.jsonObject?.get("share_url")?.jsonPrimitive?.content
}
reference.length == 11 -> "https://www.youtube.com/watch?v=$reference"
reference.length == 40 -> {
val response = client.post("https://api.na-backend.odysee.com/api/v1/proxy?m=claim_search", hashMapOf(
"Content-Type" to "application/json"
))
if (!response.isOk) {
throw Exception("Failed to resolve claim (${response.code}).")
}
val jsonElement = response.body?.string()?.let { Json.parseToJsonElement(it) }
val canonicalUrl = jsonElement?.jsonObject?.get("result")
?.jsonObject?.get("items")
?.jsonArray?.get(0)
?.jsonObject?.get("canonical_url")
?.jsonPrimitive?.content
canonicalUrl ?: throw Exception("Failed to get canonical URL.")
}
reference.startsWith("v") && (reference.length == 7 || reference.length == 6) -> "https://rumble.com/$reference"
Regex("^\\d+\$").matches(reference) -> "https://www.twitch.tv/videos/$reference"
else -> null
}
}
private fun setLoading(loading: Boolean) {
if (_loading == loading) {
return;
}
_loading = loading;
if (!loading) {
_loaderOverlay.hide()
} else {
_loaderOverlay.show()
}
} }
fun cleanup() { fun cleanup() {
@@ -116,4 +279,8 @@ class RepliesOverlay : LinearLayout {
_onCommentAdded = null; _onCommentAdded = null;
_commentsList.cancel(); _commentsList.cancel();
} }
companion object {
private const val TAG = "RepliesOverlay"
}
} }
@@ -14,6 +14,10 @@ class SupportOverlay : LinearLayout {
private val _topbar: OverlayTopbar; private val _topbar: OverlayTopbar;
private val _support: SupportView; private val _support: SupportView;
val hasSupportItems: Boolean get() {
return _support.hasSupportItems;
}
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.overlay_support, this) inflate(context, R.layout.overlay_support, this)
_topbar = findViewById(R.id.topbar); _topbar = findViewById(R.id.topbar);
@@ -0,0 +1,38 @@
package com.futo.platformplayer.views.overlays.slideup
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R
import com.futo.platformplayer.views.AnyAdapterView
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.adapters.AnyAdapter
class SlideUpMenuRecycler<T : Any, VType : AnyAdapter.AnyViewHolder<T>> : LinearLayout {
private lateinit var recyclerView: RecyclerView;
private val adapter: AnyAdapterView<T, VType>?;
var groupTag: Any? = null;
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
init();
adapter = null;
}
constructor(context: Context, tag: Any, creation: (RecyclerView)->AnyAdapterView<T, VType>) : super(context){
init();
groupTag = tag;
adapter = creation(recyclerView);
}
private fun init(){
LayoutInflater.from(context).inflate(R.layout.overlay_slide_up_menu_recycler, this, true);
recyclerView = findViewById(R.id.slide_up_menu_recycler);
}
}
@@ -14,6 +14,7 @@ class WidePillButton : LinearLayout {
private val _iconPrefix: ImageView private val _iconPrefix: ImageView
private val _iconSuffix: ImageView private val _iconSuffix: ImageView
private val _text: TextView private val _text: TextView
private val _textDescription: TextView
val onClick = Event0() val onClick = Event0()
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
@@ -21,11 +22,13 @@ class WidePillButton : LinearLayout {
_iconPrefix = findViewById(R.id.image_prefix) _iconPrefix = findViewById(R.id.image_prefix)
_iconSuffix = findViewById(R.id.image_suffix) _iconSuffix = findViewById(R.id.image_suffix)
_text = findViewById(R.id.text) _text = findViewById(R.id.text)
_textDescription = findViewById(R.id.text_description)
val attrArr = context.obtainStyledAttributes(attrs, R.styleable.WidePillButton, 0, 0) val attrArr = context.obtainStyledAttributes(attrs, R.styleable.WidePillButton, 0, 0)
setIconPrefix(attrArr.getResourceId(R.styleable.WidePillButton_widePillIconPrefix, -1)) setIconPrefix(attrArr.getResourceId(R.styleable.WidePillButton_widePillIconPrefix, -1))
setIconSuffix(attrArr.getResourceId(R.styleable.WidePillButton_widePillIconSuffix, -1)) setIconSuffix(attrArr.getResourceId(R.styleable.WidePillButton_widePillIconSuffix, -1))
setText(attrArr.getText(R.styleable.PillButton_pillText) ?: "") setText(attrArr.getText(R.styleable.WidePillButton_widePillText) ?: "")
setDescription(attrArr.getText(R.styleable.WidePillButton_widePillDescription))
attrArr.recycle() attrArr.recycle()
findViewById<LinearLayout>(R.id.root).setOnClickListener { findViewById<LinearLayout>(R.id.root).setOnClickListener {
@@ -54,4 +57,13 @@ class WidePillButton : LinearLayout {
fun setText(t: CharSequence) { fun setText(t: CharSequence) {
_text.text = t _text.text = t
} }
fun setDescription(t: CharSequence?) {
if (!t.isNullOrEmpty()) {
_textDescription.visibility = View.VISIBLE
_textDescription.text = t
} else {
_textDescription.visibility= View.GONE
}
}
} }
@@ -4,7 +4,6 @@ import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Gravity import android.view.Gravity
import android.view.KeyCharacterMap.UnavailableException
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
@@ -12,10 +11,8 @@ import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
@@ -25,6 +22,8 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.adapters.CommentViewHolder import com.futo.platformplayer.views.adapters.CommentViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
@@ -72,6 +71,9 @@ class CommentsList : ConstraintLayout {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy); super.onScrolled(recyclerView, dx, dy);
onScrolled(); onScrolled();
val totalScrollDistance = recyclerView.computeVerticalScrollOffset()
_layoutScrollToTop.visibility = if (totalScrollDistance > recyclerView.height) View.VISIBLE else View.GONE
} }
}; };
@@ -83,16 +85,22 @@ class CommentsList : ConstraintLayout {
private var _loading = false; private var _loading = false;
private val _prependedView: FrameLayout; private val _prependedView: FrameLayout;
private var _readonly: Boolean = false; private var _readonly: Boolean = false;
private val _layoutScrollToTop: FrameLayout;
var onRepliesClick = Event1<IPlatformComment>(); var onRepliesClick = Event1<IPlatformComment>();
var onCommentsLoaded = Event1<Int>(); var onCommentsLoaded = Event1<Int>();
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
LayoutInflater.from(context).inflate(R.layout.view_comments_list, this, true); LayoutInflater.from(context).inflate(R.layout.view_comments_list, this, true);
_recyclerComments = findViewById(R.id.recycler_comments); _recyclerComments = findViewById(R.id.recycler_comments);
_layoutScrollToTop = findViewById(R.id.layout_scroll_to_top);
_layoutScrollToTop.setOnClickListener {
_recyclerComments.smoothScrollToPosition(0)
}
_layoutScrollToTop.visibility = View.GONE
_textMessage = TextView(context).apply { _textMessage = TextView(context).apply {
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply { layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply {
setMargins(0, 30, 0, 0) setMargins(0, 30, 0, 0)
@@ -1,9 +1,12 @@
package com.futo.platformplayer.views.video package com.futo.platformplayer.views.video
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.media.AudioManager
import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.util.TypedValue import android.util.TypedValue
@@ -119,6 +122,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
private var _currentChapterLoopActive = false; private var _currentChapterLoopActive = false;
private var _currentChapterLoopId: Int = 0; private var _currentChapterLoopId: Int = 0;
private var _currentChapter: IChapter? = null; private var _currentChapter: IChapter? = null;
private var _promptedForPermissions: Boolean = false;
//Events //Events
@@ -235,16 +239,38 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
gestureControl.setupTouchArea(_layoutControls, background); gestureControl.setupTouchArea(_layoutControls, background);
gestureControl.onSeek.subscribe { seekFromCurrent(it); }; gestureControl.onSeek.subscribe { seekFromCurrent(it); };
gestureControl.onSoundAdjusted.subscribe { setVolume(it) }; gestureControl.onSoundAdjusted.subscribe {
gestureControl.onToggleFullscreen.subscribe { setFullScreen(!isFullScreen) }; if (Settings.instance.gestureControls.useSystemVolume) {
gestureControl.onBrightnessAdjusted.subscribe { val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (it == 1.0f) { val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
_overlay_brightness.visibility = View.GONE; audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, (it * maxVolume).toInt(), 0)
} else { } else {
_overlay_brightness.visibility = View.VISIBLE; setVolume(it)
_overlay_brightness.setBackgroundColor(Color.valueOf(0.0f, 0.0f, 0.0f, (1.0f - it)).toArgb());
} }
}; };
gestureControl.onToggleFullscreen.subscribe { setFullScreen(!isFullScreen) };
gestureControl.onBrightnessAdjusted.subscribe {
if (Settings.instance.gestureControls.useSystemBrightness) {
setSystemBrightness(it)
} else {
if (it == 1.0f) {
_overlay_brightness.visibility = View.GONE;
} else {
_overlay_brightness.visibility = View.VISIBLE;
_overlay_brightness.setBackgroundColor(Color.valueOf(0.0f, 0.0f, 0.0f, (1.0f - it)).toArgb());
}
}
};
gestureControl.onPan.subscribe { x, y ->
_videoView.translationX = x
_videoView.translationY = y
}
gestureControl.onZoom.subscribe {
_videoView.scaleX = it
_videoView.scaleY = it
}
gestureControl.setZoomPanEnabled(_videoView.videoSurfaceView!!)
if(!isInEditMode) { if(!isInEditMode) {
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; _videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
@@ -405,6 +431,30 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
} }
} }
private fun setSystemBrightness(brightness: Float) {
Log.i(TAG, "setSystemBrightness $brightness")
if (android.provider.Settings.System.canWrite(context)) {
Log.i(TAG, "setSystemBrightness canWrite $brightness")
android.provider.Settings.System.putInt(context.contentResolver, android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE, android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
android.provider.Settings.System.putInt(context.contentResolver, android.provider.Settings.System.SCREEN_BRIGHTNESS, (brightness * 255.0f).toInt().coerceAtLeast(1).coerceAtMost(255));
} else if (!_promptedForPermissions) {
Log.i(TAG, "setSystemBrightness prompt $brightness")
_promptedForPermissions = true
UIDialogs.showConfirmationDialog(context, "System brightness controls require explicit permission", action = {
openAndroidPermissionsMenu()
})
} else {
Log.i(TAG, "setSystemBrightness no permission?")
//No permissions but already prompted, ignore
}
}
private fun openAndroidPermissionsMenu() {
val intent = Intent(android.provider.Settings.ACTION_MANAGE_WRITE_SETTINGS)
intent.setData(Uri.parse("package:" + context.packageName))
context.startActivity(intent)
}
fun updateNextPrevious() { fun updateNextPrevious() {
val vidPrev = StatePlayer.instance.getPrevQueueItem(true); val vidPrev = StatePlayer.instance.getPrevQueueItem(true);
val vidNext = StatePlayer.instance.getNextQueueItem(true); val vidNext = StatePlayer.instance.getNextQueueItem(true);
@@ -531,7 +581,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; _videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
_videoControls_fullscreen.show(); _videoControls_fullscreen.show();
videoControls.hide(); videoControls.hideImmediately();
videoControls.visibility = View.GONE;
} }
else { else {
val lp = background.layoutParams as ConstraintLayout.LayoutParams; val lp = background.layoutParams as ConstraintLayout.LayoutParams;
@@ -543,7 +594,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM; _videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
videoControls.show(); videoControls.show();
_videoControls_fullscreen.hide(); _videoControls_fullscreen.hideImmediately();
_videoControls_fullscreen.visibility = View.GONE;
} }
fitOrFill(fullScreen); fitOrFill(fullScreen);
@@ -574,6 +626,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
} }
override fun onVideoSizeChanged(videoSize: VideoSize) { override fun onVideoSizeChanged(videoSize: VideoSize) {
gestureControl.resetZoomPan()
_lastSourceFit = null; _lastSourceFit = null;
if(isFullScreen) if(isFullScreen)
fillHeight(); fillHeight();
@@ -603,6 +656,10 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
} }
} }
override fun onIsPlayingChanged(playing: Boolean) {
super.onIsPlayingChanged(playing)
updatePlayPause();
}
override fun onPlaybackStateChanged(playbackState: Int) { override fun onPlaybackStateChanged(playbackState: Int) {
Logger.v(TAG, "onPlaybackStateChanged $playbackState"); Logger.v(TAG, "onPlaybackStateChanged $playbackState");
updatePlayPause() updatePlayPause()
@@ -658,7 +715,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
val viewWidth = Math.min(metrics.widthPixels, metrics.heightPixels); //TODO: Get parent width. was this.width val viewWidth = Math.min(metrics.widthPixels, metrics.heightPixels); //TODO: Get parent width. was this.width
val deviceHeight = Math.max(metrics.widthPixels, metrics.heightPixels); val deviceHeight = Math.max(metrics.widthPixels, metrics.heightPixels);
val maxHeight = deviceHeight * 0.6; val maxHeight = deviceHeight * 0.4;
val determinedHeight = if(w > h) val determinedHeight = if(w > h)
((h * (viewWidth.toDouble() / w)).toInt()) ((h * (viewWidth.toDouble() / w)).toInt())
@@ -730,8 +787,11 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
} }
} }
fun setGestureSoundFactor(soundFactor: Float) { fun setGestureSoundFactor(soundFactor: Float) {
gestureControl.setSoundFactor(soundFactor); gestureControl.setSoundFactor(soundFactor);
} }
override fun onSurfaceSizeChanged(width: Int, height: Int) {
gestureControl.resetZoomPan()
}
} }
@@ -100,6 +100,21 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
private var _toResume = false; private var _toResume = false;
private val _playerEventListener = object: Player.Listener { private val _playerEventListener = object: Player.Listener {
override fun onPlaybackSuppressionReasonChanged(playbackSuppressionReason: Int) {
super.onPlaybackSuppressionReasonChanged(playbackSuppressionReason)
}
override fun onSurfaceSizeChanged(width: Int, height: Int) {
super.onSurfaceSizeChanged(width, height)
this@FutoVideoPlayerBase.onSurfaceSizeChanged(width, height);
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying);
this@FutoVideoPlayerBase.onIsPlayingChanged(isPlaying);
updatePlaying();
}
//TODO: Figure out why this is deprecated, and what the alternative is. //TODO: Figure out why this is deprecated, and what the alternative is.
override fun onPlaybackStateChanged(playbackState: Int) { override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState) super.onPlaybackStateChanged(playbackState)
@@ -582,6 +597,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
exoPlayer?.setVolume(volume); exoPlayer?.setVolume(volume);
} }
protected open fun onSurfaceSizeChanged(width: Int, height: Int) {
}
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
protected open fun onPlayerError(error: PlaybackException) { protected open fun onPlayerError(error: PlaybackException) {
Logger.i(TAG, "onPlayerError error=$error error.errorCode=${error.errorCode} connectivityLoss"); Logger.i(TAG, "onPlayerError error=$error error.errorCode=${error.errorCode} connectivityLoss");
@@ -616,6 +635,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
protected open fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource? = null, resume: Boolean = true) { } protected open fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource? = null, resume: Boolean = true) { }
protected open fun onIsPlayingChanged(playing: Boolean) {
}
protected open fun onPlaybackStateChanged(playbackState: Int) { protected open fun onPlaybackStateChanged(playbackState: Int) {
if (_shouldPlaybackRestartOnConnectivity && playbackState == ExoPlayer.STATE_READY) { if (_shouldPlaybackRestartOnConnectivity && playbackState == ExoPlayer.STATE_READY) {
Logger.i(TAG, "_shouldPlaybackRestartOnConnectivity=false"); Logger.i(TAG, "_shouldPlaybackRestartOnConnectivity=false");
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#2A2A2A" />
<corners android:radius="25dp" />
<size android:height="20dp" />
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:color="#992D63ED" android:width="5dp" />
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>
@@ -110,6 +110,7 @@
<!--Security Warnings--> <!--Security Warnings-->
<LinearLayout <LinearLayout
android:id="@+id/container_source_warnings"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
@@ -55,6 +55,15 @@
app:buttonText="@string/install_by_qr" app:buttonText="@string/install_by_qr"
app:buttonSubText="@string/install_a_plugin_by_scanning_a_qr_code" app:buttonSubText="@string/install_a_plugin_by_scanning_a_qr_code"
app:buttonIcon="@drawable/ic_qr" /> app:buttonIcon="@drawable/ic_qr" />
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/option_browse"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
app:buttonText="Browse Online Sources"
app:buttonSubText="Install a plugin by browsing official plugins"
app:buttonIcon="@drawable/ic_explore" />
<com.futo.platformplayer.views.buttons.BigButton <com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/option_url" android:id="@+id/option_url"
android:layout_width="match_parent" android:layout_width="match_parent"
+16 -3
View File
@@ -37,9 +37,22 @@
android:fontFamily="@font/inter_extra_light" /> android:fontFamily="@font/inter_extra_light" />
</FrameLayout> </FrameLayout>
<Space <LinearLayout
android:layout_width="20dp" android:id="@+id/button_check_for_updates"
android:layout_height="match_parent" /> android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:gravity="center"
android:background="@drawable/background_button_primary_round_4dp"
android:layout_gravity="center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="center"
android:text="@string/check_for_updates"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout <LinearLayout
android:id="@+id/layout_header" android:id="@+id/layout_header"
@@ -65,7 +65,8 @@
android:id="@+id/replies_overlay" android:id="@+id/replies_overlay"
android:visibility="gone" android:visibility="gone"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent"
android:clickable="true" />
<LinearLayout android:id="@+id/layout_not_logged_in" <LinearLayout android:id="@+id/layout_not_logged_in"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -128,13 +128,17 @@
tools:text="(7 playlists, 85 videos)" tools:text="(7 playlists, 85 videos)"
/> />
</LinearLayout> </LinearLayout>
<LinearLayout <HorizontalScrollView
android:orientation="horizontal"
android:id="@+id/downloads_playlist_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<!--Fill Programmatically--> <LinearLayout
</LinearLayout> android:orientation="horizontal"
android:id="@+id/downloads_playlist_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<!--Fill Programmatically-->
</LinearLayout>
</HorizontalScrollView>
</LinearLayout> </LinearLayout>
<!--Videos--> <!--Videos-->
@@ -17,6 +17,40 @@
android:orientation="vertical" android:orientation="vertical"
android:paddingStart="20dp" android:paddingStart="20dp"
android:paddingEnd="20dp"> android:paddingEnd="20dp">
<LinearLayout
android:id="@+id/no_sources"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_error"
app:tint="#FFF" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#FFF"
android:textSize="12dp"
android:fontFamily="@font/inter_light"
android:text="@string/no_sources_installed"
android:layout_gravity="center"
android:layout_marginStart="8dp"/>
</LinearLayout>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/container_enabled" android:id="@+id/container_enabled"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -91,6 +125,7 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:id="@+id/plugin_disclaimer"
android:orientation="horizontal" android:orientation="horizontal"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"> android:layout_marginBottom="10dp">
@@ -113,6 +148,15 @@
</LinearLayout> </LinearLayout>
<com.futo.platformplayer.views.buttons.BigButton
android:id="@+id/button_add_sources"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:buttonIcon="@drawable/ic_explore"
app:buttonText="Add Sources"
app:buttonSubText="Install new sources to see more content."
/>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
+2 -1
View File
@@ -8,7 +8,8 @@
android:orientation="vertical" android:orientation="vertical"
android:id="@+id/container" android:id="@+id/container"
android:background="#77000000" android:background="#77000000"
android:elevation="4dp"> android:elevation="4dp"
android:clickable="true">
<ImageView <ImageView
android:id="@+id/loader" android:id="@+id/loader"
android:layout_width="80dp" android:layout_width="80dp"
+101 -98
View File
@@ -1,110 +1,113 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout
android:layout_width="match_parent" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:background="@color/black" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto"> android:layout_width="match_parent"
android:layout_height="match_parent">
<com.futo.platformplayer.views.overlays.OverlayTopbar <LinearLayout android:layout_width="match_parent"
android:id="@+id/topbar" android:layout_height="match_parent"
android:layout_width="match_parent" android:background="@color/black"
android:layout_height="40dp" android:orientation="vertical"
app:title="Replies" android:id="@+id/layout_items">
app:metadata="3 replies"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout <com.futo.platformplayer.views.overlays.OverlayTopbar
android:id="@+id/layout_parent_comment" android:id="@+id/topbar"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="40dp"
app:layout_constraintTop_toBottomOf="@id/topbar" app:title="Replies"
app:layout_constraintLeft_toLeftOf="parent" app:metadata="3 replies" />
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginTop="6dp"
android:padding="12dp"
android:background="@drawable/background_16_round_4dp">
<com.futo.platformplayer.views.others.CreatorThumbnail <com.futo.platformplayer.views.comments.AddCommentView
android:id="@+id/image_thumbnail" android:id="@+id/add_comment_view"
android:layout_width="25dp" android:layout_width="match_parent"
android:layout_height="25dp"
android:contentDescription="@string/channel_image"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/placeholder_channel_thumbnail" />
<TextView
android:id="@+id/text_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="10dp" android:layout_marginTop="12dp"
android:ellipsize="end" android:layout_marginStart="12dp"
android:gravity="center_vertical" android:layout_marginEnd="12dp" />
android:maxLines="1"
android:fontFamily="@font/inter_regular"
android:textColor="@color/white"
android:textSize="14sp"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintTop_toTopOf="@id/image_thumbnail"
android:text="ShortCircuit" />
<TextView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/text_metadata" android:id="@+id/layout_parent_comment"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:layout_width="match_parent"
android:gravity="center_vertical" android:layout_marginStart="12dp"
android:maxLines="1" android:layout_marginEnd="12dp"
android:fontFamily="@font/inter_regular" android:layout_marginBottom="12dp"
android:textColor="@color/gray_ac" android:padding="12dp"
android:textSize="14sp" android:background="@drawable/background_16_round_4dp">
app:layout_constraintBottom_toBottomOf="@id/text_author"
app:layout_constraintLeft_toRightOf="@id/text_author"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/text_author"
android:text=" • 3 years ago" />
<com.futo.platformplayer.views.behavior.NonScrollingTextView <com.futo.platformplayer.views.others.CreatorThumbnail
android:id="@+id/text_body" android:id="@+id/image_thumbnail"
android:layout_width="0dp" android:layout_width="25dp"
android:layout_height="wrap_content" android:layout_height="25dp"
android:layout_marginTop="5dp" android:contentDescription="@string/channel_image"
android:layout_marginStart="10dp" app:layout_constraintLeft_toLeftOf="parent"
android:background="@color/transparent" app:layout_constraintTop_toTopOf="parent"
android:fontFamily="@font/inter_regular" tools:src="@drawable/placeholder_channel_thumbnail" />
android:isScrollContainer="false"
android:textColor="#CCCCCC"
android:textSize="13sp"
android:maxLines="3"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@id/text_metadata"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintRight_toRightOf="parent"
android:text="@string/lorem_ipsum" />
</androidx.constraintlayout.widget.ConstraintLayout> <TextView
android:id="@+id/text_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:fontFamily="@font/inter_regular"
android:textColor="@color/white"
android:textSize="14sp"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintTop_toTopOf="@id/image_thumbnail"
tools:text="ShortCircuit" />
<com.futo.platformplayer.views.comments.AddCommentView <TextView
android:id="@+id/add_comment_view" android:id="@+id/text_metadata"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:fontFamily="@font/inter_regular"
android:textColor="@color/gray_ac"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@id/text_author"
app:layout_constraintLeft_toRightOf="@id/text_author"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/text_author"
tools:text=" • 3 years ago" />
<com.futo.platformplayer.views.behavior.NonScrollingTextView
android:id="@+id/text_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginStart="10dp"
android:background="@color/transparent"
android:fontFamily="@font/inter_regular"
android:isScrollContainer="false"
android:textColor="#CCCCCC"
android:textSize="13sp"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@id/text_metadata"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintRight_toRightOf="parent"
tools:text="@string/lorem_ipsum" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.futo.platformplayer.views.segments.CommentsList
android:id="@+id/comments_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="12dp" />
</LinearLayout>
<com.futo.platformplayer.views.overlays.LoaderOverlay
android:id="@+id/loader_overlay"
android:visibility="gone"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_marginTop="12dp" android:clickable="true" />
android:layout_marginStart="12dp" </FrameLayout>
android:layout_marginEnd="12dp"
app:layout_constraintTop_toBottomOf="@id/layout_parent_comment"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<com.futo.platformplayer.views.segments.CommentsList
android:id="@+id/comments_list"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/add_comment_view"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="12dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/slide_up_menu_recycler"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
+21 -1
View File
@@ -7,5 +7,25 @@
android:id="@+id/recycler_comments" android:id="@+id/recycler_comments"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="12dp"
android:paddingBottom="7dp"
android:paddingEnd="14dp"
android:paddingTop="7dp"
android:paddingStart="14dp"
android:background="@drawable/background_pill"
android:id="@+id/layout_scroll_to_top">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/scroll_to_top"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular"
android:textSize="14dp"/>
</FrameLayout>
</FrameLayout> </FrameLayout>
@@ -152,4 +152,47 @@
android:textSize="16dp"/> android:textSize="16dp"/>
</FrameLayout> </FrameLayout>
<FrameLayout
android:id="@+id/layout_controls_zoom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="@drawable/background_gesture_controls"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:visibility="gone">
<TextView
android:id="@+id/text_zoom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_regular"
tools:text="@string/zoom"
android:textColor="@color/white"
android:textSize="16dp"/>
</FrameLayout>
<FrameLayout
android:id="@+id/layout_indicator_fill"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_primary_border"
android:visibility="gone" />
<FrameLayout
android:id="@+id/layout_indicator_fit"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@drawable/background_primary_border"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
@@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="50dp" android:layout_height="wrap_content"
android:paddingTop="6dp" android:paddingTop="6dp"
android:paddingBottom="7dp" android:paddingBottom="7dp"
android:paddingStart="7dp" android:paddingStart="7dp"
@@ -21,19 +21,36 @@
android:scaleType="fitCenter" android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_thumb_up" /> app:srcCompat="@drawable/ic_thumb_up" />
<TextView <LinearLayout
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="@color/white"
android:textSize="16sp"
android:gravity="center_vertical"
android:fontFamily="@font/inter_light"
tools:text="500K" />
<Space android:layout_height="match_parent"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_weight="1" /> android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="16sp"
android:maxLines="1"
android:ellipsize="end"
android:gravity="center_vertical"
android:fontFamily="@font/inter_light"
tools:text="500K" />
<TextView
android:id="@+id/text_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#848484"
android:textSize="16sp"
android:maxLines="2"
android:ellipsize="end"
android:gravity="center_vertical"
android:fontFamily="@font/inter_light"
tools:text="500K" />
</LinearLayout>
<ImageView <ImageView
android:id="@+id/image_suffix" android:id="@+id/image_suffix"
+24
View File
@@ -214,6 +214,7 @@
<string name="videos">Videos</string> <string name="videos">Videos</string>
<string name="clear_history">Clear history</string> <string name="clear_history">Clear history</string>
<string name="nothing_to_import">Nothing to import</string> <string name="nothing_to_import">Nothing to import</string>
<string name="no_sources_installed">You have no sources installed, please add sources to use the app as intended.</string>
<string name="enabling_lots_of_sources_can_reduce_the_loading_speed_of_your_application">Enabling lots of sources can reduce the loading speed of your application.</string> <string name="enabling_lots_of_sources_can_reduce_the_loading_speed_of_your_application">Enabling lots of sources can reduce the loading speed of your application.</string>
<string name="support">Support</string> <string name="support">Support</string>
<string name="membership">Membership</string> <string name="membership">Membership</string>
@@ -344,6 +345,23 @@
<string name="get_answers_to_common_questions">Get answers to common questions</string> <string name="get_answers_to_common_questions">Get answers to common questions</string>
<string name="give_feedback_on_the_application">Give feedback on the application</string> <string name="give_feedback_on_the_application">Give feedback on the application</string>
<string name="info">Info</string> <string name="info">Info</string>
<string name="gesture_controls">Gesture controls</string>
<string name="volume_slider">Volume slider</string>
<string name="volume_slider_descr">Enable slide gesture to change volume</string>
<string name="brightness_slider">Brightness slider</string>
<string name="brightness_slider_descr">Enable slide gesture to change brightness</string>
<string name="toggle_full_screen">Toggle full screen</string>
<string name="toggle_full_screen_descr">Enable swipe gesture to toggle fullscreen</string>
<string name="system_brightness">System brightness</string>
<string name="system_brightness_descr">Gesture controls adjust system brightness</string>
<string name="restore_system_brightness">Restore system brightness</string>
<string name="restore_system_brightness_descr">Restore system brightness when exiting fullscreen</string>
<string name="zoom_option">Enable zoom</string>
<string name="zoom_option_descr">Enable two finger pinch zoom gesture</string>
<string name="pan_option">Enable pan</string>
<string name="pan_option_descr">Enable two finger pan gesture</string>
<string name="system_volume">System volume</string>
<string name="system_volume_descr">Gesture controls adjust system volume</string>
<string name="live_chat_webview">Live Chat Webview</string> <string name="live_chat_webview">Live Chat Webview</string>
<string name="full_screen_portrait">Fullscreen portrait</string> <string name="full_screen_portrait">Fullscreen portrait</string>
<string name="allow_full_screen_portrait">Allow fullscreen portrait</string> <string name="allow_full_screen_portrait">Allow fullscreen portrait</string>
@@ -467,6 +485,8 @@
<string name="various_tests_against_a_custom_source">Various tests against a custom source</string> <string name="various_tests_against_a_custom_source">Various tests against a custom source</string>
<string name="writes_to_disk_till_no_space_is_left">Writes to disk till no space is left</string> <string name="writes_to_disk_till_no_space_is_left">Writes to disk till no space is left</string>
<string name="visibility">Visibility</string> <string name="visibility">Visibility</string>
<string name="check_for_updates_setting">Check for updates</string>
<string name="check_for_updates_setting_description">If a plugin should be checked for updates on startup</string>
<string name="ratelimit">Rate-limit</string> <string name="ratelimit">Rate-limit</string>
<string name="ratelimit_description">Settings related to rate-limiting this plugin\'s behavior</string> <string name="ratelimit_description">Settings related to rate-limiting this plugin\'s behavior</string>
<string name="ratelimit_sub_setting">Rate-limit Subscriptions</string> <string name="ratelimit_sub_setting">Rate-limit Subscriptions</string>
@@ -605,6 +625,7 @@
<string name="you_have_too_many_subscriptions_for_the_following_plugins">\n\nYou have too many subscriptions for the following plugins:\n</string> <string name="you_have_too_many_subscriptions_for_the_following_plugins">\n\nYou have too many subscriptions for the following plugins:\n</string>
<string name="posts">Posts</string> <string name="posts">Posts</string>
<string name="planned">Planned</string> <string name="planned">Planned</string>
<string name="watched">Watched</string>
<string name="no_results_found_swipe_down_to_refresh">No results found\nSwipe down to refresh</string> <string name="no_results_found_swipe_down_to_refresh">No results found\nSwipe down to refresh</string>
<string name="overlay">Overlay</string> <string name="overlay">Overlay</string>
<string name="reload">Reload</string> <string name="reload">Reload</string>
@@ -728,6 +749,9 @@
<string name="do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button">Do you want to see the tutorials? You can find them at any time through the more button.</string> <string name="do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button">Do you want to see the tutorials? You can find them at any time through the more button.</string>
<string name="add_creator">Add Creators</string> <string name="add_creator">Add Creators</string>
<string name="select">Select</string> <string name="select">Select</string>
<string name="zoom">Zoom</string>
<string name="check_to_see_if_an_update_is_available">Check to see if an update is available.</string>
<string name="scroll_to_top">Scroll to top</string>
<string-array name="home_screen_array"> <string-array name="home_screen_array">
<item>Recommendations</item> <item>Recommendations</item>
<item>Subscriptions</item> <item>Subscriptions</item>
@@ -2,7 +2,8 @@
<resources> <resources>
<declare-styleable name="WidePillButton"> <declare-styleable name="WidePillButton">
<attr name="widePillIconPrefix" format="reference" /> <attr name="widePillIconPrefix" format="reference" />
<attr name="widePilllText" format="string" /> <attr name="widePillText" format="string" />
<attr name="widePillDescription" format="string" />
<attr name="widePillIconSuffix" format="reference" /> <attr name="widePillIconSuffix" format="reference" />
</declare-styleable> </declare-styleable>
</resources> </resources>
+2 -4
View File
@@ -1,7 +1,5 @@
{ {
"SOURCES_EMBEDDED": { "SOURCES_EMBEDDED": {},
"1c291164-294c-4c2d-800d-7bc6d31d0019": "sources/peertube/PeerTubeConfig.json" "SOURCES_EMBEDDED_DEFAULT": [],
},
"SOURCES_EMBEDDED_DEFAULT": ["1c291164-294c-4c2d-800d-7bc6d31d0019"],
"SOURCES_UNDER_CONSTRUCTION": {} "SOURCES_UNDER_CONSTRUCTION": {}
} }
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# Array of directories to look in # Array of directories to look in
dirs=("app/src/unstable/assets/sources" "app/src/stable/assets/sources" "app/src/playstore/assets/sources") dirs=("app/src/unstable/assets/sources" "app/src/stable/assets/sources")
# Loop through each directory # Loop through each directory
for dir in "${dirs[@]}"; do for dir in "${dirs[@]}"; do