mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45d818ac81 | |||
| 7729681829 | |||
| b12d04b27d | |||
| e6608b9a5c | |||
| 2d503dfaf6 | |||
| 08934ef8de | |||
| 62d927739a | |||
| c8db8f58e8 | |||
| 0fc966a77d | |||
| 9f6c6c8cf3 | |||
| 43a6ff138c | |||
| 269a3460e7 | |||
| 18150e9e15 | |||
| 362c7f5b2c | |||
| 2adb8ad7f9 | |||
| 6b5d4e7507 | |||
| 49c82726f0 | |||
| c8ddcda384 | |||
| b75217f789 | |||
| 8ba8e535bd | |||
| e4c574db6b | |||
| fae73293d7 | |||
| 3bd0aac4f8 | |||
| 26b822e04b | |||
| 96b9b8843c | |||
| 6d9c1e17b5 | |||
| 507ad105c0 | |||
| 40a283017e | |||
| be14597670 | |||
| 837609abb9 | |||
| d64cd98b43 | |||
| 0081ff1483 | |||
| f78ca6c7ed | |||
| cfc7cbcaa4 | |||
| e533eb7778 | |||
| 7c1d0a7f88 | |||
| 01ef471708 | |||
| 2fd0a9a41d | |||
| 635749dfe4 | |||
| c4bd5626f3 | |||
| 568a0f6329 | |||
| 7ee67b5cd0 | |||
| fc94c6903c | |||
| a0af8805e7 | |||
| 9b64cde17d | |||
| f6931bcf8c | |||
| a4ff47d863 | |||
| 982d251126 | |||
| 8820a0ecc0 | |||
| b99a713ffc | |||
| dfc8c4b740 | |||
| c3df9e5259 | |||
| b9c7e0a8ca | |||
| 2c7f02a24d | |||
| 5cc8488d94 | |||
| 6f7304f59c | |||
| ea4fea4401 | |||
| 9b48664de4 | |||
| 8964dc68f0 | |||
| 4711b8055b | |||
| 84e3373fa7 | |||
| fdd7e32dd8 | |||
| e57119ebbd | |||
| ed29dd8365 | |||
| 196cacb452 | |||
| c025913fc8 | |||
| 48b2c68e72 | |||
| 689766a6ac | |||
| 9306024d17 | |||
| 195163840b | |||
| 788c54bf8f | |||
| 031aabd523 | |||
| 85db4cc4e6 | |||
| 745aad385b | |||
| ba87261f9f | |||
| 7d091382c0 | |||
| 781d0797e7 | |||
| ec12a06b88 | |||
| bf3e8867c3 | |||
| 176814a715 | |||
| 898637a616 | |||
| f1860126a7 | |||
| f8402676d7 | |||
| cf86ce1ab3 | |||
| f4cb1719e0 | |||
| 4898cb53ae |
@@ -1,9 +1,6 @@
|
||||
[submodule "dep/polycentricandroid"]
|
||||
path = dep/polycentricandroid
|
||||
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"]
|
||||
path = app/src/stable/assets/sources/kick
|
||||
url = ../plugins/kick.git
|
||||
|
||||
+10
-10
@@ -151,7 +151,7 @@ dependencies {
|
||||
//Core
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
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'
|
||||
|
||||
//Images
|
||||
@@ -172,15 +172,15 @@ dependencies {
|
||||
implementation("com.caoccao.javet:javet-android:3.0.2")
|
||||
|
||||
//Exoplayer
|
||||
implementation 'androidx.media3:media3-exoplayer:1.2.0'
|
||||
implementation 'androidx.media3:media3-exoplayer-dash:1.2.0'
|
||||
implementation 'androidx.media3:media3-ui:1.2.0'
|
||||
implementation 'androidx.media3:media3-exoplayer-hls:1.2.0'
|
||||
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.0'
|
||||
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.0'
|
||||
implementation 'androidx.media3:media3-transformer:1.2.0'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.5'
|
||||
implementation 'androidx.media3:media3-exoplayer:1.2.1'
|
||||
implementation 'androidx.media3:media3-exoplayer-dash:1.2.1'
|
||||
implementation 'androidx.media3:media3-ui:1.2.1'
|
||||
implementation 'androidx.media3:media3-exoplayer-hls:1.2.1'
|
||||
implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.1'
|
||||
implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.1'
|
||||
implementation 'androidx.media3:media3-transformer:1.2.1'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
|
||||
implementation 'androidx.media:media:1.7.0'
|
||||
|
||||
//Other
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<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_DATA_SYNC"/>
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
||||
@@ -233,6 +233,9 @@ function pluginRemoteProp(objID, propName) {
|
||||
function pluginRemoteCall(objID, methodName, 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) {
|
||||
fetch("/plugin/isLoggedIn", {
|
||||
|
||||
@@ -385,8 +385,8 @@
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div style="width: 50%" v-if="Plugin.currentPlugin">
|
||||
<!--Get Home-->
|
||||
<v-card class="requestCard" v-for="req in Testing.requests">
|
||||
<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-show="req.title.indexOf(searchTestMethods) >= 0">
|
||||
<v-card-text>
|
||||
<div class="title">
|
||||
<span v-if="req.isOptional">(Optional)</span>
|
||||
@@ -416,6 +416,9 @@
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="testSourceRemotely(req)">
|
||||
Test Android
|
||||
</v-btn>
|
||||
<v-btn @click="testSource(req)">
|
||||
Test
|
||||
</v-btn>
|
||||
@@ -545,6 +548,7 @@
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
searchTestMethods: "",
|
||||
page: "Plugin",
|
||||
pastPluginUrls: [],
|
||||
settings: {},
|
||||
@@ -860,6 +864,53 @@
|
||||
"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) {
|
||||
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import android.util.Log
|
||||
import com.google.common.base.CharMatcher
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
@@ -215,17 +216,20 @@ private fun ByteArray.toInetAddress(): InetAddress {
|
||||
}
|
||||
|
||||
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
||||
val timeout = 5000
|
||||
val timeout = 2000
|
||||
|
||||
if (addresses.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (addresses.size == 1) {
|
||||
val socket = Socket()
|
||||
|
||||
try {
|
||||
return Socket().apply { this.connect(InetSocketAddress(addresses[0], port), timeout) };
|
||||
return socket.apply { this.connect(InetSocketAddress(addresses[0], port), timeout) }
|
||||
} catch (e: Throwable) {
|
||||
//Ignored.
|
||||
Log.i("getConnectedSocket", "Failed to connect to: ${addresses[0]}", e)
|
||||
socket.close()
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -264,7 +268,7 @@ fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
//Ignore
|
||||
Log.i("getConnectedSocket", "Failed to connect to: $address", e)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -809,7 +809,36 @@ class Settings : FragmentedStorageFileJson() {
|
||||
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();
|
||||
@Serializable
|
||||
class Info {
|
||||
|
||||
@@ -304,12 +304,16 @@ class UIDialogs {
|
||||
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);
|
||||
registerDialogOpened(dialog);
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
dialog.setMaxVersion(lastVersion);
|
||||
|
||||
if (hideExceptionButtons) {
|
||||
dialog.hideExceptionButtons()
|
||||
}
|
||||
}
|
||||
|
||||
fun showChangelogDialog(context: Context, lastVersion: Int) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.activities.SettingsActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
@@ -38,11 +38,17 @@ import com.futo.platformplayer.states.StateMeta
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.views.AnyAdapterView
|
||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuFilters
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuRecycler
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
||||
import com.futo.platformplayer.views.pills.RoundButton
|
||||
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
||||
@@ -68,7 +74,7 @@ class UISlideOverlays {
|
||||
return menu;
|
||||
}
|
||||
|
||||
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) {
|
||||
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
||||
val items = arrayListOf<View>();
|
||||
|
||||
val originalNotif = subscription.doNotifications;
|
||||
@@ -77,20 +83,48 @@ class UISlideOverlays {
|
||||
val originalVideo = subscription.doFetchVideos;
|
||||
val originalPosts = subscription.doFetchPosts;
|
||||
|
||||
val menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, listOf());
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
|
||||
val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
|
||||
val capabilities = plugin.getChannelCapabilities();
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
var menu: SlideUpMenuOverlay? = null;
|
||||
|
||||
|
||||
items.addAll(listOf(
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
|
||||
subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
|
||||
}, false),
|
||||
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||
SlideUpMenuGroup(container.context, "Subscription Groups",
|
||||
"You can select which groups this subscription is part of.",
|
||||
-1, listOf()) else null,
|
||||
if(StateSubscriptionGroups.instance.getSubscriptionGroups().isNotEmpty())
|
||||
SlideUpMenuRecycler(container.context, "as") {
|
||||
val groups = ArrayList<SubscriptionGroup>(StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
|
||||
.sortedBy { !it.selected });
|
||||
var adapter: AnyAdapterView<SubscriptionGroup, SubscriptionGroupBarViewHolder>? = null;
|
||||
adapter = it.asAny(groups, RecyclerView.HORIZONTAL) {
|
||||
it.onClick.subscribe {
|
||||
if(it is SubscriptionGroup.Selectable) {
|
||||
val actualGroup = StateSubscriptionGroups.instance.getSubscriptionGroup(it.id)
|
||||
?: return@subscribe;
|
||||
groups.clear();
|
||||
if(it.selected)
|
||||
actualGroup.urls.remove(subscription.channel.url);
|
||||
else
|
||||
actualGroup.urls.add(subscription.channel.url);
|
||||
|
||||
StateSubscriptionGroups.instance.updateSubscriptionGroup(actualGroup);
|
||||
groups.addAll(StateSubscriptionGroups.instance.getSubscriptionGroups()
|
||||
.map { SubscriptionGroup.Selectable(it, it.urls.contains(subscription.channel.url)) }
|
||||
.sortedBy { !it.selected });
|
||||
adapter?.notifyContentChanged();
|
||||
}
|
||||
}
|
||||
};
|
||||
return@SlideUpMenuRecycler adapter;
|
||||
} else null,
|
||||
SlideUpMenuGroup(container.context, "Fetch Settings",
|
||||
"Depending on the platform you might not need to enable a type for it to be available.",
|
||||
-1, listOf()),
|
||||
@@ -119,7 +153,7 @@ class UISlideOverlays {
|
||||
}, false)*/
|
||||
).filterNotNull());
|
||||
|
||||
menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
|
||||
menu.setItems(items);
|
||||
|
||||
if(subscription.doNotifications)
|
||||
menu.selectOption(null, "notifications", true, true);
|
||||
@@ -174,6 +208,8 @@ class UISlideOverlays {
|
||||
menu.show();
|
||||
}
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {
|
||||
@@ -647,9 +683,17 @@ class UISlideOverlays {
|
||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||
(listOf(
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), container.context.getString(R.string.download), {
|
||||
showDownloadVideoOverlay(video, container, true);
|
||||
}, false),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_download, container.context.getString(R.string.download), container.context.getString(R.string.download_the_video), "download", {
|
||||
showDownloadVideoOverlay(video, container, true);
|
||||
}, false),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_share, container.context.getString(R.string.share), "Share the video", "share", {
|
||||
val url = if(video.shareUrl.isNotEmpty()) video.shareUrl else video.url;
|
||||
container.context.startActivity(Intent.createChooser(Intent().apply {
|
||||
action = Intent.ACTION_SEND;
|
||||
putExtra(Intent.EXTRA_TEXT, url);
|
||||
type = "text/plain";
|
||||
}, null));
|
||||
}, false),
|
||||
SlideUpMenuItem(container.context, R.drawable.ic_visibility_off, container.context.getString(R.string.hide_creator_from_home), "", "hide_creator", {
|
||||
StateMeta.instance.addHiddenCreator(video.author.url);
|
||||
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
|
||||
@@ -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>();
|
||||
|
||||
@@ -718,6 +762,13 @@ class UISlideOverlays {
|
||||
);
|
||||
|
||||
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) {
|
||||
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 androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
@@ -37,8 +38,10 @@ class AddSourceActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var _sourceHeader: SourceHeaderView;
|
||||
|
||||
|
||||
private lateinit var _sourcePermissions: LinearLayout;
|
||||
private lateinit var _sourceWarnings: LinearLayout;
|
||||
private lateinit var _sourceWarningsContainer: LinearLayout;
|
||||
|
||||
private lateinit var _container: ScrollView;
|
||||
private lateinit var _loader: ImageView;
|
||||
@@ -79,6 +82,7 @@ class AddSourceActivity : AppCompatActivity() {
|
||||
|
||||
_sourcePermissions = findViewById(R.id.source_permissions);
|
||||
_sourceWarnings = findViewById(R.id.source_warnings);
|
||||
_sourceWarningsContainer = findViewById(R.id.container_source_warnings);
|
||||
|
||||
_container = findViewById(R.id.configContainer);
|
||||
_loader = findViewById(R.id.loader);
|
||||
@@ -203,21 +207,28 @@ class AddSourceActivity : AppCompatActivity() {
|
||||
|
||||
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(
|
||||
SourceInfoView(this,
|
||||
R.drawable.ic_security_pred,
|
||||
warning.first,
|
||||
warning.second)
|
||||
.withDescriptionColor(pastelRed));
|
||||
_sourceWarningsContainer.isVisible = warnings.isNotEmpty();
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
fun install(config: SourcePluginConfig, script: String) {
|
||||
val isNew = !StatePlatform.instance.getAvailableClients().any { it.id == config.id };
|
||||
StatePlugins.instance.installPlugin(this, lifecycleScope, config, script) {
|
||||
if(it) {
|
||||
StatePlatform.instance.clearUpdateAvailable(config)
|
||||
if(isNew)
|
||||
lifecycleScope.launch {
|
||||
StatePlatform.instance.enableClient(listOf(config.id));
|
||||
}
|
||||
backToSources();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
||||
lateinit var _buttonBack: ImageButton;
|
||||
|
||||
lateinit var _buttonQR: BigButton;
|
||||
lateinit var _buttonBrowse: BigButton;
|
||||
lateinit var _buttonURL: BigButton;
|
||||
lateinit var _buttonPlugins: BigButton;
|
||||
|
||||
@@ -56,6 +57,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
||||
_buttonBack = findViewById(R.id.button_back);
|
||||
|
||||
_buttonQR = findViewById(R.id.option_qr);
|
||||
_buttonBrowse = findViewById(R.id.option_browse);
|
||||
_buttonURL = findViewById(R.id.option_url);
|
||||
_buttonPlugins = findViewById(R.id.option_plugins);
|
||||
|
||||
@@ -74,6 +76,9 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
||||
integrator.setCaptureActivity(QRCaptureActivity::class.java);
|
||||
_qrCodeResultLauncher.launch(integrator.createScanIntent())
|
||||
}
|
||||
_buttonBrowse.onClick.subscribe {
|
||||
startActivity(MainActivity.getTabIntent(this, "BROWSE_PLUGINS"));
|
||||
}
|
||||
|
||||
_buttonURL.onClick.subscribe {
|
||||
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
@@ -15,6 +16,7 @@ import com.futo.platformplayer.logging.LogLevel
|
||||
import com.futo.platformplayer.logging.Logging
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateUpdate
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -28,6 +30,7 @@ class ExceptionActivity : AppCompatActivity() {
|
||||
private lateinit var _buttonSubmit: LinearLayout;
|
||||
private lateinit var _buttonRestart: LinearLayout;
|
||||
private lateinit var _buttonClose: LinearLayout;
|
||||
private lateinit var _buttonCheckForUpdates: LinearLayout;
|
||||
private var _file: File? = null;
|
||||
private var _submitted = false;
|
||||
|
||||
@@ -45,6 +48,7 @@ class ExceptionActivity : AppCompatActivity() {
|
||||
_buttonSubmit = findViewById(R.id.button_submit);
|
||||
_buttonRestart = findViewById(R.id.button_restart);
|
||||
_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 stack = intent.getStringExtra(EXTRA_STACK) ?: getString(R.string.something_went_wrong_missing_stack_trace);
|
||||
@@ -83,6 +87,17 @@ class ExceptionActivity : AppCompatActivity() {
|
||||
_buttonClose.setOnClickListener {
|
||||
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() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
@@ -39,6 +40,7 @@ class LoginActivity : AppCompatActivity() {
|
||||
_textUrl = findViewById(R.id.text_url);
|
||||
_buttonClose = findViewById(R.id.button_close);
|
||||
_buttonClose.setOnClickListener {
|
||||
UIDialogs.toast("Login cancelled", false);
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||
@@ -141,7 +142,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
}
|
||||
|
||||
try {
|
||||
handleUrlAll(content)
|
||||
runBlocking {
|
||||
handleUrlAll(content)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.i(TAG, "Failed to handle URL.", e)
|
||||
UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
|
||||
@@ -535,13 +538,28 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
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 {
|
||||
if (targetData != null) {
|
||||
handleUrlAll(targetData)
|
||||
runBlocking {
|
||||
handleUrlAll(targetData)
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
when (uri.scheme) {
|
||||
"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)")
|
||||
|
||||
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
||||
navigate(_fragVideoDetail, url);
|
||||
_fragVideoDetail.maximizeVideoDetail(true);
|
||||
return true;
|
||||
} else if(StatePlatform.instance.hasEnabledChannelClient(url)) {
|
||||
navigate(_fragMainChannel, url);
|
||||
return withContext(Dispatchers.IO) {
|
||||
Logger.i(TAG, "handleUrl(url=$url) on IO");
|
||||
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
||||
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
navigate(_fragVideoDetail, url);
|
||||
|
||||
lifecycleScope.launch {
|
||||
delay(100);
|
||||
_fragVideoDetail.minimizeVideoDetail();
|
||||
};
|
||||
return true;
|
||||
_fragVideoDetail.maximizeVideoDetail(true);
|
||||
}
|
||||
return@withContext true;
|
||||
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
|
||||
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 {
|
||||
Logger.i(TAG, "handleContent(url=$file)");
|
||||
@@ -802,11 +835,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
if(_fragBotBarMenu.onBackPressed())
|
||||
return;
|
||||
|
||||
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED &&
|
||||
_fragVideoDetail.onBackPressed())
|
||||
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed())
|
||||
return;
|
||||
|
||||
|
||||
if(!fragCurrent.onBackPressed())
|
||||
closeSegment();
|
||||
}
|
||||
|
||||
+8
-2
@@ -12,14 +12,13 @@ import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.polycentric.core.ProcessHandle
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.Synchronization
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -71,6 +70,13 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
||||
try {
|
||||
processHandle = ProcessHandle.create();
|
||||
Store.instance.addProcessSecret(processHandle.processSecret);
|
||||
|
||||
try {
|
||||
PolycentricStorage.instance.addProcessSecret(processHandle.processSecret)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||
}
|
||||
|
||||
processHandle.addServer("https://srv1-stg.polycentric.io");
|
||||
processHandle.setUsername(username);
|
||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||
|
||||
+7
@@ -13,6 +13,7 @@ import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
@@ -126,6 +127,12 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
val processSecret = ProcessSecret(keyPair, Process.random());
|
||||
Store.instance.addProcessSecret(processSecret);
|
||||
|
||||
try {
|
||||
PolycentricStorage.instance.addProcessSecret(processSecret)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||
}
|
||||
|
||||
val processHandle = processSecret.toProcessHandle();
|
||||
|
||||
for (e in exportBundle.events.eventsList) {
|
||||
|
||||
+5
-2
@@ -19,8 +19,9 @@ class PolycentricPlatformComment : IPlatformComment {
|
||||
|
||||
val eventPointer: Pointer;
|
||||
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.author = author;
|
||||
this.message = msg;
|
||||
@@ -29,6 +30,7 @@ class PolycentricPlatformComment : IPlatformComment {
|
||||
this.replyCount = replyCount;
|
||||
this.eventPointer = eventPointer;
|
||||
this.reference = eventPointer.toReference();
|
||||
this.parentReference = parentReference;
|
||||
}
|
||||
|
||||
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
|
||||
@@ -36,10 +38,11 @@ class PolycentricPlatformComment : IPlatformComment {
|
||||
}
|
||||
|
||||
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 {
|
||||
private const val TAG = "PolycentricPlatformComment"
|
||||
val MAX_COMMENT_SIZE = 2000
|
||||
}
|
||||
}
|
||||
+1
@@ -11,4 +11,5 @@ class SourcePluginAuthConfig(
|
||||
val userAgent: String? = null,
|
||||
val loginButton: String? = null,
|
||||
val domainHeadersToFind: Map<String, List<String>>? = null,
|
||||
val loginWarning: String? = null
|
||||
) { }
|
||||
+26
-2
@@ -2,6 +2,9 @@ package com.futo.platformplayer.api.media.platforms.js
|
||||
|
||||
import com.futo.platformplayer.R
|
||||
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.FieldForm
|
||||
import com.futo.platformplayer.views.fields.FormField
|
||||
@@ -55,7 +58,16 @@ class SourcePluginDescriptor {
|
||||
onCaptchaChanged.emit();
|
||||
}
|
||||
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?) {
|
||||
@@ -63,12 +75,24 @@ class SourcePluginDescriptor {
|
||||
onAuthChanged.emit();
|
||||
}
|
||||
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
|
||||
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)
|
||||
var tabEnabled = TabEnabled();
|
||||
@Serializable
|
||||
|
||||
@@ -328,6 +328,8 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
|
||||
val factory = sslContext.socketFactory;
|
||||
|
||||
val address = InetSocketAddress(usedRemoteAddress, port)
|
||||
|
||||
//Connection loop
|
||||
while (_scopeIO?.isActive == true) {
|
||||
Logger.i(TAG, "Connecting to Chromecast.");
|
||||
@@ -341,7 +343,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
connectedSocket = null
|
||||
} else {
|
||||
Logger.i(TAG, "Using new socket.")
|
||||
val s = Socket().apply { this.connect(InetSocketAddress(usedRemoteAddress, port), 5000) }
|
||||
val s = Socket().apply { this.connect(address, 2000) }
|
||||
_socket = factory.createSocket(s, s.inetAddress.hostAddress, s.port, true) as SSLSocket
|
||||
}
|
||||
|
||||
@@ -444,10 +446,11 @@ class ChromecastCastingDevice : CastingDevice {
|
||||
while (_scopeIO?.isActive == true) {
|
||||
try {
|
||||
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.heartbeat", pingObject.toString());
|
||||
Thread.sleep(5000);
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "Failed to send ping.");
|
||||
}
|
||||
|
||||
Thread.sleep(5000);
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Stopped ping loop.");
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.futo.platformplayer.casting.models.FCastSetSpeedMessage
|
||||
import com.futo.platformplayer.casting.models.FCastSetVolumeMessage
|
||||
import com.futo.platformplayer.casting.models.FCastVersionMessage
|
||||
import com.futo.platformplayer.casting.models.FCastVolumeUpdateMessage
|
||||
import com.futo.platformplayer.ensureNotMainThread
|
||||
import com.futo.platformplayer.getConnectedSocket
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||
@@ -27,9 +28,9 @@ import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.math.BigInteger
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
@@ -82,12 +83,15 @@ class FCastCastingDevice : CastingDevice {
|
||||
var port: Int = 0;
|
||||
|
||||
private var _socket: Socket? = null;
|
||||
private var _outputStream: DataOutputStream? = null;
|
||||
private var _inputStream: DataInputStream? = null;
|
||||
private var _outputStream: OutputStream? = null;
|
||||
private var _inputStream: InputStream? = null;
|
||||
private var _scopeIO: CoroutineScope? = null;
|
||||
private var _started: Boolean = false;
|
||||
private var _version: Long = 1;
|
||||
private var _thread: Thread? = null
|
||||
private var _pingThread: Thread? = null
|
||||
private var _lastPongTime = -1L
|
||||
private var _outputStreamLock = Object()
|
||||
|
||||
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
||||
this.name = name;
|
||||
@@ -207,7 +211,13 @@ class FCastCastingDevice : CastingDevice {
|
||||
|
||||
private fun invokeInIOScopeIfRequired(action: () -> Unit): Boolean {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -241,7 +251,8 @@ class FCastCastingDevice : CastingDevice {
|
||||
val adrs = addresses ?: return;
|
||||
|
||||
val thread = _thread
|
||||
if (thread == null || !thread.isAlive) {
|
||||
val pingThread = _pingThread
|
||||
if (_started && (thread == null || !thread.isAlive || pingThread == null || !pingThread.isAlive)) {
|
||||
Log.i(TAG, "(Re)starting thread because the thread has died")
|
||||
|
||||
_scopeIO?.let {
|
||||
@@ -258,7 +269,7 @@ class FCastCastingDevice : CastingDevice {
|
||||
var connectedSocket: Socket? = null
|
||||
while (_scopeIO?.isActive == true) {
|
||||
try {
|
||||
Log.i(TAG, "getConnectedSocket.")
|
||||
Log.i(TAG, "getConnectedSocket (adrs = [ ${adrs.joinToString(", ")} ], port = ${port}).")
|
||||
|
||||
val resultSocket = getConnectedSocket(adrs.toList(), port);
|
||||
|
||||
@@ -279,6 +290,8 @@ class FCastCastingDevice : CastingDevice {
|
||||
}
|
||||
}
|
||||
|
||||
val address = InetSocketAddress(usedRemoteAddress, port)
|
||||
|
||||
//Connection loop
|
||||
while (_scopeIO?.isActive == true) {
|
||||
Logger.i(TAG, "Connecting to FastCast.");
|
||||
@@ -286,20 +299,24 @@ class FCastCastingDevice : CastingDevice {
|
||||
|
||||
try {
|
||||
_socket?.close()
|
||||
_inputStream?.close()
|
||||
_outputStream?.close()
|
||||
if (connectedSocket != null) {
|
||||
Logger.i(TAG, "Using connected socket.");
|
||||
_socket = connectedSocket
|
||||
connectedSocket = null
|
||||
} else {
|
||||
Logger.i(TAG, "Using new socket.");
|
||||
_socket = Socket().apply { this.connect(InetSocketAddress(usedRemoteAddress, port), 5000) };
|
||||
_socket = Socket().apply { this.connect(address, 2000) };
|
||||
}
|
||||
Logger.i(TAG, "Successfully connected to FastCast at $usedRemoteAddress:$port");
|
||||
|
||||
_outputStream = DataOutputStream(_socket?.outputStream);
|
||||
_inputStream = DataInputStream(_socket?.inputStream);
|
||||
_outputStream = _socket?.outputStream;
|
||||
_inputStream = _socket?.inputStream;
|
||||
} catch (e: IOException) {
|
||||
_socket?.close();
|
||||
_socket?.close()
|
||||
_inputStream?.close()
|
||||
_outputStream?.close()
|
||||
Logger.i(TAG, "Failed to connect to FastCast.", e);
|
||||
|
||||
connectionState = CastConnectionState.CONNECTING;
|
||||
@@ -309,28 +326,38 @@ class FCastCastingDevice : CastingDevice {
|
||||
|
||||
localAddress = _socket?.localAddress;
|
||||
connectionState = CastConnectionState.CONNECTED;
|
||||
_lastPongTime = -1L
|
||||
|
||||
val buffer = ByteArray(4096);
|
||||
|
||||
Logger.i(TAG, "Started receiving.");
|
||||
var exceptionOccurred = false;
|
||||
while (_scopeIO?.isActive == true && !exceptionOccurred) {
|
||||
while (_scopeIO?.isActive == true) {
|
||||
try {
|
||||
val inputStream = _inputStream ?: break;
|
||||
Log.d(TAG, "Receiving next packet...");
|
||||
val b1 = inputStream.readUnsignedByte();
|
||||
val b2 = inputStream.readUnsignedByte();
|
||||
val b3 = inputStream.readUnsignedByte();
|
||||
val b4 = inputStream.readUnsignedByte();
|
||||
val size = ((b4.toLong() shl 24) or (b3.toLong() shl 16) or (b2.toLong() shl 8) or b1.toLong()).toInt();
|
||||
|
||||
var headerBytesRead = 0
|
||||
while (headerBytesRead < 4) {
|
||||
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();
|
||||
if (size > buffer.size) {
|
||||
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
|
||||
inputStream.skip(size.toLong());
|
||||
continue;
|
||||
Logger.w(TAG, "Packets larger than $size bytes are not supported.")
|
||||
break
|
||||
}
|
||||
|
||||
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
||||
inputStream.read(buffer, 0, size);
|
||||
var bytesRead = 0
|
||||
while (bytesRead < size) {
|
||||
val read = inputStream.read(buffer, bytesRead, size - bytesRead)
|
||||
if (read == -1)
|
||||
throw Exception("Stream closed")
|
||||
bytesRead += read
|
||||
}
|
||||
|
||||
val messageBytes = buffer.sliceArray(IntRange(0, size));
|
||||
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
||||
@@ -344,19 +371,22 @@ class FCastCastingDevice : CastingDevice {
|
||||
try {
|
||||
handleMessage(Opcode.find(opcode), json);
|
||||
} 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) {
|
||||
Logger.e(TAG, "Socket exception while receiving.", e);
|
||||
exceptionOccurred = true;
|
||||
break
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Exception while receiving.", e);
|
||||
exceptionOccurred = true;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
_socket?.close();
|
||||
_socket?.close()
|
||||
_inputStream?.close()
|
||||
_outputStream?.close()
|
||||
Logger.i(TAG, "Socket disconnected.");
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to close socket.", e)
|
||||
@@ -368,7 +398,41 @@ class FCastCastingDevice : CastingDevice {
|
||||
|
||||
Logger.i(TAG, "Stopped connection loop.");
|
||||
connectionState = CastConnectionState.DISCONNECTED;
|
||||
}.apply { start() };
|
||||
}.apply { start() }
|
||||
|
||||
_pingThread = Thread {
|
||||
Logger.i(TAG, "Started ping loop.")
|
||||
|
||||
while (_scopeIO?.isActive == true) {
|
||||
try {
|
||||
send(Opcode.Ping)
|
||||
} catch (e: Throwable) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/*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.");
|
||||
}.apply { start() }
|
||||
} else {
|
||||
Log.i(TAG, "Thread was still alive, not restarted")
|
||||
}
|
||||
@@ -421,39 +485,44 @@ class FCastCastingDevice : CastingDevice {
|
||||
Logger.i(TAG, "Remote version received: $version")
|
||||
}
|
||||
Opcode.Ping -> send(Opcode.Pong)
|
||||
Opcode.Pong -> _lastPongTime = System.currentTimeMillis()
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
|
||||
private fun send(opcode: Opcode, message: String? = null) {
|
||||
try {
|
||||
val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
|
||||
val size = 1 + data.size
|
||||
val outputStream = _outputStream
|
||||
if (outputStream == null) {
|
||||
Log.w(TAG, "Failed to send $size bytes, output stream is null.")
|
||||
return
|
||||
ensureNotMainThread()
|
||||
|
||||
synchronized (_outputStreamLock) {
|
||||
try {
|
||||
val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
|
||||
val size = 1 + data.size
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,6 +542,7 @@ class FCastCastingDevice : CastingDevice {
|
||||
_started = false;
|
||||
//TODO: Kill and/or join thread?
|
||||
_thread = null;
|
||||
_pingThread = null;
|
||||
|
||||
val socket = _socket;
|
||||
val scopeIO = _scopeIO;
|
||||
@@ -482,6 +552,8 @@ class FCastCastingDevice : CastingDevice {
|
||||
|
||||
scopeIO.launch {
|
||||
socket.close();
|
||||
_inputStream?.close()
|
||||
_outputStream?.close()
|
||||
connectionState = CastConnectionState.DISCONNECTED;
|
||||
scopeIO.cancel();
|
||||
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.media.platforms.js.JSClient
|
||||
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.structures.IPager
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.engine.dev.V8RemoteObject
|
||||
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.StateDeveloper
|
||||
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.JsonElement
|
||||
import com.google.gson.JsonParser
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.lang.reflect.Modifier
|
||||
import java.util.UUID
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.memberFunctions
|
||||
import kotlin.reflect.jvm.javaType
|
||||
import kotlin.reflect.jvm.jvmErasure
|
||||
|
||||
class DeveloperEndpoints(private val context: Context) {
|
||||
private val TAG = "DeveloperEndpoints";
|
||||
private val _client = ManagedHttpClient();
|
||||
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 _testPluginVariables: HashMap<String, V8RemoteObject> = hashMapOf();
|
||||
|
||||
@@ -190,6 +204,17 @@ class DeveloperEndpoints(private val context: Context) {
|
||||
val client = JSHttpClient(null, null, null, config);
|
||||
val clientAuth = JSHttpClient(null, null, null, config);
|
||||
_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());
|
||||
}
|
||||
@@ -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
|
||||
@HttpPOST("/get")
|
||||
fun get(context: HttpContext) {
|
||||
|
||||
@@ -96,6 +96,11 @@ class AutoUpdateDialog(context: Context?) : AlertDialog(context) {
|
||||
Logger.i(TAG, "Cleared InstallReceiver.onReceiveResult handler.")
|
||||
}
|
||||
|
||||
fun hideExceptionButtons() {
|
||||
_buttonNever.visibility = View.GONE
|
||||
_buttonShowChangelog.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
_buttonShowChangelog.visibility = Button.GONE;
|
||||
_buttonNever.visibility = Button.GONE;
|
||||
|
||||
@@ -123,7 +123,8 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
|
||||
msg = comment,
|
||||
rating = RatingLikeDislikes(0, 0),
|
||||
date = OffsetDateTime.now(),
|
||||
eventPointer = eventPointer
|
||||
eventPointer = eventPointer,
|
||||
parentReference = ref
|
||||
));
|
||||
|
||||
dismiss();
|
||||
|
||||
@@ -143,7 +143,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
|
||||
StateCasting.instance.onActiveDeviceDurationChanged.remove(this);
|
||||
StateCasting.instance.onActiveDeviceDurationChanged.subscribe {
|
||||
_sliderPosition.valueTo = it.toFloat().coerceAtLeast(1.0f);
|
||||
val dur = it.toFloat().coerceAtLeast(1.0f)
|
||||
_sliderPosition.value = _sliderPosition.value.coerceAtLeast(0.0f).coerceAtMost(dur);
|
||||
_sliderPosition.valueTo = dur
|
||||
};
|
||||
|
||||
_device = StateCasting.instance.activeDevice;
|
||||
@@ -185,8 +187,10 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
_sliderPosition.valueFrom = 0.0f;
|
||||
_sliderVolume.valueFrom = 0.0f;
|
||||
_sliderVolume.value = d.volume.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
|
||||
_sliderPosition.valueTo = d.duration.toFloat().coerceAtLeast(1.0f);
|
||||
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
|
||||
|
||||
val dur = d.duration.toFloat().coerceAtLeast(1.0f)
|
||||
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
|
||||
_sliderPosition.valueTo = dur
|
||||
|
||||
if (d.canSetVolume) {
|
||||
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.engine
|
||||
|
||||
import android.content.Context
|
||||
import com.caoccao.javet.exceptions.JavetCompilationException
|
||||
import com.caoccao.javet.exceptions.JavetException
|
||||
import com.caoccao.javet.exceptions.JavetExecutionException
|
||||
import com.caoccao.javet.interop.V8Host
|
||||
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.V8ValueString
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
@@ -173,8 +175,16 @@ class V8Plugin {
|
||||
isStopped = true;
|
||||
_runtime?.let {
|
||||
_runtime = null;
|
||||
if(!it.isClosed && !it.isDead)
|
||||
it.close();
|
||||
if(!it.isClosed && !it.isDead) {
|
||||
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}]");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@ import com.caoccao.javet.annotations.V8Function
|
||||
import com.caoccao.javet.annotations.V8Property
|
||||
import com.caoccao.javet.enums.V8ConversionMode
|
||||
import com.caoccao.javet.enums.V8ProxyMode
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.engine.internal.V8BindObject
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
@@ -65,7 +68,7 @@ class PackageDOMParser : V8Package {
|
||||
return result;
|
||||
}
|
||||
@V8Property
|
||||
fun attributes(): Map<String, String> = _element.attributes().dataset();
|
||||
fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
|
||||
@V8Property
|
||||
fun innerHTML(): String = _element.html();
|
||||
@V8Property
|
||||
@@ -138,10 +141,32 @@ class PackageDOMParser : V8Package {
|
||||
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 {
|
||||
fun parse(parser: PackageDOMParser, str: String): DOMNode {
|
||||
return DOMNode(parser, Jsoup.parse(str));
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class SerializedNode(
|
||||
val children: List<SerializedNode>,
|
||||
val name: String,
|
||||
val value: String,
|
||||
val attributes: Map<String, String>
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -27,6 +27,7 @@ import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.exceptions.ChannelException
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -336,8 +337,11 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||
context?.let {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
val channel = if(kv.value is ChannelException) (kv.value as ChannelException).channelNameOrUrl else 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
|
||||
UIDialogs.toast(it, kv.value.message ?: "", false);
|
||||
} catch (e: Throwable) {
|
||||
|
||||
+6
-3
@@ -247,11 +247,14 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
||||
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
||||
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
|
||||
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
||||
if (_buttonsVisible - 1 >= defs.size) {
|
||||
if (_buttonsVisible >= defs.size) {
|
||||
updateBottomMenuButtons(defs.toMutableList(), false);
|
||||
} else if (_buttonsVisible > 0) {
|
||||
updateBottomMenuButtons(defs.take(_buttonsVisible - 1).toMutableList(), true);
|
||||
updateMoreButtons(defs.drop(_buttonsVisible - 1).toMutableList());
|
||||
} else {
|
||||
updateBottomMenuButtons(defs.slice(IntRange(0, _buttonsVisible - 2)).toMutableList(), true);
|
||||
updateMoreButtons(defs.slice(IntRange(_buttonsVisible - 1, defs.size - 1)).toMutableList());
|
||||
updateBottomMenuButtons(mutableListOf(), false)
|
||||
updateMoreButtons(defs.toMutableList())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+31
-6
@@ -22,15 +22,17 @@ class BrowserFragment : MainFragment() {
|
||||
override val hasBottomBar: Boolean get() = true;
|
||||
|
||||
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 {
|
||||
val view = inflater.inflate(R.layout.fragment_browser, container, false);
|
||||
_webview = view.findViewById<WebView?>(R.id.webview).apply {
|
||||
this.webViewClient = object: WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
this.webViewClient = _webviewWithoutHandling;
|
||||
this.settings.javaScriptEnabled = true;
|
||||
CookieManager.getInstance().setAcceptCookie(true);
|
||||
this.settings.domStorageEnabled = true;
|
||||
@@ -41,8 +43,26 @@ class BrowserFragment : MainFragment() {
|
||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||
super.onShownWithView(parameter, isBack)
|
||||
|
||||
if(parameter is String)
|
||||
if(parameter is String) {
|
||||
_webview?.webViewClient = _webviewWithoutHandling;
|
||||
_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
|
||||
_webview?.loadUrl("about:blank");
|
||||
}
|
||||
@@ -59,4 +79,9 @@ class BrowserFragment : MainFragment() {
|
||||
companion object {
|
||||
fun newInstance() = BrowserFragment().apply {}
|
||||
}
|
||||
|
||||
class NavigateOptions(
|
||||
val url: String,
|
||||
val urlHandlers: Map<String, (WebResourceRequest)->Unit>? = null
|
||||
)
|
||||
}
|
||||
+12
@@ -117,6 +117,7 @@ class CommentsFragment : MainFragment() {
|
||||
val holder = CommentWithReferenceViewHolder(viewGroup, _cache);
|
||||
holder.onDelete.subscribe(::onDelete);
|
||||
holder.onRepliesClick.subscribe(::onRepliesClick);
|
||||
holder.onClick.subscribe(::onClick);
|
||||
return@InsertedViewAdapterWithLoader holder;
|
||||
}
|
||||
);
|
||||
@@ -200,6 +201,17 @@ class CommentsFragment : MainFragment() {
|
||||
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) {
|
||||
val replyCount = c.replyCount ?: 0;
|
||||
var metadata = "";
|
||||
|
||||
+8
-2
@@ -154,8 +154,14 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||
};
|
||||
|
||||
onSearch.subscribe(this) {
|
||||
if(it.isHttpUrl())
|
||||
navigate<VideoDetailFragment>(it);
|
||||
if(it.isHttpUrl()) {
|
||||
if(StatePlatform.instance.hasEnabledPlaylistClient(it))
|
||||
navigate<PlaylistFragment>(it);
|
||||
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
||||
navigate<ChannelFragment>(it);
|
||||
else
|
||||
navigate<VideoDetailFragment>(it);
|
||||
}
|
||||
else
|
||||
setQuery(it, true);
|
||||
};
|
||||
|
||||
+48
-1
@@ -8,6 +8,7 @@ import android.view.ViewGroup
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.platforms.js.JSClient
|
||||
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.ScriptImplementationException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.SearchType
|
||||
import com.futo.platformplayer.states.AnnouncementType
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateMeta
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
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.InsertedViewAdapterWithLoader
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||
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.util.UUID
|
||||
|
||||
@@ -147,6 +156,42 @@ class HomeFragment : MainFragment() {
|
||||
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() {
|
||||
loadResults();
|
||||
}
|
||||
@@ -161,13 +206,15 @@ class HomeFragment : MainFragment() {
|
||||
}
|
||||
private fun loadedResult(pager : IPager<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}");
|
||||
finishRefreshLayoutLoader();
|
||||
setLoading(false);
|
||||
setPager(pager);
|
||||
if(pager.getResults().isEmpty() && !pager.hasMorePages())
|
||||
setEmptyPager(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -315,7 +315,7 @@ class PostDetailFragment : MainFragment {
|
||||
_rating.visibility = View.GONE;
|
||||
|
||||
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;
|
||||
|
||||
_rating.onLikeDislikeUpdated.remove(this);
|
||||
@@ -663,7 +663,7 @@ class PostDetailFragment : MainFragment {
|
||||
Logger.i(TAG, "fetchPolycentricComments")
|
||||
val post = _post;
|
||||
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) {
|
||||
Logger.w(TAG, "Failed to fetch polycentric comments because url was not set null")
|
||||
|
||||
+33
-10
@@ -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)
|
||||
StatePlugins.instance.getPlugin(config.id);
|
||||
else null;
|
||||
groups.add(
|
||||
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();
|
||||
}.withBackground(R.drawable.background_big_button_red).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);
|
||||
};
|
||||
} 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)
|
||||
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);
|
||||
}
|
||||
|
||||
val isEmbedded = StatePlugins.instance.getEmbeddedSources(context).any { it.key == config.id };
|
||||
val advancedButtons = BigButtonGroup(c, "Advanced",
|
||||
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;
|
||||
},
|
||||
if(isEmbedded) BigButton(c, "Reinstall", "Modify the source of this plugin", R.drawable.ic_refresh) {
|
||||
StatePlugins.instance.updateEmbeddedPlugins(context, listOf(config.id), true);
|
||||
reloadSource(config.id);
|
||||
UIDialogs.toast(context, "Embedded plugin reinstalled, may require refresh");
|
||||
val embeddedConfig = StatePlugins.instance.getEmbeddedPluginConfigFromID(context, config.id);
|
||||
|
||||
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 {
|
||||
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);
|
||||
@@ -354,11 +366,22 @@ class SourceDetailFragment : MainFragment() {
|
||||
if(config.authentication == null)
|
||||
return;
|
||||
|
||||
LoginActivity.showLogin(StateApp.instance.context, config) {
|
||||
StatePlugins.instance.setPluginAuth(config.id, it);
|
||||
|
||||
reloadSource(config.id);
|
||||
};
|
||||
if(config.authentication.loginWarning != null) {
|
||||
UIDialogs.showDialog(context, R.drawable.ic_warning_yellow, "Login Warning",
|
||||
config.authentication.loginWarning, null, 0,
|
||||
UIDialogs.Action("Cancel", {}, UIDialogs.ActionStyle.NONE),
|
||||
UIDialogs.Action("Login", {
|
||||
LoginActivity.showLogin(StateApp.instance.context, config) {
|
||||
StatePlugins.instance.setPluginAuth(config.id, it);
|
||||
reloadSource(config.id);
|
||||
};
|
||||
}, UIDialogs.ActionStyle.PRIMARY))
|
||||
}
|
||||
else
|
||||
LoginActivity.showLogin(StateApp.instance.context, config) {
|
||||
StatePlugins.instance.setPluginAuth(config.id, it);
|
||||
reloadSource(config.id);
|
||||
};
|
||||
}
|
||||
private fun logoutSource(clear: Boolean = true) {
|
||||
val config = _config ?: return;
|
||||
|
||||
+10
-7
@@ -8,6 +8,7 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.EnabledSourceViewHolder
|
||||
import com.futo.platformplayer.views.adapters.ItemMoveCallback
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import com.futo.platformplayer.views.sources.SourceUnderConstructionView
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.Collections
|
||||
@@ -86,6 +88,14 @@ class SourcesFragment : MainFragment() {
|
||||
_containerDisabledViews = findViewById(R.id.container_disabled_views);
|
||||
_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))
|
||||
_containerConstruction.addView(SourceUnderConstructionView(context, inConstructSource.key, inConstructSource.value));
|
||||
|
||||
@@ -111,8 +121,6 @@ class SourcesFragment : MainFragment() {
|
||||
|
||||
adapterSourcesEnabled.notifyItemMoved(fromPosition, toPosition);
|
||||
onEnabledChanged(enabledSources);
|
||||
if(toPosition == 0)
|
||||
onPrimaryChanged(enabledSources.first());
|
||||
|
||||
StatePlatform.instance.setPlatformOrder(enabledSources.map { it.name });
|
||||
};
|
||||
@@ -133,8 +141,6 @@ class SourcesFragment : MainFragment() {
|
||||
|
||||
updateContainerVisibility();
|
||||
onEnabledChanged(enabledSources);
|
||||
if(index == 0)
|
||||
onPrimaryChanged(enabledSources.first());
|
||||
|
||||
if(enabledSources.size <= 1)
|
||||
setCanRemove(false);
|
||||
@@ -221,9 +227,6 @@ class SourcesFragment : MainFragment() {
|
||||
_adapterSourcesEnabled.canRemove = canRemove;
|
||||
}
|
||||
|
||||
private fun onPrimaryChanged(client: IPlatformClient) {
|
||||
StatePlatform.instance.selectPrimaryClient(client.id);
|
||||
}
|
||||
private fun onEnabledChanged(clients: List<IPlatformClient>) {
|
||||
runBlocking {
|
||||
StatePlatform.instance.selectClients(*clients.map { it.id }.toTypedArray());
|
||||
|
||||
+29
-13
@@ -25,12 +25,14 @@ import com.futo.platformplayer.models.SearchType
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateCache
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
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.InsertedViewAdapterWithLoader
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||
@@ -44,6 +46,7 @@ import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.nio.channels.Channel
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
@@ -109,16 +112,6 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
|
||||
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
||||
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
|
||||
StateSubscriptions.instance.onFeedProgress.subscribe(this) { id, progress, total ->
|
||||
if(subGroup?.id == id)
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
setProgress(progress, total);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to set progress", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
StateSubscriptions.instance.global.onUpdateProgress.subscribe(this) { progress, total ->
|
||||
};
|
||||
|
||||
@@ -174,12 +167,24 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
if (!StateSubscriptions.instance.global.isGlobalUpdating) {
|
||||
finishRefreshLayoutLoader();
|
||||
}
|
||||
|
||||
StateSubscriptions.instance.onFeedProgress.subscribe(this) { id, progress, total ->
|
||||
if(subGroup?.id == id)
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
setProgress(progress, total);
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to set progress", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanup() {
|
||||
super.cleanup()
|
||||
StateSubscriptions.instance.global.onUpdateProgress.remove(this);
|
||||
StateSubscriptions.instance.onSubscriptionsChanged.remove(this);
|
||||
StateSubscriptions.instance.onFeedProgress.remove(this);
|
||||
}
|
||||
|
||||
override val feedStyle: FeedStyle get() = Settings.instance.subscriptions.getSubscriptionsFeedStyle();
|
||||
@@ -193,6 +198,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
val allowContentTypes: MutableList<ContentType> = mutableListOf(ContentType.MEDIA, ContentType.POST);
|
||||
var allowLive: Boolean = true;
|
||||
var allowPlanned: Boolean = false;
|
||||
var allowWatched: Boolean = true;
|
||||
override fun encode(): String {
|
||||
return Json.encodeToString(this);
|
||||
}
|
||||
@@ -300,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.posts), _filterSettings.allowContentTypes.contains(ContentType.POST)) { toggleFilterContentType(ContentType.POST, it); },
|
||||
SubscriptionBar.Toggle(context.getString(R.string.live), _filterSettings.allowLive) { _filterSettings.allowLive = it; _filterSettings.save(); loadResults(false); },
|
||||
SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); }
|
||||
SubscriptionBar.Toggle(context.getString(R.string.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); }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -332,6 +339,9 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
return results.filter {
|
||||
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
|
||||
|
||||
if(it is IPlatformVideo && it.duration > 0 && !_filterSettings.allowWatched && StateHistory.instance.isHistoryWatched(it.url, it.duration))
|
||||
return@filter false;
|
||||
|
||||
//TODO: Check against a sub cache
|
||||
if(filterGroup != null && !filterGroup.urls.contains(it.author.url))
|
||||
return@filter false;
|
||||
@@ -438,14 +448,17 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
}
|
||||
Logger.e(TAG, "Channel [${channel}] failed", ex);
|
||||
if (toShow is PluginException)
|
||||
UIDialogs.appToast(
|
||||
context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", toShow.config.name).replace("{message}", toShow.message ?: "")
|
||||
UIDialogs.appToast(ToastView.Toast(
|
||||
toShow.message +
|
||||
(if(channel != null) "\nChannel: " + channel else ""), false, null,
|
||||
"Plugin ${toShow.config.name} failed")
|
||||
);
|
||||
else
|
||||
UIDialogs.appToast(ex.message ?: "");
|
||||
}
|
||||
}
|
||||
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) }
|
||||
.map { if(it is ChannelException) (it.cause as PluginException) else if(it is PluginException) it else null }
|
||||
.filter { it != null }
|
||||
@@ -454,6 +467,9 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
.toList();
|
||||
for(distinctPluginFail in failedPlugins)
|
||||
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) {
|
||||
Logger.e(TAG, "Failed to handle exceptions", e)
|
||||
|
||||
+24
-11
@@ -67,7 +67,7 @@ class TutorialFragment : MainFragment() {
|
||||
|
||||
addView(createHeader("Initial setup"))
|
||||
initialSetupVideos.forEach {
|
||||
addView(createTutorialPill(R.drawable.ic_movie, it.name).apply {
|
||||
addView(createTutorialPill(R.drawable.ic_movie, it.name, it.description).apply {
|
||||
onClick.subscribe {
|
||||
fragment.navigate<VideoDetailFragment>(it)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ class TutorialFragment : MainFragment() {
|
||||
|
||||
addView(createHeader("Features"))
|
||||
featuresVideos.forEach {
|
||||
addView(createTutorialPill(R.drawable.ic_movie, it.name).apply {
|
||||
addView(createTutorialPill(R.drawable.ic_movie, it.name, it.description).apply {
|
||||
onClick.subscribe {
|
||||
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 {
|
||||
setIconPrefix(iconPrefix)
|
||||
setText(t)
|
||||
setDescription(d)
|
||||
setIconSuffix(R.drawable.ic_play_notif)
|
||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
|
||||
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(
|
||||
VideoUrlSource("1080p", url, 1920, 1080, duration, "video/mp4")
|
||||
VideoUrlSource("Original", url, width, height, duration, "video/mp4")
|
||||
)
|
||||
override val audioSources: Array<IAudioSource> = arrayOf()
|
||||
}
|
||||
@@ -120,7 +121,9 @@ class TutorialFragment : MainFragment() {
|
||||
override val description: String,
|
||||
thumbnailUrl: String,
|
||||
videoUrl: String,
|
||||
override val duration: Long
|
||||
override val duration: Long,
|
||||
width: Int = 1920,
|
||||
height: Int = 1080
|
||||
) : IPlatformVideoDetails {
|
||||
override val id: PlatformID = PlatformID("tutorial", uuid)
|
||||
override val contentType: ContentType = ContentType.MEDIA
|
||||
@@ -137,7 +140,7 @@ class TutorialFragment : MainFragment() {
|
||||
override val isLive: Boolean = false
|
||||
override val rating: IRating = RatingLikes(-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> {
|
||||
return EmptyPager()
|
||||
}
|
||||
@@ -163,7 +166,7 @@ class TutorialFragment : MainFragment() {
|
||||
TutorialVideo(
|
||||
uuid = "3b99ebfe-2640-4643-bfe0-a0cf04261fc5",
|
||||
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",
|
||||
videoUrl = "https://releases.grayjay.app/tutorials/getting-started.mp4",
|
||||
duration = 50
|
||||
@@ -171,7 +174,7 @@ class TutorialFragment : MainFragment() {
|
||||
TutorialVideo(
|
||||
uuid = "793aa009-516c-4581-b82f-a8efdfef4c27",
|
||||
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",
|
||||
videoUrl = "https://releases.grayjay.app/tutorials/pay.mp4",
|
||||
duration = 52
|
||||
@@ -182,7 +185,7 @@ class TutorialFragment : MainFragment() {
|
||||
TutorialVideo(
|
||||
uuid = "d2238d88-4252-4a91-a12d-b90c049bb7cf",
|
||||
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",
|
||||
videoUrl = "https://releases.grayjay.app/tutorials/search.mp4",
|
||||
duration = 39
|
||||
@@ -198,10 +201,20 @@ class TutorialFragment : MainFragment() {
|
||||
TutorialVideo(
|
||||
uuid = "94d36959-e3fc-4c24-a988-89147067a179",
|
||||
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",
|
||||
videoUrl = "https://releases.grayjay.app/tutorials/how-to-cast.mp4",
|
||||
duration = 79
|
||||
),
|
||||
TutorialVideo(
|
||||
uuid = "5128c2e3-852b-4281-869b-efea2ec82a0e",
|
||||
name = "Monetization",
|
||||
description = "How can I monetize as a creator?",
|
||||
thumbnailUrl = "https://releases.grayjay.app/tutorials/monetization.jpg",
|
||||
videoUrl = "https://releases.grayjay.app/tutorials/monetization.mp4",
|
||||
duration = 47,
|
||||
1080,
|
||||
1920
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+4
-3
@@ -169,14 +169,14 @@ class VideoDetailFragment : MainFragment {
|
||||
_view!!.transitionToStart();
|
||||
}
|
||||
fun maximizeVideoDetail(instant: Boolean = false) {
|
||||
if(_maximizeProgress > 0.9f && state != State.MAXIMIZED) {
|
||||
if((_maximizeProgress > 0.9f || instant) && state != State.MAXIMIZED) {
|
||||
state = State.MAXIMIZED;
|
||||
onMaximized.emit();
|
||||
}
|
||||
_view?.let {
|
||||
if(!instant)
|
||||
if(!instant) {
|
||||
it.transitionToEnd();
|
||||
else {
|
||||
} else {
|
||||
it.progress = 1f;
|
||||
onTransitioning.emit(true);
|
||||
}
|
||||
@@ -424,6 +424,7 @@ class VideoDetailFragment : MainFragment {
|
||||
changeOrientation(OrientationManager.Orientation.PORTRAIT);
|
||||
}
|
||||
isFullscreen = fullscreen;
|
||||
_view?.allowMotion = !fullscreen;
|
||||
}
|
||||
private fun changeOrientation(orientation: OrientationManager.Orientation) {
|
||||
Logger.i(TAG, "Orientation Change:" + orientation.name);
|
||||
|
||||
+33
-17
@@ -145,7 +145,6 @@ import com.futo.polycentric.core.Opinion
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -374,7 +373,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
|
||||
_buttonSubscribe.onSubscribed.subscribe {
|
||||
UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||
};
|
||||
|
||||
_container_content_liveChat.onRaidNow.subscribe {
|
||||
@@ -399,6 +398,10 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
};
|
||||
_monetization.onUrlTap.subscribe {
|
||||
fragment.navigate<BrowserFragment>(it);
|
||||
onMinimize.emit();
|
||||
}
|
||||
|
||||
_player.attachPlayer();
|
||||
|
||||
@@ -762,7 +765,9 @@ class VideoDetailView : ConstraintLayout {
|
||||
fun updateMoreButtons() {
|
||||
val buttons = listOf(RoundButton(context, R.drawable.ic_add, context.getString(R.string.add), TAG_ADD) {
|
||||
(video ?: _searchVideo)?.let {
|
||||
_slideUpOverlay = UISlideOverlays.showAddToOverlay(it, _overlayContainer);
|
||||
_slideUpOverlay = UISlideOverlays.showAddToOverlay(it, _overlayContainer) {
|
||||
_slideUpOverlay = it
|
||||
};
|
||||
}
|
||||
},
|
||||
if(video?.isLive ?: false)
|
||||
@@ -858,11 +863,11 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
|
||||
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 {
|
||||
val current = _historyIndex;
|
||||
if(current == null || current.url != video.url) {
|
||||
val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
|
||||
val index = StateHistory.instance.getHistoryByVideo(video, true);
|
||||
_historyIndex = index;
|
||||
return@withContext index;
|
||||
}
|
||||
@@ -1004,6 +1009,9 @@ class VideoDetailView : ConstraintLayout {
|
||||
fun setVideo(url: String, resumeSeconds: Long = 0, playWhenReady: Boolean = true) {
|
||||
Logger.i(TAG, "setVideo url=$url resumeSeconds=$resumeSeconds playWhenReady=$playWhenReady")
|
||||
|
||||
if(this.video?.url == url)
|
||||
return;
|
||||
|
||||
_searchVideo = null;
|
||||
video = null;
|
||||
_playbackTracker = null;
|
||||
@@ -1031,9 +1039,12 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
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")
|
||||
|
||||
if(!bypassSameVideoCheck && this.video?.url == video.url)
|
||||
return;
|
||||
|
||||
val cachedVideo = StateDownloads.instance.getCachedVideo(video.id);
|
||||
if(cachedVideo != null) {
|
||||
setVideoDetails(cachedVideo, true);
|
||||
@@ -1132,6 +1143,9 @@ class VideoDetailView : ConstraintLayout {
|
||||
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
||||
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
||||
|
||||
if(newVideo && this.video?.url == videoDetail.url)
|
||||
return;
|
||||
|
||||
if (newVideo) {
|
||||
_lastVideoSource = null;
|
||||
_lastAudioSource = null;
|
||||
@@ -1220,7 +1234,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
|
||||
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)
|
||||
_player.setMetadata(video.name, video.author.name);
|
||||
|
||||
@@ -1372,13 +1386,15 @@ class VideoDetailView : ConstraintLayout {
|
||||
val toResume = _videoResumePositionMilliseconds;
|
||||
_videoResumePositionMilliseconds = 0;
|
||||
loadCurrentVideo(toResume);
|
||||
_player.setGestureSoundFactor(1.0f);
|
||||
if (!Settings.instance.gestureControls.useSystemVolume) {
|
||||
_player.setGestureSoundFactor(1.0f);
|
||||
}
|
||||
|
||||
updateQueueState();
|
||||
|
||||
if (video !is TutorialFragment.TutorialVideo) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val historyItem = getHistoryIndex(videoDetail);
|
||||
val historyItem = getHistoryIndex(videoDetail) ?: return@launch;
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong());
|
||||
@@ -1651,7 +1667,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
Logger.i(TAG, "prevVideo")
|
||||
val next = StatePlayer.instance.prevQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9);
|
||||
if(next != null) {
|
||||
setVideoOverview(next);
|
||||
setVideoOverview(next, true, 0, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1661,7 +1677,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
if(next == null && forceLoop)
|
||||
next = StatePlayer.instance.restartQueue();
|
||||
if(next != null) {
|
||||
setVideoOverview(next);
|
||||
setVideoOverview(next, true, 0, true);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
@@ -1968,14 +1984,14 @@ class VideoDetailView : ConstraintLayout {
|
||||
Logger.i(TAG, "fetchPolycentricComments")
|
||||
val video = video;
|
||||
val idValue = video?.id?.value
|
||||
if (idValue == null) {
|
||||
Logger.w(TAG, "Failed to fetch polycentric comments because id was null")
|
||||
if (video?.url?.isEmpty() != false) {
|
||||
Logger.w(TAG, "Failed to fetch polycentric comments because url was null")
|
||||
_commentsList.clear()
|
||||
return
|
||||
}
|
||||
|
||||
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)); };
|
||||
}
|
||||
private fun fetchVideo() {
|
||||
@@ -2240,7 +2256,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) {
|
||||
if (v !is TutorialFragment.TutorialVideo) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -2349,7 +2365,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
else if(isOverlayed) {
|
||||
_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);
|
||||
}
|
||||
@@ -2550,7 +2566,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
else
|
||||
withContext(Dispatchers.Main) {
|
||||
setVideoDetails(videoDetail);
|
||||
setVideoDetails(videoDetail, false);
|
||||
_liveTryJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
+9
-8
@@ -13,17 +13,17 @@ import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
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.stores.SearchHistoryStorage
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
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.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.SearchHistoryStorage
|
||||
|
||||
class SearchTopBarFragment : TopFragment() {
|
||||
private val TAG = "SearchTopBarFragment"
|
||||
@@ -54,11 +54,12 @@ class SearchTopBarFragment : TopFragment() {
|
||||
|
||||
private val _searchDoneListener = object : TextView.OnEditorActionListener {
|
||||
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
|
||||
|
||||
onDone();
|
||||
return true;
|
||||
onDone()
|
||||
return true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||
import com.futo.platformplayer.timestampRegex
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class PlatformLinkMovementMethod : LinkMovementMethod {
|
||||
private val _context: Context;
|
||||
@@ -32,33 +33,36 @@ class PlatformLinkMovementMethod : LinkMovementMethod {
|
||||
val links = buffer.getSpans(off, off, URLSpan::class.java);
|
||||
|
||||
if (links.isNotEmpty()) {
|
||||
for (link in links) {
|
||||
Logger.i(TAG) { "Link clicked '${link.url}'." };
|
||||
runBlocking {
|
||||
for (link in links) {
|
||||
Logger.i(TAG) { "Link clicked '${link.url}'." };
|
||||
|
||||
if (_context is MainActivity) {
|
||||
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);
|
||||
if (_context is MainActivity) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||
}
|
||||
|
||||
|
||||
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)));
|
||||
}
|
||||
|
||||
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.Intent
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class AudioNoisyReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
Logger.i(TAG, "Audio Noisy received");
|
||||
MediaControlReceiver.onPauseReceived.emit();
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
Logger.i(TAG, "Audio Noisy received");
|
||||
MediaControlReceiver.onPauseReceived.emit();
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -37,6 +37,7 @@ class DownloadService : Service() {
|
||||
private val DOWNLOAD_NOTIF_ID = 3;
|
||||
private val DOWNLOAD_NOTIF_TAG = "download";
|
||||
private val DOWNLOAD_NOTIF_CHANNEL_ID = "downloadChannel";
|
||||
private val DOWNLOAD_NOTIF_CHANNEL_NAME = "Downloads";
|
||||
|
||||
//Context
|
||||
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
|
||||
@@ -95,7 +96,7 @@ class DownloadService : Service() {
|
||||
}
|
||||
fun setupNotificationRequirements() {
|
||||
_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.setSound(null, null);
|
||||
};
|
||||
|
||||
@@ -36,6 +36,7 @@ class ExportingService : Service() {
|
||||
private val EXPORT_NOTIF_ID = 4;
|
||||
private val EXPORT_NOTIF_TAG = "export";
|
||||
private val EXPORT_NOTIF_CHANNEL_ID = "exportChannel";
|
||||
private val EXPORT_NOTIF_CHANNEL_NAME = "Export";
|
||||
|
||||
//Context
|
||||
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
|
||||
@@ -88,7 +89,7 @@ class ExportingService : Service() {
|
||||
}
|
||||
fun setupNotificationRequirements() {
|
||||
_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.setSound(null, null);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.Random
|
||||
import java.util.UUID
|
||||
|
||||
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() {
|
||||
registerAnnouncement(
|
||||
"default-url-handler",
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.Color
|
||||
import android.media.AudioManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
@@ -14,6 +13,7 @@ import android.net.NetworkRequest
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -42,7 +42,6 @@ import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import com.futo.platformplayer.views.ToastView
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
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)
|
||||
StateDeveloper.instance.runServer();
|
||||
|
||||
@@ -477,7 +474,11 @@ class StateApp {
|
||||
Logger.i(TAG, "MainApp Started: Initialize [Noisy]");
|
||||
_receiverBecomingNoisy?.let {
|
||||
_receiverBecomingNoisy = null;
|
||||
context.unregisterReceiver(it);
|
||||
try {
|
||||
context.unregisterReceiver(it);
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to unregister receiver.", e)
|
||||
}
|
||||
}
|
||||
_receiverBecomingNoisy = AudioNoisyReceiver();
|
||||
context.registerReceiver(_receiverBecomingNoisy, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
||||
@@ -557,7 +558,6 @@ class StateApp {
|
||||
}
|
||||
|
||||
StateAnnouncement.instance.registerDefaultHandlerAnnouncement();
|
||||
StateAnnouncement.instance.registerDidYouKnow();
|
||||
Logger.i(TAG, "MainApp Started: Finished");
|
||||
|
||||
StatePlaylists.instance.toMigrateCheck();
|
||||
@@ -580,24 +580,9 @@ class StateApp {
|
||||
null,
|
||||
"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) {
|
||||
@@ -659,7 +644,11 @@ class StateApp {
|
||||
Logger.i(TAG, "App ended");
|
||||
_receiverBecomingNoisy?.let {
|
||||
_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.")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
@@ -49,6 +50,9 @@ class StateHistory {
|
||||
fun getHistoryPosition(url: String): Long {
|
||||
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 {
|
||||
@@ -92,14 +96,20 @@ class StateHistory {
|
||||
}
|
||||
fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? {
|
||||
val existing = historyIndex[video.url];
|
||||
if(existing != null)
|
||||
return _historyDBStore.get(existing.id!!);
|
||||
var result: DBHistory.Index? = null;
|
||||
if(existing != null) {
|
||||
result = _historyDBStore.getOrNull(existing.id!!);
|
||||
if(result == null)
|
||||
UIDialogs.toast("History item null?\nNo history tracking..");
|
||||
}
|
||||
else if(create) {
|
||||
val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now());
|
||||
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) {
|
||||
|
||||
@@ -46,6 +46,7 @@ import com.futo.platformplayer.models.ImageVariable
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringArrayStorage
|
||||
import com.futo.platformplayer.stores.StringStorage
|
||||
import com.futo.platformplayer.views.ToastView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -94,11 +95,6 @@ class StatePlatform {
|
||||
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();
|
||||
|
||||
val hasClients: Boolean get() = _availableClients.size > 0;
|
||||
@@ -171,8 +167,13 @@ class StatePlatform {
|
||||
var enabled: Array<String>;
|
||||
synchronized(_clientsLock) {
|
||||
for(e in _enabledClients) {
|
||||
e.disable();
|
||||
onSourceDisabled.emit(e);
|
||||
try {
|
||||
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();
|
||||
@@ -207,20 +208,6 @@ class StatePlatform {
|
||||
.filter { id -> _availableClients.any { it.id == id } }
|
||||
.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);
|
||||
};
|
||||
@@ -323,8 +310,6 @@ class StatePlatform {
|
||||
newClient.initialize();
|
||||
_enabledClients.add(newClient);
|
||||
}
|
||||
if (_primaryClientObj == client)
|
||||
_primaryClientObj = newClient;
|
||||
|
||||
_availableClients.removeIf { it.id == id };
|
||||
_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.
|
||||
* 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> {
|
||||
Logger.i(TAG, "Platform - getHome");
|
||||
var clientIdsOngoing = mutableListOf<String>();
|
||||
@@ -448,14 +427,12 @@ class StatePlatform {
|
||||
toAwait.map { PlaceholderPager(5, { PlatformContentPlaceholder(it.first.id) }) });
|
||||
}
|
||||
|
||||
fun getHomePrimary(): IPager<IPlatformContent> {
|
||||
return primaryClient.getHome();
|
||||
}
|
||||
|
||||
//Search
|
||||
fun searchSuggestions(query: String): Array<String> {
|
||||
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> {
|
||||
@@ -841,6 +818,7 @@ class StatePlatform {
|
||||
return urls;
|
||||
}
|
||||
|
||||
fun hasEnabledPlaylistClient(url: String) : Boolean = getEnabledClients().any { it.isPlaylistUrl(url) };
|
||||
fun getPlaylistClientOrNull(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})");
|
||||
@@ -886,7 +864,6 @@ class StatePlatform {
|
||||
synchronized(_clientsLock) {
|
||||
val enabledExisting = _enabledClients.filter { it is DevJSClient };
|
||||
val isEnabled = !enabledExisting.isEmpty()
|
||||
val isPrimary = _primaryClientObj is DevJSClient;
|
||||
|
||||
for (enabled in enabledExisting) {
|
||||
enabled.disable();
|
||||
@@ -901,11 +878,7 @@ class StatePlatform {
|
||||
devId = newClient.devID;
|
||||
try {
|
||||
StateDeveloper.instance.initializeDev(devId!!);
|
||||
if (isPrimary) {
|
||||
_primaryClientObj = newClient;
|
||||
_enabledClients.add(0, newClient);
|
||||
newClient.initialize();
|
||||
} else if (isEnabled) {
|
||||
if (isEnabled) {
|
||||
_enabledClients.add(newClient);
|
||||
newClient.initialize();
|
||||
}
|
||||
@@ -944,7 +917,7 @@ class StatePlatform {
|
||||
suspend fun checkForUpdates(): List<SourcePluginConfig> = withContext(Dispatchers.IO) {
|
||||
var configs = mutableListOf<SourcePluginConfig>()
|
||||
val updatesAvailableFor = hashSetOf<String>()
|
||||
for (availableClient in getAvailableClients()) {
|
||||
for (availableClient in getAvailableClients().filter { it is JSClient && it.descriptor.appSettings.checkForUpdates }) {
|
||||
if (availableClient !is JSClient) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -361,6 +361,12 @@ class StatePlayer {
|
||||
if (queueShuffle) {
|
||||
removeFromShuffledQueue(video);
|
||||
}
|
||||
if(currentVideo != null) {
|
||||
val newPos = _queue.indexOfFirst { it.url == currentVideo?.url };
|
||||
if(newPos >= 0)
|
||||
_queuePosition = newPos;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onQueueChanged.emit(shouldSwapCurrentItem);
|
||||
@@ -407,6 +413,12 @@ class StatePlayer {
|
||||
if(_queue.size == 1) {
|
||||
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 queue = if (queueShuffle && shuffledQueue != null) {
|
||||
@@ -421,6 +433,8 @@ class StatePlayer {
|
||||
}
|
||||
//Standard Behavior
|
||||
if(_queuePosition - 1 >= 0) {
|
||||
if(queue.size <= _queuePosition)
|
||||
return null;
|
||||
return queue[_queuePosition - 1];
|
||||
}
|
||||
//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.models.Playlist
|
||||
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.ReconstructStore
|
||||
import kotlinx.serialization.encodeToString
|
||||
@@ -35,6 +36,8 @@ class StatePlaylists {
|
||||
= SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
|
||||
})
|
||||
.load();
|
||||
private val _watchlistOrderStore = FragmentedStorage.get<StringArrayStorage>("watchListOrder"); //Temporary workaround to add order..
|
||||
|
||||
val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists")
|
||||
.withRestore(PlaylistBackup())
|
||||
.load();
|
||||
@@ -48,26 +51,32 @@ class StatePlaylists {
|
||||
}
|
||||
fun getWatchLater() : List<SerializedPlatformVideo> {
|
||||
synchronized(_watchlistStore) {
|
||||
return _watchlistStore.getItems();
|
||||
val order = _watchlistOrderStore.getAllValues();
|
||||
return _watchlistStore.getItems().sortedBy { order.indexOf(it.url) };
|
||||
}
|
||||
}
|
||||
fun updateWatchLater(updated: List<SerializedPlatformVideo>) {
|
||||
synchronized(_watchlistStore) {
|
||||
_watchlistStore.deleteAll();
|
||||
_watchlistStore.saveAllAsync(updated);
|
||||
_watchlistOrderStore.set(*updated.map { it.url }.toTypedArray());
|
||||
_watchlistOrderStore.save();
|
||||
}
|
||||
onWatchLaterChanged.emit();
|
||||
}
|
||||
fun removeFromWatchLater(video: SerializedPlatformVideo) {
|
||||
synchronized(_watchlistStore) {
|
||||
_watchlistStore.delete(video);
|
||||
_watchlistOrderStore.set(*_watchlistOrderStore.values.filter { it != video.url }.toTypedArray());
|
||||
_watchlistOrderStore.save();
|
||||
}
|
||||
|
||||
onWatchLaterChanged.emit();
|
||||
}
|
||||
fun addToWatchLater(video: SerializedPlatformVideo) {
|
||||
synchronized(_watchlistStore) {
|
||||
_watchlistStore.saveAsync(video);
|
||||
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values) .toTypedArray());
|
||||
_watchlistOrderStore.save();
|
||||
}
|
||||
onWatchLaterChanged.emit();
|
||||
}
|
||||
|
||||
@@ -160,6 +160,13 @@ class StatePlugins {
|
||||
val configJson = StateAssets.readAsset(context, assetConfigPath) ?: return null;
|
||||
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 {
|
||||
try {
|
||||
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.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||
import com.futo.platformplayer.resolveChannelUrl
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
@@ -48,6 +49,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import userpackage.Protocol
|
||||
import userpackage.Protocol.Reference
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
@@ -66,28 +68,40 @@ class StatePolycentric {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val db = SqlLiteDbHelper(context);
|
||||
Store.initializeSqlLiteStore(db);
|
||||
for (i in 0 .. 1) {
|
||||
try {
|
||||
val db = SqlLiteDbHelper(context);
|
||||
Store.initializeSqlLiteStore(db);
|
||||
|
||||
val activeProcessHandleString = _activeProcessHandle.value;
|
||||
if (activeProcessHandleString.isNotEmpty()) {
|
||||
try {
|
||||
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
||||
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
||||
} catch (e: Throwable) {
|
||||
db.upgradeOldSecrets(db.writableDatabase);
|
||||
val activeProcessHandleString = _activeProcessHandle.value;
|
||||
if (activeProcessHandleString.isNotEmpty()) {
|
||||
try {
|
||||
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
||||
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
||||
} catch (e: Throwable) {
|
||||
db.upgradeOldSecrets(db.writableDatabase);
|
||||
|
||||
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
||||
setProcessHandle(Store.instance.getProcessSecret(system)?.toProcessHandle());
|
||||
val system = PublicKey.fromProto(Protocol.PublicKey.parseFrom(activeProcessHandleString.base64ToByteArray()));
|
||||
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)
|
||||
}
|
||||
}
|
||||
} 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 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?) {
|
||||
@@ -287,7 +326,8 @@ class StatePolycentric {
|
||||
rating = RatingLikeDislikes(0, 0),
|
||||
date = if (ev.unixMilliseconds != null) Instant.ofEpochMilli(ev.unixMilliseconds!!).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
||||
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)
|
||||
}
|
||||
|
||||
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> {
|
||||
if (!enabled) {
|
||||
return EmptyPager()
|
||||
@@ -453,7 +564,8 @@ class StatePolycentric {
|
||||
rating = RatingLikeDislikes(likes, dislikes),
|
||||
date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
|
||||
replyCount = replies.toInt(),
|
||||
eventPointer = sev.toPointer()
|
||||
eventPointer = sev.toPointer(),
|
||||
parentReference = sev.event.references.getOrNull(0)
|
||||
);
|
||||
} catch (e: Throwable) {
|
||||
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 {
|
||||
val client = ManagedHttpClient();
|
||||
val latestVersion = downloadVersionCode(client);
|
||||
@@ -167,7 +167,7 @@ class StateUpdate {
|
||||
if (latestVersion > currentVersion) {
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
UIDialogs.showUpdateAvailableDialog(context, latestVersion);
|
||||
UIDialogs.showUpdateAvailableDialog(context, latestVersion, hideExceptionButtons);
|
||||
} catch (e: Throwable) {
|
||||
UIDialogs.toast(context, "Failed to show 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.StoreSerializer
|
||||
import kotlinx.serialization.KSerializer
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.lang.reflect.Field
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
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 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? {
|
||||
val result = dbDaoBase.getNullable(_sqlGet(id));
|
||||
|
||||
+3
-3
@@ -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.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||
import com.futo.platformplayer.exceptions.ChannelException
|
||||
import com.futo.platformplayer.findNonRuntimeException
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
|
||||
@@ -69,7 +70,6 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
val cachedChannels = mutableListOf<String>()
|
||||
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
|
||||
|
||||
|
||||
val taskResults = arrayListOf<SubscriptionTaskResult>();
|
||||
val timeTotal = measureTimeMillis {
|
||||
for(task in forkTasks) {
|
||||
@@ -126,7 +126,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
val pager = MultiChronoContentPager(groupedPagers, allowFailure, 15);
|
||||
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>> {
|
||||
@@ -200,7 +200,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
else {
|
||||
Logger.i(StateSubscriptions.TAG, "Channel ${task.sub.channel.name} failed, substituting with cache");
|
||||
pager = StateCache.instance.getChannelCachePager(task.sub.channel.url);
|
||||
taskEx = ex;
|
||||
taskEx = channelEx;
|
||||
return@submit SubscriptionTaskResult(task, pager, taskEx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,13 @@ import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.HorizontalSpaceItemDecoration
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
@@ -61,6 +63,7 @@ class MonetizationView : LinearLayout {
|
||||
|
||||
val onSupportTap = Event0();
|
||||
val onStoreTap = Event0();
|
||||
val onUrlTap = Event1<String>();
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
inflate(context, R.layout.view_monetization, this);
|
||||
@@ -70,10 +73,12 @@ class MonetizationView : LinearLayout {
|
||||
_membershipPlatform = findViewById(R.id.membership_platform);
|
||||
_buttonMembership.setOnClickListener {
|
||||
_membershipUrl?.let {
|
||||
/*
|
||||
val uri = Uri.parse(it);
|
||||
val intent = Intent(Intent.ACTION_VIEW);
|
||||
intent.data = uri;
|
||||
context.startActivity(intent);
|
||||
context.startActivity(intent);*/
|
||||
onUrlTap.emit(it);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,9 +134,18 @@ class MonetizationView : LinearLayout {
|
||||
_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;
|
||||
} else {
|
||||
_root.visibility = View.GONE;
|
||||
_buttonSupport.isVisible = false;
|
||||
}
|
||||
|
||||
setMerchandise(null);
|
||||
|
||||
@@ -10,6 +10,8 @@ import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.size
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
@@ -33,6 +35,13 @@ class SupportView : LinearLayout {
|
||||
private var _textNoSupportOptionsSet: TextView
|
||||
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) {
|
||||
inflate(context, R.layout.view_support, this);
|
||||
|
||||
|
||||
+6
@@ -55,6 +55,7 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
||||
|
||||
var onRepliesClick = Event1<IPlatformComment>();
|
||||
var onDelete = Event1<IPlatformComment>();
|
||||
var onClick = Event1<IPlatformComment>();
|
||||
var comment: IPlatformComment? = null
|
||||
private set;
|
||||
|
||||
@@ -108,6 +109,11 @@ class CommentWithReferenceViewHolder : ViewHolder {
|
||||
onDelete.emit(c);
|
||||
}
|
||||
|
||||
_layoutComment.setOnClickListener {
|
||||
val c = comment ?: return@setOnClickListener;
|
||||
onClick.emit(c);
|
||||
}
|
||||
|
||||
_textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,15 @@ import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.media.AudioManager
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.GestureDetector
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.ScaleGestureDetector
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
@@ -19,8 +23,11 @@ import androidx.core.animation.doOnEnd
|
||||
import androidx.core.animation.doOnStart
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
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.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.views.others.CircularProgressBar
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -33,6 +40,7 @@ import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class GestureControlView : LinearLayout {
|
||||
private val _scope = CoroutineScope(Dispatchers.Main);
|
||||
private val _imageFastForward: ImageView;
|
||||
@@ -59,6 +67,8 @@ class GestureControlView : LinearLayout {
|
||||
private val _progressSound: CircularProgressBar;
|
||||
private var _animatorSound: ObjectAnimator? = null;
|
||||
private var _brightnessFactor = 1.0f;
|
||||
private var _originalBrightnessFactor = 1.0f;
|
||||
private var _originalBrightnessMode: Int = 0;
|
||||
private var _adjustingBrightness: Boolean = false;
|
||||
private val _layoutControlsBrightness: FrameLayout;
|
||||
private val _progressBrightness: CircularProgressBar;
|
||||
@@ -70,10 +80,27 @@ class GestureControlView : LinearLayout {
|
||||
private var _fullScreenFactorUp = 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;
|
||||
|
||||
val isUserGesturing get() = _rewinding || _skipping || _adjustingBrightness || _adjustingSound || _adjustingFullscreenUp || _adjustingFullscreenDown || _isPanning || _isZooming;
|
||||
|
||||
val onSeek = Event1<Long>();
|
||||
val onBrightnessAdjusted = Event1<Float>();
|
||||
val onPan = Event2<Float, Float>();
|
||||
val onZoom = Event1<Float>();
|
||||
val onSoundAdjusted = Event1<Float>();
|
||||
val onToggleFullscreen = Event0();
|
||||
|
||||
@@ -91,8 +118,48 @@ class GestureControlView : LinearLayout {
|
||||
_layoutControlsSound = findViewById(R.id.layout_controls_sound);
|
||||
_progressSound = findViewById(R.id.progress_sound);
|
||||
_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);
|
||||
_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 {
|
||||
override fun onDown(p0: MotionEvent): Boolean { return false; }
|
||||
@@ -103,41 +170,48 @@ class GestureControlView : LinearLayout {
|
||||
if(p0 == null)
|
||||
return false;
|
||||
|
||||
val minDistance = Math.min(width, height)
|
||||
if (_isFullScreen && _adjustingBrightness) {
|
||||
val adjustAmount = (distanceY * 2) / minDistance;
|
||||
_brightnessFactor = (_brightnessFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_progressBrightness.progress = _brightnessFactor;
|
||||
onBrightnessAdjusted.emit(_brightnessFactor);
|
||||
} else if (_isFullScreen && _adjustingSound) {
|
||||
val adjustAmount = (distanceY * 2) / minDistance;
|
||||
_soundFactor = (_soundFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_progressSound.progress = _soundFactor;
|
||||
onSoundAdjusted.emit(_soundFactor);
|
||||
} else if (_adjustingFullscreenUp) {
|
||||
val adjustAmount = (distanceY * 2) / minDistance;
|
||||
_fullScreenFactorUp = (_fullScreenFactorUp + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_layoutControlsFullscreen.alpha = _fullScreenFactorUp;
|
||||
} else if (_adjustingFullscreenDown) {
|
||||
val adjustAmount = (-distanceY * 2) / minDistance;
|
||||
_fullScreenFactorDown = (_fullScreenFactorDown + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_layoutControlsFullscreen.alpha = _fullScreenFactorDown;
|
||||
} else {
|
||||
val rx = (p0.x + p1.x) / (2 * width);
|
||||
val ry = (p0.y + p1.y) / (2 * height);
|
||||
if (ry > 0.1 && ry < 0.9) {
|
||||
if (_isFullScreen && rx < 0.2) {
|
||||
startAdjustingBrightness();
|
||||
} else if (_isFullScreen && rx > 0.8) {
|
||||
startAdjustingSound();
|
||||
} else if (fullScreenGestureEnabled && rx in 0.3..0.7) {
|
||||
if (_isFullScreen) {
|
||||
startAdjustingFullscreenDown();
|
||||
} else {
|
||||
startAdjustingFullscreenUp();
|
||||
if (!_isPanning && p1.pointerCount == 1) {
|
||||
val minDistance = Math.min(width, height)
|
||||
if (_isFullScreen && _adjustingBrightness) {
|
||||
val adjustAmount = (distanceY * 2) / minDistance;
|
||||
_brightnessFactor = (_brightnessFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_progressBrightness.progress = _brightnessFactor;
|
||||
onBrightnessAdjusted.emit(_brightnessFactor);
|
||||
} else if (_isFullScreen && _adjustingSound) {
|
||||
val adjustAmount = (distanceY * 2) / minDistance;
|
||||
_soundFactor = (_soundFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_progressSound.progress = _soundFactor;
|
||||
onSoundAdjusted.emit(_soundFactor);
|
||||
} else if (_adjustingFullscreenUp) {
|
||||
val adjustAmount = (distanceY * 2) / minDistance;
|
||||
_fullScreenFactorUp = (_fullScreenFactorUp + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_layoutControlsFullscreen.alpha = _fullScreenFactorUp;
|
||||
} else if (_adjustingFullscreenDown) {
|
||||
val adjustAmount = (-distanceY * 2) / minDistance;
|
||||
_fullScreenFactorDown = (_fullScreenFactorDown + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
|
||||
_layoutControlsFullscreen.alpha = _fullScreenFactorDown;
|
||||
} else if (p0.pointerCount == 1) {
|
||||
val rx = (p0.x + p1.x) / (2 * width);
|
||||
val ry = (p0.y + p1.y) / (2 * height);
|
||||
if (ry > 0.1 && ry < 0.9) {
|
||||
if (Settings.instance.gestureControls.brightnessSlider && _isFullScreen && rx < 0.2) {
|
||||
startAdjustingBrightness();
|
||||
} else if (Settings.instance.gestureControls.volumeSlider && _isFullScreen && rx > 0.8) {
|
||||
startAdjustingSound();
|
||||
} else if (Settings.instance.gestureControls.toggleFullscreen && fullScreenGestureEnabled && rx in 0.3..0.7) {
|
||||
if (_isFullScreen) {
|
||||
startAdjustingFullscreenDown();
|
||||
} else {
|
||||
startAdjustingFullscreenUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (_isZoomPanEnabled && _isFullScreen && !_isZooming && Settings.instance.gestureControls.pan) {
|
||||
_isPanning = true
|
||||
stopAllGestures()
|
||||
updateSnappingVisibility()
|
||||
pan(_translationX - distanceX, _translationY - distanceY)
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -178,6 +252,55 @@ class GestureControlView : LinearLayout {
|
||||
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) {
|
||||
_layoutControls = layoutControls;
|
||||
_background = background;
|
||||
@@ -223,12 +346,67 @@ class GestureControlView : LinearLayout {
|
||||
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();
|
||||
|
||||
_gestureController.onTouchEvent(ev)
|
||||
_scaleGestureDetector.onTouchEvent(ev)
|
||||
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() {
|
||||
_jobHideControls?.cancel();
|
||||
_jobHideControls = null;
|
||||
@@ -558,11 +736,56 @@ class GestureControlView : LinearLayout {
|
||||
}
|
||||
|
||||
fun setFullscreen(isFullScreen: Boolean) {
|
||||
resetZoomPan()
|
||||
|
||||
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);
|
||||
onSoundAdjusted.emit(_soundFactor);
|
||||
} 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);
|
||||
stopAdjustingBrightness();
|
||||
stopAdjustingSound();
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.others.PlatformLinkMovementMethod
|
||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||
import com.futo.platformplayer.timestampRegex
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
||||
constructor(context: Context) : super(context) {}
|
||||
@@ -40,32 +41,34 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
|
||||
if (text is Spannable) {
|
||||
val links = text.getSpans(offset, offset, URLSpan::class.java)
|
||||
if (links.isNotEmpty()) {
|
||||
for (link in links) {
|
||||
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." };
|
||||
runBlocking {
|
||||
for (link in links) {
|
||||
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." };
|
||||
|
||||
val c = context;
|
||||
if (c is MainActivity) {
|
||||
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);
|
||||
val c = context;
|
||||
if (c is MainActivity) {
|
||||
if (c.handleUrl(link.url)) {
|
||||
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.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.formatDuration
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.toHumanTime
|
||||
import com.futo.platformplayer.views.behavior.GestureControlView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -252,8 +252,8 @@ class CastView : ConstraintLayout {
|
||||
.load(video.thumbnails.getHQThumbnail())
|
||||
.placeholder(R.drawable.placeholder_video_thumbnail)
|
||||
.into(_thumbnail);
|
||||
_textPosition.text = position.toHumanTime(false);
|
||||
_textDuration.text = video.duration.toHumanTime(false);
|
||||
_textPosition.text = (position * 1000).formatDuration();
|
||||
_textDuration.text = (video.duration * 1000).formatDuration();
|
||||
_timeBar.setPosition(position);
|
||||
_timeBar.setDuration(video.duration);
|
||||
}
|
||||
@@ -261,7 +261,7 @@ class CastView : ConstraintLayout {
|
||||
@OptIn(UnstableApi::class)
|
||||
fun setTime(ms: Long) {
|
||||
updateCurrentChapter(ms);
|
||||
_textPosition.text = ms.toHumanTime(true);
|
||||
_textPosition.text = ms.formatDuration();
|
||||
_timeBar.setPosition(ms / 1000);
|
||||
StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), ms);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.futo.platformplayer.views.overlays
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
@@ -8,11 +9,15 @@ import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.futo.platformplayer.R
|
||||
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.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
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.StatePolycentric
|
||||
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.others.CreatorThumbnail
|
||||
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
|
||||
|
||||
class RepliesOverlay : LinearLayout {
|
||||
@@ -34,10 +46,16 @@ class RepliesOverlay : LinearLayout {
|
||||
private val _creatorThumbnail: CreatorThumbnail;
|
||||
private val _layoutParentComment: ConstraintLayout;
|
||||
private var _readonly = false;
|
||||
private var _loading = true;
|
||||
private var _parentComment: IPlatformComment? = 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) {
|
||||
inflate(context, R.layout.overlay_replies, this)
|
||||
_layoutItems = findViewById(R.id.layout_items)
|
||||
_topbar = findViewById(R.id.topbar);
|
||||
_commentsList = findViewById(R.id.comments_list);
|
||||
_addCommentView = findViewById(R.id.add_comment_view);
|
||||
@@ -46,6 +64,11 @@ class RepliesOverlay : LinearLayout {
|
||||
_textAuthor = findViewById(R.id.text_author)
|
||||
_creatorThumbnail = findViewById(R.id.image_thumbnail)
|
||||
_layoutParentComment = findViewById(R.id.layout_parent_comment)
|
||||
_loaderOverlay = findViewById(R.id.loader_overlay)
|
||||
setLoading(false);
|
||||
|
||||
_layoutItems.removeView(_layoutParentComment)
|
||||
_commentsList.setPrependedView(_layoutParentComment)
|
||||
|
||||
_addCommentView.onCommentAdded.subscribe {
|
||||
_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.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;
|
||||
if (readonly) {
|
||||
_addCommentView.visibility = View.GONE;
|
||||
@@ -109,6 +142,136 @@ class RepliesOverlay : LinearLayout {
|
||||
_topbar.setInfo(context.getString(R.string.Replies), metadata);
|
||||
_commentsList.load(readonly, loader);
|
||||
_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() {
|
||||
@@ -116,4 +279,8 @@ class RepliesOverlay : LinearLayout {
|
||||
_onCommentAdded = null;
|
||||
_commentsList.cancel();
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "RepliesOverlay"
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,10 @@ class SupportOverlay : LinearLayout {
|
||||
private val _topbar: OverlayTopbar;
|
||||
private val _support: SupportView;
|
||||
|
||||
val hasSupportItems: Boolean get() {
|
||||
return _support.hasSupportItems;
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
inflate(context, R.layout.overlay_support, this)
|
||||
_topbar = findViewById(R.id.topbar);
|
||||
|
||||
+38
@@ -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 _iconSuffix: ImageView
|
||||
private val _text: TextView
|
||||
private val _textDescription: TextView
|
||||
val onClick = Event0()
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
@@ -21,11 +22,13 @@ class WidePillButton : LinearLayout {
|
||||
_iconPrefix = findViewById(R.id.image_prefix)
|
||||
_iconSuffix = findViewById(R.id.image_suffix)
|
||||
_text = findViewById(R.id.text)
|
||||
_textDescription = findViewById(R.id.text_description)
|
||||
|
||||
val attrArr = context.obtainStyledAttributes(attrs, R.styleable.WidePillButton, 0, 0)
|
||||
setIconPrefix(attrArr.getResourceId(R.styleable.WidePillButton_widePillIconPrefix, -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()
|
||||
|
||||
findViewById<LinearLayout>(R.id.root).setOnClickListener {
|
||||
@@ -54,4 +57,13 @@ class WidePillButton : LinearLayout {
|
||||
fun setText(t: CharSequence) {
|
||||
_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.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.KeyCharacterMap.UnavailableException
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
@@ -12,10 +11,8 @@ import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.states.StateApp
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
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.video.IPlatformVideoDetails
|
||||
@@ -25,6 +22,8 @@ import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||
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.views.adapters.CommentViewHolder
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
@@ -72,6 +71,9 @@ class CommentsList : ConstraintLayout {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
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 val _prependedView: FrameLayout;
|
||||
private var _readonly: Boolean = false;
|
||||
private val _layoutScrollToTop: FrameLayout;
|
||||
|
||||
var onRepliesClick = Event1<IPlatformComment>();
|
||||
var onCommentsLoaded = Event1<Int>();
|
||||
|
||||
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_comments_list, this, true);
|
||||
|
||||
_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 {
|
||||
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||
setMargins(0, 30, 0, 0)
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.futo.platformplayer.views.video
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
@@ -119,6 +122,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
private var _currentChapterLoopActive = false;
|
||||
private var _currentChapterLoopId: Int = 0;
|
||||
private var _currentChapter: IChapter? = null;
|
||||
private var _promptedForPermissions: Boolean = false;
|
||||
|
||||
|
||||
//Events
|
||||
@@ -235,16 +239,38 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
gestureControl.setupTouchArea(_layoutControls, background);
|
||||
gestureControl.onSeek.subscribe { seekFromCurrent(it); };
|
||||
gestureControl.onSoundAdjusted.subscribe { setVolume(it) };
|
||||
gestureControl.onToggleFullscreen.subscribe { setFullScreen(!isFullScreen) };
|
||||
gestureControl.onBrightnessAdjusted.subscribe {
|
||||
if (it == 1.0f) {
|
||||
_overlay_brightness.visibility = View.GONE;
|
||||
gestureControl.onSoundAdjusted.subscribe {
|
||||
if (Settings.instance.gestureControls.useSystemVolume) {
|
||||
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, (it * maxVolume).toInt(), 0)
|
||||
} else {
|
||||
_overlay_brightness.visibility = View.VISIBLE;
|
||||
_overlay_brightness.setBackgroundColor(Color.valueOf(0.0f, 0.0f, 0.0f, (1.0f - it)).toArgb());
|
||||
setVolume(it)
|
||||
}
|
||||
};
|
||||
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) {
|
||||
_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() {
|
||||
val vidPrev = StatePlayer.instance.getPrevQueueItem(true);
|
||||
val vidNext = StatePlayer.instance.getNextQueueItem(true);
|
||||
@@ -531,7 +581,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||
|
||||
_videoControls_fullscreen.show();
|
||||
videoControls.hide();
|
||||
videoControls.hideImmediately();
|
||||
videoControls.visibility = View.GONE;
|
||||
}
|
||||
else {
|
||||
val lp = background.layoutParams as ConstraintLayout.LayoutParams;
|
||||
@@ -543,7 +594,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
_videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||
|
||||
videoControls.show();
|
||||
_videoControls_fullscreen.hide();
|
||||
_videoControls_fullscreen.hideImmediately();
|
||||
_videoControls_fullscreen.visibility = View.GONE;
|
||||
}
|
||||
|
||||
fitOrFill(fullScreen);
|
||||
@@ -574,6 +626,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
|
||||
override fun onVideoSizeChanged(videoSize: VideoSize) {
|
||||
gestureControl.resetZoomPan()
|
||||
_lastSourceFit = null;
|
||||
if(isFullScreen)
|
||||
fillHeight();
|
||||
@@ -603,6 +656,10 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(playing: Boolean) {
|
||||
super.onIsPlayingChanged(playing)
|
||||
updatePlayPause();
|
||||
}
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
Logger.v(TAG, "onPlaybackStateChanged $playbackState");
|
||||
updatePlayPause()
|
||||
@@ -658,7 +715,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
|
||||
val viewWidth = Math.min(metrics.widthPixels, metrics.heightPixels); //TODO: Get parent width. was this.width
|
||||
val deviceHeight = Math.max(metrics.widthPixels, metrics.heightPixels);
|
||||
val maxHeight = deviceHeight * 0.6;
|
||||
val maxHeight = deviceHeight * 0.4;
|
||||
|
||||
val determinedHeight = if(w > h)
|
||||
((h * (viewWidth.toDouble() / w)).toInt())
|
||||
@@ -730,8 +787,11 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun setGestureSoundFactor(soundFactor: Float) {
|
||||
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 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.
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
super.onPlaybackStateChanged(playbackState)
|
||||
@@ -582,6 +597,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
exoPlayer?.setVolume(volume);
|
||||
}
|
||||
|
||||
protected open fun onSurfaceSizeChanged(width: Int, height: Int) {
|
||||
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
protected open fun onPlayerError(error: PlaybackException) {
|
||||
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 onIsPlayingChanged(playing: Boolean) {
|
||||
|
||||
}
|
||||
protected open fun onPlaybackStateChanged(playbackState: Int) {
|
||||
if (_shouldPlaybackRestartOnConnectivity && playbackState == ExoPlayer.STATE_READY) {
|
||||
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>
|
||||
@@ -2,8 +2,7 @@
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M120,640L120,560L440,560L440,640L120,640ZM120,480L120,400L600,400L600,480L120,480ZM120,320L120,240L600,240L600,320L120,320ZM640,840L640,520L880,680L640,840Z"/>
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
|
||||
<!--Security Warnings-->
|
||||
<LinearLayout
|
||||
android:id="@+id/container_source_warnings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
@@ -55,6 +55,15 @@
|
||||
app:buttonText="@string/install_by_qr"
|
||||
app:buttonSubText="@string/install_a_plugin_by_scanning_a_qr_code"
|
||||
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
|
||||
android:id="@+id/option_url"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -37,9 +37,22 @@
|
||||
android:fontFamily="@font/inter_extra_light" />
|
||||
</FrameLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="match_parent" />
|
||||
<LinearLayout
|
||||
android:id="@+id/button_check_for_updates"
|
||||
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>
|
||||
|
||||
|
||||
@@ -38,12 +38,12 @@
|
||||
android:id="@+id/dialog_text_details"
|
||||
android:layout_width="match_parent"
|
||||
android:textColor="#AAAAAA"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text=""
|
||||
android:textAlignment="center"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp"
|
||||
android:textSize="11dp"
|
||||
android:textSize="13dp"
|
||||
android:layout_height="wrap_content" />
|
||||
<TextView
|
||||
android:id="@+id/dialog_text_code"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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
|
||||
android:id="@+id/layout_header"
|
||||
@@ -65,7 +65,8 @@
|
||||
android:id="@+id/replies_overlay"
|
||||
android:visibility="gone"
|
||||
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"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -128,13 +128,17 @@
|
||||
tools:text="(7 playlists, 85 videos)"
|
||||
/>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:id="@+id/downloads_playlist_list"
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
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>
|
||||
|
||||
<!--Videos-->
|
||||
|
||||
@@ -17,6 +17,40 @@
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="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
|
||||
android:id="@+id/container_enabled"
|
||||
android:layout_width="match_parent"
|
||||
@@ -91,6 +125,7 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/plugin_disclaimer"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp">
|
||||
@@ -113,6 +148,15 @@
|
||||
|
||||
</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>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/container"
|
||||
android:background="#77000000"
|
||||
android:elevation="4dp">
|
||||
android:elevation="4dp"
|
||||
android:clickable="true">
|
||||
<ImageView
|
||||
android:id="@+id/loader"
|
||||
android:layout_width="80dp"
|
||||
|
||||
@@ -1,110 +1,113 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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
|
||||
android:id="@+id/topbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
app:title="Replies"
|
||||
app:metadata="3 replies"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
<LinearLayout android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/layout_items">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/layout_parent_comment"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/topbar"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
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.overlays.OverlayTopbar
|
||||
android:id="@+id/topbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
app:title="Replies"
|
||||
app:metadata="3 replies" />
|
||||
|
||||
<com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
android:id="@+id/image_thumbnail"
|
||||
android:layout_width="25dp"
|
||||
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"
|
||||
<com.futo.platformplayer.views.comments.AddCommentView
|
||||
android:id="@+id/add_comment_view"
|
||||
android:layout_width="match_parent"
|
||||
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"
|
||||
android:text="ShortCircuit" />
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_metadata"
|
||||
android:layout_width="0dp"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/layout_parent_comment"
|
||||
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"
|
||||
android:text=" • 3 years ago" />
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:padding="12dp"
|
||||
android:background="@drawable/background_16_round_4dp">
|
||||
|
||||
<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: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" />
|
||||
<com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
android:id="@+id/image_thumbnail"
|
||||
android:layout_width="25dp"
|
||||
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" />
|
||||
|
||||
</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
|
||||
android:id="@+id/add_comment_view"
|
||||
<TextView
|
||||
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_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginStart="12dp"
|
||||
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>
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="true" />
|
||||
</FrameLayout>
|
||||
@@ -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>
|
||||
@@ -7,5 +7,25 @@
|
||||
android:id="@+id/recycler_comments"
|
||||
android:layout_width="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>
|
||||
@@ -152,4 +152,47 @@
|
||||
android:textSize="16dp"/>
|
||||
</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>
|
||||
@@ -3,7 +3,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="7dp"
|
||||
android:paddingStart="7dp"
|
||||
@@ -21,19 +21,36 @@
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_thumb_up" />
|
||||
|
||||
<TextView
|
||||
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"
|
||||
<LinearLayout
|
||||
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
|
||||
android:id="@+id/image_suffix"
|
||||
|
||||
@@ -214,6 +214,7 @@
|
||||
<string name="videos">Videos</string>
|
||||
<string name="clear_history">Clear history</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="support">Support</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="give_feedback_on_the_application">Give feedback on the application</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="full_screen_portrait">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="writes_to_disk_till_no_space_is_left">Writes to disk till no space is left</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_description">Settings related to rate-limiting this plugin\'s behavior</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="posts">Posts</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="overlay">Overlay</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="add_creator">Add Creators</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">
|
||||
<item>Recommendations</item>
|
||||
<item>Subscriptions</item>
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
<resources>
|
||||
<declare-styleable name="WidePillButton">
|
||||
<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" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
Submodule app/src/playstore/assets/sources/peertube deleted from cfabdc97ab
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"SOURCES_EMBEDDED": {
|
||||
"1c291164-294c-4c2d-800d-7bc6d31d0019": "sources/peertube/PeerTubeConfig.json"
|
||||
},
|
||||
"SOURCES_EMBEDDED_DEFAULT": ["1c291164-294c-4c2d-800d-7bc6d31d0019"],
|
||||
"SOURCES_EMBEDDED": {},
|
||||
"SOURCES_EMBEDDED_DEFAULT": [],
|
||||
"SOURCES_UNDER_CONSTRUCTION": {}
|
||||
}
|
||||
|
||||
Submodule app/src/stable/assets/sources/patreon updated: 139444608d...bc13b38411
Submodule app/src/stable/assets/sources/rumble updated: 263ed8c7df...bedbc4a989
Submodule app/src/stable/assets/sources/youtube updated: c3e2198356...83cccf8ba5
Submodule app/src/unstable/assets/sources/patreon updated: 139444608d...bc13b38411
Submodule app/src/unstable/assets/sources/rumble updated: 263ed8c7df...bedbc4a989
Submodule app/src/unstable/assets/sources/youtube updated: c3e2198356...83cccf8ba5
+1
-1
Submodule dep/polycentricandroid updated: 7695198eea...00927bb700
+1
-1
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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
|
||||
for dir in "${dirs[@]}"; do
|
||||
|
||||
Reference in New Issue
Block a user