mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-29 19:13:01 +02:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a59e04f19 | |||
| b57abb646f | |||
| dd6bde97a9 | |||
| b545545712 | |||
| c1993ffa03 | |||
| 7f7ebafa46 | |||
| b652597924 | |||
| 258fe77928 | |||
| 5a9fcd6fab | |||
| 3c05521a5b | |||
| 034b8b15ae | |||
| 7bd687331b | |||
| 54d58df4b6 | |||
| 9165a9f7cb | |||
| b556d1e81d | |||
| 7c25678211 | |||
| c83a9924e2 | |||
| bbeb9b83a0 | |||
| 06478f3e36 | |||
| 40f20002b2 | |||
| 442272f517 | |||
| 88dae8e9c4 | |||
| 1bbfa7d39e | |||
| edc2b3d295 | |||
| 0006da7385 | |||
| b5ac8b3ec6 | |||
| 78f5169880 | |||
| 3361b77aec | |||
| 8b7c9df286 | |||
| 157d5b4c36 | |||
| 44c8800bec | |||
| 2f0ba1b1f7 | |||
| 36c51f1a0c | |||
| 1dfe18aa6f | |||
| b9bbfb44c5 | |||
| 83843f192d | |||
| 8839d9f1c6 | |||
| 0630ec1d46 | |||
| 4dce8d6a80 | |||
| 3b62f999bf | |||
| 65ae8610fd | |||
| c1c2000c98 | |||
| 287c2d82a1 | |||
| 5cde1650f4 | |||
| a4b90f14ab | |||
| 4826b40136 | |||
| 62618224da | |||
| 49f15e1637 | |||
| e36047c890 | |||
| 8f1199bd08 | |||
| d6e045ea4e | |||
| 304e48996b | |||
| f350dc83b8 | |||
| ebb7beda8c | |||
| a01f3da66e | |||
| 72f5b5fbc0 | |||
| 330aa495c8 | |||
| 0b529ae94d | |||
| 83b35183d0 | |||
| 2cd01eb1fe | |||
| 07378f665a | |||
| bfd5f24f4c | |||
| 3d617187af | |||
| d040b93ca9 | |||
| a410e2962a | |||
| da58b72f9d | |||
| 978f76ffb6 | |||
| 084bac00f5 | |||
| 94454172dd |
+7
-1
@@ -83,8 +83,14 @@
|
|||||||
path = app/src/stable/assets/sources/dailymotion
|
path = app/src/stable/assets/sources/dailymotion
|
||||||
url = ../plugins/dailymotion.git
|
url = ../plugins/dailymotion.git
|
||||||
[submodule "app/src/stable/assets/sources/apple-podcast"]
|
[submodule "app/src/stable/assets/sources/apple-podcast"]
|
||||||
path = app/src/stable/assets/sources/apple-podcast
|
path = app/src/stable/assets/sources/apple-podcasts
|
||||||
url = ../plugins/apple-podcasts.git
|
url = ../plugins/apple-podcasts.git
|
||||||
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
|
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
|
||||||
path = app/src/unstable/assets/sources/apple-podcasts
|
path = app/src/unstable/assets/sources/apple-podcasts
|
||||||
url = ../plugins/apple-podcasts.git
|
url = ../plugins/apple-podcasts.git
|
||||||
|
[submodule "app/src/stable/assets/sources/tedtalks"]
|
||||||
|
path = app/src/stable/assets/sources/tedtalks
|
||||||
|
url = ../plugins/tedtalks.git
|
||||||
|
[submodule "app/src/unstable/assets/sources/tedtalks"]
|
||||||
|
path = app/src/unstable/assets/sources/tedtalks
|
||||||
|
url = ../plugins/tedtalks.git
|
||||||
|
|||||||
+1
-1
@@ -197,7 +197,7 @@ dependencies {
|
|||||||
implementation 'org.jsoup:jsoup:1.15.3'
|
implementation 'org.jsoup:jsoup:1.15.3'
|
||||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
|
implementation 'com.arthenica:ffmpeg-kit-full:6.0-2.LTS'
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
||||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||||
implementation 'com.google.zxing:core:3.4.1'
|
implementation 'com.google.zxing:core:3.4.1'
|
||||||
|
|||||||
@@ -156,7 +156,6 @@
|
|||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SettingsActivity"
|
android:name=".activities.SettingsActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.DeveloperActivity"
|
android:name=".activities.DeveloperActivity"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -11,7 +11,8 @@ let Type = {
|
|||||||
Streams: "STREAMS",
|
Streams: "STREAMS",
|
||||||
Mixed: "MIXED",
|
Mixed: "MIXED",
|
||||||
Live: "LIVE",
|
Live: "LIVE",
|
||||||
Subscriptions: "SUBSCRIPTIONS"
|
Subscriptions: "SUBSCRIPTIONS",
|
||||||
|
Shorts: "SHORTS"
|
||||||
},
|
},
|
||||||
Order: {
|
Order: {
|
||||||
Chronological: "CHRONOLOGICAL"
|
Chronological: "CHRONOLOGICAL"
|
||||||
@@ -244,6 +245,7 @@ class PlatformVideo extends PlatformContent {
|
|||||||
this.viewCount = obj.viewCount ?? -1; //Long
|
this.viewCount = obj.viewCount ?? -1; //Long
|
||||||
|
|
||||||
this.isLive = obj.isLive ?? false; //Boolean
|
this.isLive = obj.isLive ?? false; //Boolean
|
||||||
|
this.isShort = !!obj.isShort ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class PlatformVideoDetails extends PlatformVideo {
|
class PlatformVideoDetails extends PlatformVideo {
|
||||||
@@ -260,6 +262,11 @@ class PlatformVideoDetails extends PlatformVideo {
|
|||||||
|
|
||||||
this.rating = obj.rating ?? null; //IRating
|
this.rating = obj.rating ?? null; //IRating
|
||||||
this.subtitles = obj.subtitles ?? [];
|
this.subtitles = obj.subtitles ?? [];
|
||||||
|
this.isShort = !!obj.isShort ?? false;
|
||||||
|
|
||||||
|
if (obj.getContentRecommendations) {
|
||||||
|
this.getContentRecommendations = obj.getContentRecommendations
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -226,6 +226,25 @@ fun Long.toHumanTime(isMs: Boolean): String {
|
|||||||
else
|
else
|
||||||
return "${prefix}${minsStr}:${secsStr}"
|
return "${prefix}${minsStr}:${secsStr}"
|
||||||
}
|
}
|
||||||
|
fun Long.toHumanDuration(isMs: Boolean): String {
|
||||||
|
var scaler = 1;
|
||||||
|
if(isMs)
|
||||||
|
scaler = 1000;
|
||||||
|
val v = Math.abs(this);
|
||||||
|
val hours = Math.max(v/(secondsInHour*scaler), 0);
|
||||||
|
val mins = Math.max((v % (secondsInHour*scaler)) / (secondsInMinute * scaler), 0);
|
||||||
|
val minsStr = mins.toString();
|
||||||
|
val seconds = Math.max(((v % (secondsInHour*scaler)) % (secondsInMinute * scaler))/scaler, 0);
|
||||||
|
val secsStr = seconds.toString().padStart(2, '0');
|
||||||
|
val prefix = if (this < 0) { "-" } else { "" };
|
||||||
|
|
||||||
|
return listOf(
|
||||||
|
if(hours > 0) "${hours}h" else null,
|
||||||
|
if(mins > 0) "${mins}m" else null ,
|
||||||
|
if(seconds > 0) "${seconds}s" else null
|
||||||
|
).filterNotNull().joinToString(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//TODO: Determine if below stuff should have its own proper class, seems a bit too complex for a utility method
|
//TODO: Determine if below stuff should have its own proper class, seems a bit too complex for a utility method
|
||||||
fun String.fixHtmlWhitespace(): Spanned {
|
fun String.fixHtmlWhitespace(): Spanned {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import java.io.ByteArrayOutputStream
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.Inet4Address
|
import java.net.Inet4Address
|
||||||
|
import java.net.Inet6Address
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
import com.futo.platformplayer.states.StateAnnouncement
|
import com.futo.platformplayer.states.StateAnnouncement
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.base64UrlToByteArray
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -40,33 +40,25 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
|
|||||||
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
|
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.getDataLinkFromUrl(): Protocol.URLInfoDataLink? {
|
||||||
|
val urlData = if (this.startsWith("polycentric://")) {
|
||||||
|
this.substring("polycentric://".length)
|
||||||
|
} else this;
|
||||||
|
|
||||||
|
val urlBytes = urlData.base64UrlToByteArray();
|
||||||
|
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
|
||||||
|
if (urlInfo.urlType != 4L) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
||||||
|
return dataLink
|
||||||
|
}
|
||||||
|
|
||||||
fun Protocol.Claim.resolveChannelUrl(): String? {
|
fun Protocol.Claim.resolveChannelUrl(): String? {
|
||||||
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
||||||
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
|
||||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
|
|
||||||
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
|
|
||||||
Logger.w("Backfill", "Polycentric prod server not added, adding it.")
|
|
||||||
addServer(PolycentricCache.SERVER)
|
|
||||||
}
|
|
||||||
|
|
||||||
val exceptions = fullyBackfillServers()
|
|
||||||
for (pair in exceptions) {
|
|
||||||
val server = pair.key
|
|
||||||
val exception = pair.value
|
|
||||||
|
|
||||||
StateAnnouncement.instance.registerAnnouncement(
|
|
||||||
"backfill-failed",
|
|
||||||
"Backfill failed",
|
|
||||||
"Failed to backfill server $server. $exception",
|
|
||||||
AnnouncementType.SESSION_RECURRING
|
|
||||||
);
|
|
||||||
|
|
||||||
Logger.e("Backfill", "Failed to backfill server $server.", exception)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -33,10 +33,10 @@ fun Boolean?.toYesNo(): String {
|
|||||||
fun InetAddress?.toUrlAddress(): String {
|
fun InetAddress?.toUrlAddress(): String {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is Inet6Address -> {
|
is Inet6Address -> {
|
||||||
"[${toString()}]"
|
"[${hostAddress}]"
|
||||||
}
|
}
|
||||||
is Inet4Address -> {
|
is Inet4Address -> {
|
||||||
toString()
|
hostAddress
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
throw Exception("Invalid address type")
|
throw Exception("Invalid address type")
|
||||||
|
|||||||
@@ -254,6 +254,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
||||||
var progressBar: Boolean = true;
|
var progressBar: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.hide_hidden_from_search, FieldForm.TOGGLE, R.string.hide_hidden_from_search_description, 7)
|
||||||
|
var hidefromSearch: Boolean = false;
|
||||||
|
|
||||||
|
|
||||||
fun getSearchFeedStyle(): FeedStyle {
|
fun getSearchFeedStyle(): FeedStyle {
|
||||||
if(searchFeedStyle == 0)
|
if(searchFeedStyle == 0)
|
||||||
@@ -291,6 +294,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
|
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
|
||||||
var showSubscriptionGroups: Boolean = true;
|
var showSubscriptionGroups: Boolean = true;
|
||||||
|
|
||||||
|
@FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
|
||||||
|
var useSubscriptionExchange: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||||
var previewFeedItems: Boolean = true;
|
var previewFeedItems: Boolean = true;
|
||||||
|
|
||||||
@@ -353,7 +359,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
var playback = PlaybackSettings();
|
var playback = PlaybackSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class PlaybackSettings {
|
class PlaybackSettings {
|
||||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1)
|
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -2)
|
||||||
@DropdownFieldOptionsId(R.array.audio_languages)
|
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||||
var primaryLanguage: Int = 0;
|
var primaryLanguage: Int = 0;
|
||||||
|
|
||||||
@@ -377,6 +383,8 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
|
||||||
|
var preferOriginalAudio: Boolean = true;
|
||||||
|
|
||||||
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
||||||
|
|
||||||
@@ -641,6 +649,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable
|
@Serializable
|
||||||
class Plugins {
|
class Plugins {
|
||||||
|
|
||||||
|
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
|
||||||
|
var checkDisabledPluginsForUpdates: Boolean = false;
|
||||||
|
|
||||||
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
|
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
|
||||||
var clearCookiesOnLogout: Boolean = true;
|
var clearCookiesOnLogout: Boolean = true;
|
||||||
|
|
||||||
@@ -861,11 +872,13 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
class Other {
|
class Other {
|
||||||
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
|
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
|
||||||
var playlistDeleteConfirmation: Boolean = true;
|
var playlistDeleteConfirmation: Boolean = true;
|
||||||
|
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
|
||||||
|
var playlistAllowDups: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 3)
|
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4)
|
||||||
var polycentricEnabled: Boolean = true;
|
var polycentricEnabled: Boolean = true;
|
||||||
|
|
||||||
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 4)
|
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 5)
|
||||||
var polycentricLocalCache: Boolean = true;
|
var polycentricLocalCache: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -368,8 +368,8 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showChangelogDialog(context: Context, lastVersion: Int) {
|
fun showChangelogDialog(context: Context, lastVersion: Int, changelogs: Map<Int, String>? = null) {
|
||||||
val dialog = ChangelogDialog(context);
|
val dialog = ChangelogDialog(context, changelogs);
|
||||||
registerDialogOpened(dialog);
|
registerDialogOpened(dialog);
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
|
|||||||
@@ -79,6 +79,36 @@ class UISlideOverlays {
|
|||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showQueueOptionsOverlay(context: Context, container: ViewGroup) {
|
||||||
|
UISlideOverlays.showOverlay(container, "Queue options", null, {
|
||||||
|
|
||||||
|
}, SlideUpMenuItem(context, R.drawable.ic_playlist, "Save as playlist", "", "Creates a new playlist with queue as videos", null, {
|
||||||
|
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
|
||||||
|
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
|
||||||
|
|
||||||
|
addPlaylistOverlay.onOK.subscribe {
|
||||||
|
val text = nameInput.text.trim()
|
||||||
|
if (text.isBlank()) {
|
||||||
|
return@subscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
addPlaylistOverlay.hide();
|
||||||
|
nameInput.deactivate();
|
||||||
|
nameInput.clear();
|
||||||
|
StatePlayer.instance.saveQueueAsPlaylist(text);
|
||||||
|
UIDialogs.appToast("Playlist [${text}] created");
|
||||||
|
};
|
||||||
|
|
||||||
|
addPlaylistOverlay.onCancel.subscribe {
|
||||||
|
nameInput.deactivate();
|
||||||
|
nameInput.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
addPlaylistOverlay.show();
|
||||||
|
nameInput.activate();
|
||||||
|
}, false));
|
||||||
|
}
|
||||||
|
|
||||||
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
||||||
val items = arrayListOf<View>();
|
val items = arrayListOf<View>();
|
||||||
|
|
||||||
@@ -335,7 +365,9 @@ class UISlideOverlays {
|
|||||||
call = {
|
call = {
|
||||||
selectedVideoVariant = it
|
selectedVideoVariant = it
|
||||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
if (audioButtons.isEmpty()){
|
||||||
|
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
invokeParent = false
|
invokeParent = false
|
||||||
))
|
))
|
||||||
@@ -370,7 +402,7 @@ class UISlideOverlays {
|
|||||||
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
} else if (source is IHLSManifestAudioSource) {
|
} else if (source is IHLSManifestAudioSource) {
|
||||||
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, sourceUrl), null)
|
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, sourceUrl), null)
|
||||||
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
||||||
slideUpMenuOverlay.hide()
|
slideUpMenuOverlay.hide()
|
||||||
} else {
|
} else {
|
||||||
@@ -417,7 +449,7 @@ class UISlideOverlays {
|
|||||||
}
|
}
|
||||||
|
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
||||||
listOf(listOf(SlideUpMenuItem(
|
listOf((if (audioSources != null) listOf(SlideUpMenuItem(
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_movie,
|
R.drawable.ic_movie,
|
||||||
container.context.getString(R.string.none),
|
container.context.getString(R.string.none),
|
||||||
@@ -430,7 +462,7 @@ class UISlideOverlays {
|
|||||||
menu?.setOk(container.context.getString(R.string.download));
|
menu?.setOk(container.context.getString(R.string.download));
|
||||||
},
|
},
|
||||||
invokeParent = false
|
invokeParent = false
|
||||||
)) +
|
)) else listOf()) +
|
||||||
videoSources
|
videoSources
|
||||||
.filter { it.isDownloadable() }
|
.filter { it.isDownloadable() }
|
||||||
.map {
|
.map {
|
||||||
@@ -895,7 +927,8 @@ class UISlideOverlays {
|
|||||||
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
|
||||||
|
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -906,7 +939,7 @@ class UISlideOverlays {
|
|||||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||||
(listOf(
|
(listOf(
|
||||||
if(!isLimited)
|
if(!isLimited && !video.isLive)
|
||||||
SlideUpMenuItem(
|
SlideUpMenuItem(
|
||||||
container.context,
|
container.context,
|
||||||
R.drawable.ic_download,
|
R.drawable.ic_download,
|
||||||
@@ -991,7 +1024,8 @@ class UISlideOverlays {
|
|||||||
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
|
||||||
|
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -1018,7 +1052,8 @@ class UISlideOverlays {
|
|||||||
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
|
||||||
|
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -1040,7 +1075,10 @@ class UISlideOverlays {
|
|||||||
StatePlayer.TYPE_WATCHLATER,
|
StatePlayer.TYPE_WATCHLATER,
|
||||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "watch later",
|
tag = "watch later",
|
||||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
|
call = {
|
||||||
|
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
|
||||||
|
UIDialogs.appToast("Added to watch later", false);
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1067,7 +1105,8 @@ class UISlideOverlays {
|
|||||||
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
||||||
tag = "",
|
tag = "",
|
||||||
call = {
|
call = {
|
||||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
|
||||||
|
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
|
|
||||||
class AddSourceOptionsActivity : AppCompatActivity() {
|
class AddSourceOptionsActivity : AppCompatActivity() {
|
||||||
lateinit var _buttonBack: ImageButton;
|
lateinit var _buttonBack: ImageButton;
|
||||||
|
|
||||||
|
lateinit var _overlayContainer: FrameLayout;
|
||||||
lateinit var _buttonQR: BigButton;
|
lateinit var _buttonQR: BigButton;
|
||||||
lateinit var _buttonBrowse: BigButton;
|
lateinit var _buttonBrowse: BigButton;
|
||||||
lateinit var _buttonURL: BigButton;
|
lateinit var _buttonURL: BigButton;
|
||||||
@@ -54,6 +56,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
setContentView(R.layout.activity_add_source_options);
|
setContentView(R.layout.activity_add_source_options);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
|
|
||||||
|
_overlayContainer = findViewById(R.id.overlay_container);
|
||||||
_buttonBack = findViewById(R.id.button_back);
|
_buttonBack = findViewById(R.id.button_back);
|
||||||
|
|
||||||
_buttonQR = findViewById(R.id.option_qr);
|
_buttonQR = findViewById(R.id.option_qr);
|
||||||
@@ -81,7 +84,25 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buttonURL.onClick.subscribe {
|
_buttonURL.onClick.subscribe {
|
||||||
UIDialogs.toast(this, getString(R.string.not_implemented_yet));
|
val nameInput = SlideUpMenuTextInput(this, "ex. https://yourplugin.com/config.json");
|
||||||
|
UISlideOverlays.showOverlay(_overlayContainer, "Enter your url", "Install", {
|
||||||
|
|
||||||
|
val content = nameInput.text;
|
||||||
|
|
||||||
|
val url = if (content.startsWith("https://")) {
|
||||||
|
content
|
||||||
|
} else if (content.startsWith("grayjay://plugin/")) {
|
||||||
|
content.substring("grayjay://plugin/".length)
|
||||||
|
} else {
|
||||||
|
UIDialogs.toast(this, getString(R.string.not_a_plugin_url))
|
||||||
|
return@showOverlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
||||||
|
data = Uri.parse(url);
|
||||||
|
};
|
||||||
|
startActivity(intent);
|
||||||
|
}, nameInput)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "LoginActivity";
|
private val TAG = "LoginActivity";
|
||||||
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#_ ]*");
|
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#:_ ]*");
|
||||||
|
|
||||||
private var _callback: ((SourceAuth?) -> Unit)? = null;
|
private var _callback: ((SourceAuth?) -> Unit)? = null;
|
||||||
|
|
||||||
|
|||||||
@@ -1281,7 +1281,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if (toast.long)
|
if (toast.long)
|
||||||
delay(5000);
|
delay(5000);
|
||||||
else
|
else
|
||||||
delay(3000);
|
delay(2500);
|
||||||
}
|
}
|
||||||
Logger.i(TAG, "Ending appToast loop");
|
Logger.i(TAG, "Ending appToast loop");
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
|||||||
+3
-3
@@ -11,16 +11,16 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.LoaderView
|
import com.futo.platformplayer.views.LoaderView
|
||||||
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -87,7 +87,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
|||||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
processHandle.addServer(PolycentricCache.SERVER);
|
processHandle.addServer(ApiMethods.SERVER);
|
||||||
processHandle.setUsername(username);
|
processHandle.setUsername(username);
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
|||||||
+2
-2
@@ -12,12 +12,12 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.KeyPair
|
import com.futo.polycentric.core.KeyPair
|
||||||
import com.futo.polycentric.core.Process
|
import com.futo.polycentric.core.Process
|
||||||
import com.futo.polycentric.core.ProcessSecret
|
import com.futo.polycentric.core.ProcessSecret
|
||||||
@@ -145,7 +145,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||||
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
|
processHandle.fullyBackfillClient(ApiMethods.SERVER);
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
||||||
finish();
|
finish();
|
||||||
|
|||||||
@@ -21,10 +21,8 @@ import com.bumptech.glide.Glide
|
|||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
@@ -32,8 +30,10 @@ import com.futo.platformplayer.states.StateApp
|
|||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||||
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||||
import com.futo.polycentric.core.toBase64Url
|
import com.futo.polycentric.core.toBase64Url
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
@@ -145,7 +145,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
|
processHandle.fullyBackfillClient(ApiMethods.SERVER)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
updateUI();
|
updateUI();
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ class SyncHomeActivity : AppCompatActivity() {
|
|||||||
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
|
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
|
||||||
val connected = session?.connected ?: false
|
val connected = session?.connected ?: false
|
||||||
syncDeviceView.setLinkType(if (connected) LinkType.Local else LinkType.None)
|
syncDeviceView.setLinkType(if (connected) LinkType.Local else LinkType.None)
|
||||||
.setName(publicKey)
|
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
|
||||||
|
//TODO: also display public key?
|
||||||
.setStatus(if (connected) "Connected" else "Disconnected")
|
.setStatus(if (connected) "Connected" else "Disconnected")
|
||||||
return syncDeviceView
|
return syncDeviceView
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
|
|||||||
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
|
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
|
||||||
|
|
||||||
current += bytesToSend.toLong()
|
current += bytesToSend.toLong()
|
||||||
if (current >= end) {
|
if (current > end) {
|
||||||
Logger.i(TAG, "Expected amount of bytes sent")
|
Logger.i(TAG, "Expected amount of bytes sent")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class ResultCapabilities(
|
|||||||
const val TYPE_POSTS = "POSTS";
|
const val TYPE_POSTS = "POSTS";
|
||||||
const val TYPE_MIXED = "MIXED";
|
const val TYPE_MIXED = "MIXED";
|
||||||
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
|
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
|
||||||
|
const val TYPE_SHORTS = "SHORTS";
|
||||||
|
|
||||||
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
||||||
|
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.contents
|
|||||||
|
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
interface IPlatformContent {
|
interface IPlatformContent {
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ package com.futo.platformplayer.api.media.models.streams
|
|||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
|
||||||
class LocalVideoMuxedSourceDescriptor(
|
class DownloadedVideoMuxedSourceDescriptor(
|
||||||
private val video: VideoLocal
|
private val video: VideoLocal
|
||||||
) : VideoMuxedSourceDescriptor() {
|
) : VideoMuxedSourceDescriptor() {
|
||||||
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
|
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
|
||||||
+5
-2
@@ -13,7 +13,8 @@ class AudioUrlSource(
|
|||||||
override val codec: String = "",
|
override val codec: String = "",
|
||||||
override val language: String = Language.UNKNOWN,
|
override val language: String = Language.UNKNOWN,
|
||||||
override val duration: Long? = null,
|
override val duration: Long? = null,
|
||||||
override var priority: Boolean = false
|
override var priority: Boolean = false,
|
||||||
|
override var original: Boolean = false
|
||||||
) : IAudioUrlSource, IStreamMetaDataSource{
|
) : IAudioUrlSource, IStreamMetaDataSource{
|
||||||
override var streamMetaData: StreamMetaData? = null;
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
@@ -36,7 +37,9 @@ class AudioUrlSource(
|
|||||||
source.container,
|
source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.language,
|
source.language,
|
||||||
source.duration
|
source.duration,
|
||||||
|
source.priority,
|
||||||
|
source.original
|
||||||
);
|
);
|
||||||
ret.streamMetaData = streamData;
|
ret.streamMetaData = streamData;
|
||||||
|
|
||||||
|
|||||||
+1
@@ -27,6 +27,7 @@ class HLSVariantAudioUrlSource(
|
|||||||
override val language: String,
|
override val language: String,
|
||||||
override val duration: Long?,
|
override val duration: Long?,
|
||||||
override val priority: Boolean,
|
override val priority: Boolean,
|
||||||
|
override val original: Boolean,
|
||||||
val url: String
|
val url: String
|
||||||
) : IAudioUrlSource {
|
) : IAudioUrlSource {
|
||||||
override fun getAudioUrl(): String {
|
override fun getAudioUrl(): String {
|
||||||
|
|||||||
+1
@@ -8,4 +8,5 @@ interface IAudioSource {
|
|||||||
val language : String;
|
val language : String;
|
||||||
val duration : Long?;
|
val duration : Long?;
|
||||||
val priority: Boolean;
|
val priority: Boolean;
|
||||||
|
val original: Boolean;
|
||||||
}
|
}
|
||||||
+3
-2
@@ -15,6 +15,7 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
|||||||
override val duration: Long? = null;
|
override val duration: Long? = null;
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
|
override val original: Boolean = false;
|
||||||
|
|
||||||
val filePath : String;
|
val filePath : String;
|
||||||
val fileSize: Long;
|
val fileSize: Long;
|
||||||
@@ -33,13 +34,13 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromSource(source: IAudioSource, path: String, fileSize: Long): LocalAudioSource {
|
fun fromSource(source: IAudioSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalAudioSource {
|
||||||
return LocalAudioSource(
|
return LocalAudioSource(
|
||||||
source.name,
|
source.name,
|
||||||
path,
|
path,
|
||||||
fileSize,
|
fileSize,
|
||||||
source.bitrate,
|
source.bitrate,
|
||||||
source.container,
|
overrideContainer ?: source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.language
|
source.language
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-2
@@ -35,7 +35,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromSource(source: IVideoSource, path: String, fileSize: Long): LocalVideoSource {
|
fun fromSource(source: IVideoSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalVideoSource {
|
||||||
return LocalVideoSource(
|
return LocalVideoSource(
|
||||||
source.name,
|
source.name,
|
||||||
path,
|
path,
|
||||||
@@ -43,7 +43,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
|||||||
source.width,
|
source.width,
|
||||||
source.height,
|
source.height,
|
||||||
source.duration,
|
source.duration,
|
||||||
source.container,
|
overrideContainer ?: source.container,
|
||||||
source.codec,
|
source.codec,
|
||||||
source.bitrate?:0
|
source.bitrate?:0
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ interface IPlatformVideo : IPlatformContent {
|
|||||||
val viewCount: Long;
|
val viewCount: Long;
|
||||||
|
|
||||||
val isLive : Boolean;
|
val isLive : Boolean;
|
||||||
|
|
||||||
|
val isShort: Boolean;
|
||||||
}
|
}
|
||||||
+3
-1
@@ -14,6 +14,7 @@ import java.time.OffsetDateTime
|
|||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
open class SerializedPlatformVideo(
|
open class SerializedPlatformVideo(
|
||||||
|
override val contentType: ContentType = ContentType.MEDIA,
|
||||||
override val id: PlatformID,
|
override val id: PlatformID,
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val thumbnails: Thumbnails,
|
override val thumbnails: Thumbnails,
|
||||||
@@ -25,8 +26,8 @@ open class SerializedPlatformVideo(
|
|||||||
|
|
||||||
override val duration: Long,
|
override val duration: Long,
|
||||||
override val viewCount: Long,
|
override val viewCount: Long,
|
||||||
|
override val isShort: Boolean = false
|
||||||
) : IPlatformVideo, SerializedPlatformContent {
|
) : IPlatformVideo, SerializedPlatformContent {
|
||||||
override val contentType: ContentType = ContentType.MEDIA;
|
|
||||||
|
|
||||||
override val isLive: Boolean = false;
|
override val isLive: Boolean = false;
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ open class SerializedPlatformVideo(
|
|||||||
companion object {
|
companion object {
|
||||||
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
|
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
|
||||||
return SerializedPlatformVideo(
|
return SerializedPlatformVideo(
|
||||||
|
ContentType.MEDIA,
|
||||||
video.id,
|
video.id,
|
||||||
video.name,
|
video.name,
|
||||||
video.thumbnails,
|
video.thumbnails,
|
||||||
|
|||||||
+2
-1
@@ -38,7 +38,8 @@ open class SerializedPlatformVideoDetails(
|
|||||||
override val video: ISerializedVideoSourceDescriptor,
|
override val video: ISerializedVideoSourceDescriptor,
|
||||||
override val preview: ISerializedVideoSourceDescriptor?,
|
override val preview: ISerializedVideoSourceDescriptor?,
|
||||||
|
|
||||||
override val subtitles: List<SubtitleRawSource> = listOf()
|
override val subtitles: List<SubtitleRawSource> = listOf(),
|
||||||
|
override val isShort: Boolean = false
|
||||||
) : IPlatformVideo, IPlatformVideoDetails {
|
) : IPlatformVideo, IPlatformVideoDetails {
|
||||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||||
|
|
||||||
|
|||||||
+20
-1
@@ -33,6 +33,7 @@ class SourcePluginConfig(
|
|||||||
override val allowEval: Boolean = false,
|
override val allowEval: Boolean = false,
|
||||||
override val allowUrls: List<String> = listOf(),
|
override val allowUrls: List<String> = listOf(),
|
||||||
override val packages: List<String> = listOf(),
|
override val packages: List<String> = listOf(),
|
||||||
|
override val packagesOptional: List<String> = listOf(),
|
||||||
|
|
||||||
val settings: List<Setting> = listOf(),
|
val settings: List<Setting> = listOf(),
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ class SourcePluginConfig(
|
|||||||
var allowAllHttpHeaderAccess: Boolean = false,
|
var allowAllHttpHeaderAccess: Boolean = false,
|
||||||
var maxDownloadParallelism: Int = 0,
|
var maxDownloadParallelism: Int = 0,
|
||||||
var reduceFunctionsInLimitedVersion: Boolean = false,
|
var reduceFunctionsInLimitedVersion: Boolean = false,
|
||||||
|
var changelog: HashMap<String, List<String>>? = null
|
||||||
) : IV8PluginConfig {
|
) : IV8PluginConfig {
|
||||||
|
|
||||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||||
@@ -101,6 +103,10 @@ class SourcePluginConfig(
|
|||||||
if(!packages.contains(pack))
|
if(!packages.contains(pack))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
for(pack in newConfig.packagesOptional) {
|
||||||
|
if(!packagesOptional.contains(pack))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
//Developer Submit Url should be same or empty
|
//Developer Submit Url should be same or empty
|
||||||
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
|
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
|
||||||
return false;
|
return false;
|
||||||
@@ -129,7 +135,7 @@ class SourcePluginConfig(
|
|||||||
|
|
||||||
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
|
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
|
||||||
if (currentlyInstalledPlugin != null) {
|
if (currentlyInstalledPlugin != null) {
|
||||||
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey) {
|
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey && !currentlyInstalledPlugin.config.scriptPublicKey.isNullOrEmpty()) {
|
||||||
list.add(Pair(
|
list.add(Pair(
|
||||||
"Different Author",
|
"Different Author",
|
||||||
"This plugin was signed by a different author. Please ensure that this is correct and that the plugin was not provided by a malicious actor."));
|
"This plugin was signed by a different author. Please ensure that this is correct and that the plugin was not provided by a malicious actor."));
|
||||||
@@ -178,6 +184,19 @@ class SourcePluginConfig(
|
|||||||
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
|
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getChangelogString(version: String): String?{
|
||||||
|
if(changelog == null || !changelog!!.containsKey(version))
|
||||||
|
return null;
|
||||||
|
val changelog = changelog!![version]!!;
|
||||||
|
if(changelog.size > 1) {
|
||||||
|
return "Changelog (${version})\n" + changelog.map { " - " + it.trim() }.joinToString("\n");
|
||||||
|
}
|
||||||
|
else if(changelog.size == 1) {
|
||||||
|
return "Changelog (${version})\n" + changelog[0].trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromJson(json: String, sourceUrl: String? = null): SourcePluginConfig {
|
fun fromJson(json: String, sourceUrl: String? = null): SourcePluginConfig {
|
||||||
val obj = Serializer.json.decodeFromString<SourcePluginConfig>(json);
|
val obj = Serializer.json.decodeFromString<SourcePluginConfig>(json);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.futo.platformplayer.api.media.models.Thumbnails
|
|||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
|
import com.futo.platformplayer.getOrDefault
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
|
|
||||||
open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||||
@@ -17,6 +18,7 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
|||||||
final override val viewCount: Long;
|
final override val viewCount: Long;
|
||||||
|
|
||||||
final override val isLive: Boolean;
|
final override val isLive: Boolean;
|
||||||
|
final override val isShort: Boolean;
|
||||||
|
|
||||||
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
||||||
val contextName = "PlatformVideo";
|
val contextName = "PlatformVideo";
|
||||||
@@ -26,5 +28,6 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
|||||||
duration = _content.getOrThrow<Int>(config, "duration", contextName).toLong();
|
duration = _content.getOrThrow<Int>(config, "duration", contextName).toLong();
|
||||||
viewCount = _content.getOrThrow(config, "viewCount", contextName);
|
viewCount = _content.getOrThrow(config, "viewCount", contextName);
|
||||||
isLive = _content.getOrThrow(config, "isLive", contextName);
|
isLive = _content.getOrThrow(config, "isLive", contextName);
|
||||||
|
isShort = _content.getOrDefault(config, "isShort", contextName, false) ?: false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+3
@@ -21,6 +21,8 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
|
|||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
|
|
||||||
|
override var original: Boolean = false;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
|
||||||
val contextName = "AudioUrlSource";
|
val contextName = "AudioUrlSource";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
@@ -35,6 +37,7 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
|
|||||||
name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}";
|
name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}";
|
||||||
|
|
||||||
priority = if(_obj.has("priority")) obj.getOrThrow(config, "priority", contextName) else false;
|
priority = if(_obj.has("priority")) obj.getOrThrow(config, "priority", contextName) else false;
|
||||||
|
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAudioUrl() : String {
|
override fun getAudioUrl() : String {
|
||||||
|
|||||||
+24
-4
@@ -4,6 +4,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
|||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
||||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
@@ -14,13 +16,14 @@ import com.futo.platformplayer.getOrThrow
|
|||||||
import com.futo.platformplayer.others.Language
|
import com.futo.platformplayer.others.Language
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
|
||||||
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource {
|
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||||
override val container : String = "application/dash+xml";
|
override val container : String;
|
||||||
override val name : String;
|
override val name : String;
|
||||||
override val codec: String;
|
override val codec: String;
|
||||||
override val bitrate: Int;
|
override val bitrate: Int;
|
||||||
override val duration: Long;
|
override val duration: Long;
|
||||||
override val priority: Boolean;
|
override val priority: Boolean;
|
||||||
|
override var original: Boolean = false;
|
||||||
|
|
||||||
override val language: String;
|
override val language: String;
|
||||||
|
|
||||||
@@ -29,17 +32,21 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
|||||||
|
|
||||||
override val hasGenerate: Boolean;
|
override val hasGenerate: Boolean;
|
||||||
|
|
||||||
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||||
val contextName = "DashRawSource";
|
val contextName = "DashRawSource";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
name = _obj.getOrThrow(config, "name", contextName);
|
name = _obj.getOrThrow(config, "name", contextName);
|
||||||
url = _obj.getOrThrow(config, "url", contextName);
|
url = _obj.getOrThrow(config, "url", contextName);
|
||||||
|
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
|
||||||
manifest = _obj.getOrThrow(config, "manifest", contextName);
|
manifest = _obj.getOrThrow(config, "manifest", contextName);
|
||||||
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
||||||
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
||||||
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
||||||
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
||||||
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
|
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
|
||||||
|
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
||||||
hasGenerate = _obj.has("generate");
|
hasGenerate = _obj.has("generate");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,15 +57,28 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
|||||||
throw IllegalStateException("Source object already closed");
|
throw IllegalStateException("Source object already closed");
|
||||||
|
|
||||||
val plugin = _plugin.getUnderlyingPlugin();
|
val plugin = _plugin.getUnderlyingPlugin();
|
||||||
|
|
||||||
|
var result: String? = null;
|
||||||
if(_plugin is DevJSClient)
|
if(_plugin is DevJSClient)
|
||||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
||||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(result != null){
|
||||||
|
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
|
||||||
|
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+28
-6
@@ -6,6 +6,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
|||||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
||||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
@@ -20,8 +22,8 @@ interface IJSDashManifestRawSource {
|
|||||||
var manifest: String?;
|
var manifest: String?;
|
||||||
fun generate(): String?;
|
fun generate(): String?;
|
||||||
}
|
}
|
||||||
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource {
|
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||||
override val container : String = "application/dash+xml";
|
override val container : String;
|
||||||
override val name : String;
|
override val name : String;
|
||||||
override val width: Int;
|
override val width: Int;
|
||||||
override val height: Int;
|
override val height: Int;
|
||||||
@@ -36,11 +38,14 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
|||||||
override val hasGenerate: Boolean;
|
override val hasGenerate: Boolean;
|
||||||
val canMerge: Boolean;
|
val canMerge: Boolean;
|
||||||
|
|
||||||
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||||
val contextName = "DashRawSource";
|
val contextName = "DashRawSource";
|
||||||
val config = plugin.config;
|
val config = plugin.config;
|
||||||
name = _obj.getOrThrow(config, "name", contextName);
|
name = _obj.getOrThrow(config, "name", contextName);
|
||||||
url = _obj.getOrThrow(config, "url", contextName);
|
url = _obj.getOrThrow(config, "url", contextName);
|
||||||
|
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
|
||||||
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
|
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
|
||||||
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
|
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
|
||||||
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
|
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
|
||||||
@@ -57,17 +62,30 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
|||||||
return manifest;
|
return manifest;
|
||||||
if(_obj.isClosed)
|
if(_obj.isClosed)
|
||||||
throw IllegalStateException("Source object already closed");
|
throw IllegalStateException("Source object already closed");
|
||||||
|
|
||||||
|
var result: String? = null;
|
||||||
if(_plugin is DevJSClient) {
|
if(_plugin is DevJSClient) {
|
||||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
||||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||||
_obj.invokeString("generate");
|
_obj.invokeString("generate");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(result != null){
|
||||||
|
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||||
|
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
|
||||||
|
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,12 +118,16 @@ class JSDashManifestMergingRawSource(
|
|||||||
if(videoDash == null) return null;
|
if(videoDash == null) return null;
|
||||||
|
|
||||||
//TODO: Temporary simple solution..make more reliable version
|
//TODO: Temporary simple solution..make more reliable version
|
||||||
|
|
||||||
|
var result: String? = null;
|
||||||
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
|
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
|
||||||
if(audioAdaptationSet != null) {
|
if(audioAdaptationSet != null) {
|
||||||
return videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
result = videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return videoDash;
|
result = videoDash;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
+2
@@ -21,6 +21,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
|||||||
override val language: String;
|
override val language: String;
|
||||||
|
|
||||||
override var priority: Boolean = false;
|
override var priority: Boolean = false;
|
||||||
|
override var original: Boolean = false;
|
||||||
|
|
||||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
|
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
|
||||||
val contextName = "HLSAudioSource";
|
val contextName = "HLSAudioSource";
|
||||||
@@ -32,6 +33,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
|||||||
language = _obj.getOrThrow(config, "language", contextName);
|
language = _obj.getOrThrow(config, "language", contextName);
|
||||||
|
|
||||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||||
|
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local
|
||||||
|
|
||||||
|
class LocalClient {
|
||||||
|
//TODO
|
||||||
|
}
|
||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.DownloadedVideoMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
import java.io.File
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
|
class LocalVideoDetails: IPlatformVideoDetails {
|
||||||
|
|
||||||
|
override val contentType: ContentType get() = ContentType.UNKNOWN;
|
||||||
|
|
||||||
|
override val id: PlatformID;
|
||||||
|
override val name: String;
|
||||||
|
override val author: PlatformAuthorLink;
|
||||||
|
|
||||||
|
override val datetime: OffsetDateTime?;
|
||||||
|
|
||||||
|
override val url: String;
|
||||||
|
override val shareUrl: String;
|
||||||
|
override val rating: IRating = RatingLikes(0);
|
||||||
|
override val description: String = "";
|
||||||
|
|
||||||
|
override val video: IVideoSourceDescriptor;
|
||||||
|
override val preview: IVideoSourceDescriptor? = null;
|
||||||
|
override val live: IVideoSource? = null;
|
||||||
|
override val dash: IDashManifestSource? = null;
|
||||||
|
override val hls: IHLSManifestSource? = null;
|
||||||
|
override val subtitles: List<ISubtitleSource> = listOf()
|
||||||
|
|
||||||
|
override val thumbnails: Thumbnails;
|
||||||
|
override val duration: Long;
|
||||||
|
override val viewCount: Long = 0;
|
||||||
|
override val isLive: Boolean = false;
|
||||||
|
override val isShort: Boolean = false;
|
||||||
|
|
||||||
|
constructor(file: File) {
|
||||||
|
id = PlatformID("Local", file.path, "LOCAL")
|
||||||
|
name = file.name;
|
||||||
|
author = PlatformAuthorLink.UNKNOWN;
|
||||||
|
|
||||||
|
url = file.canonicalPath;
|
||||||
|
shareUrl = "";
|
||||||
|
|
||||||
|
duration = 0;
|
||||||
|
thumbnails = Thumbnails(arrayOf());
|
||||||
|
|
||||||
|
datetime = OffsetDateTime.ofInstant(
|
||||||
|
Instant.ofEpochMilli(file.lastModified()),
|
||||||
|
ZoneId.systemDefault()
|
||||||
|
);
|
||||||
|
video = LocalVideoMuxedSourceDescriptor(LocalVideoFileSource(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPlaybackTracker(): IPlaybackTracker? {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models
|
||||||
|
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
||||||
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
|
||||||
|
class LocalVideoMuxedSourceDescriptor(
|
||||||
|
private val video: LocalVideoFileSource
|
||||||
|
) : VideoMuxedSourceDescriptor() {
|
||||||
|
override val videoSources: Array<IVideoSource> get() = arrayOf(video);
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Video
|
||||||
|
|
||||||
|
class MediaStoreVideo {
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val URI = MediaStore.Files.getContentUri("external");
|
||||||
|
val PROJECTION = arrayOf(Video.Media._ID, Video.Media.TITLE, Video.Media.DURATION, Video.Media.HEIGHT, Video.Media.WIDTH, Video.Media.MIME_TYPE);
|
||||||
|
val ORDER = MediaStore.Video.Media.TITLE;
|
||||||
|
|
||||||
|
fun readMediaStoreVideo(cursor: Cursor) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun query(context: Context, selection: String, args: Array<String>, order: String? = null): Cursor? {
|
||||||
|
val cursor = context.contentResolver.query(URI, PROJECTION, selection, args, order ?: ORDER, null);
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models.sources
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Video
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class LocalVideoFileSource: IVideoSource {
|
||||||
|
|
||||||
|
|
||||||
|
override val name: String;
|
||||||
|
override val width: Int;
|
||||||
|
override val height: Int;
|
||||||
|
override val container: String;
|
||||||
|
override val codec: String = ""
|
||||||
|
override val bitrate: Int = 0
|
||||||
|
override val duration: Long;
|
||||||
|
override val priority: Boolean = false;
|
||||||
|
|
||||||
|
constructor(file: File) {
|
||||||
|
name = file.name;
|
||||||
|
width = 0;
|
||||||
|
height = 0;
|
||||||
|
container = VideoHelper.videoExtensionToMimetype(file.extension) ?: "";
|
||||||
|
duration = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -64,7 +64,7 @@ class StateCasting {
|
|||||||
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
||||||
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
||||||
|
|
||||||
private val _castServer = ManagedHttpServer(9999);
|
private val _castServer = ManagedHttpServer();
|
||||||
private var _started = false;
|
private var _started = false;
|
||||||
|
|
||||||
var devices: HashMap<String, CastingDevice> = hashMapOf();
|
var devices: HashMap<String, CastingDevice> = hashMapOf();
|
||||||
|
|||||||
@@ -1,37 +1,24 @@
|
|||||||
package com.futo.platformplayer.dialogs
|
package com.futo.platformplayer.dialogs
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.app.PendingIntent.*
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.method.ScrollingMovementMethod
|
import android.text.method.ScrollingMovementMethod
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.receivers.InstallReceiver
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
|
||||||
import com.futo.platformplayer.states.StateUpdate
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
class ChangelogDialog(context: Context?) : AlertDialog(context) {
|
class ChangelogDialog(context: Context?, val changelogs: Map<Int, String>? = null) : AlertDialog(context) {
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = "ChangelogDialog";
|
private val TAG = "ChangelogDialog";
|
||||||
}
|
}
|
||||||
@@ -48,7 +35,11 @@ class ChangelogDialog(context: Context?) : AlertDialog(context) {
|
|||||||
private var _maxVersion: Int = 0;
|
private var _maxVersion: Int = 0;
|
||||||
private var _managedHttpClient = ManagedHttpClient();
|
private var _managedHttpClient = ManagedHttpClient();
|
||||||
|
|
||||||
private val _taskDownloadChangelog = TaskHandler<Int, String?>(StateApp.instance.scopeGetter, { version -> StateUpdate.instance.downloadChangelog(_managedHttpClient, version) })
|
private val _taskDownloadChangelog = TaskHandler<Int, String?>(StateApp.instance.scopeGetter, { version -> if(changelogs == null)
|
||||||
|
StateUpdate.instance.downloadChangelog(_managedHttpClient, version)
|
||||||
|
else
|
||||||
|
changelogs[version]
|
||||||
|
})
|
||||||
.success { setChangelog(it); }
|
.success { setChangelog(it); }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load changelog.", it);
|
Logger.w(TAG, "Failed to load changelog.", it);
|
||||||
@@ -97,7 +88,7 @@ class ChangelogDialog(context: Context?) : AlertDialog(context) {
|
|||||||
setVersion(version);
|
setVersion(version);
|
||||||
|
|
||||||
val currentVersion = BuildConfig.VERSION_CODE;
|
val currentVersion = BuildConfig.VERSION_CODE;
|
||||||
_buttonUpdate.visibility = if (currentVersion == _maxVersion) View.GONE else View.VISIBLE;
|
_buttonUpdate.visibility = if (currentVersion == _maxVersion || changelogs != null) View.GONE else View.VISIBLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setVersion(version: Int) {
|
private fun setVersion(version: Int) {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComm
|
|||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
@@ -30,6 +29,7 @@ import com.futo.platformplayer.states.StatePolycentric
|
|||||||
import com.futo.polycentric.core.ClaimType
|
import com.futo.polycentric.core.ClaimType
|
||||||
import com.futo.polycentric.core.Store
|
import com.futo.polycentric.core.Store
|
||||||
import com.futo.polycentric.core.SystemState
|
import com.futo.polycentric.core.SystemState
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
private lateinit var _buttonInstall: LinearLayout;
|
private lateinit var _buttonInstall: LinearLayout;
|
||||||
|
|
||||||
private lateinit var _textPlugin: TextView;
|
private lateinit var _textPlugin: TextView;
|
||||||
|
private lateinit var _textChangelog: TextView;
|
||||||
private lateinit var _textProgres: TextView;
|
private lateinit var _textProgres: TextView;
|
||||||
private lateinit var _textError: TextView;
|
private lateinit var _textError: TextView;
|
||||||
private lateinit var _textResult: TextView;
|
private lateinit var _textResult: TextView;
|
||||||
@@ -94,6 +95,7 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
_buttonInstall = findViewById(R.id.button_install);
|
_buttonInstall = findViewById(R.id.button_install);
|
||||||
|
|
||||||
_textPlugin = findViewById(R.id.text_plugin);
|
_textPlugin = findViewById(R.id.text_plugin);
|
||||||
|
_textChangelog = findViewById(R.id.text_changelog);
|
||||||
_textProgres = findViewById(R.id.text_progress);
|
_textProgres = findViewById(R.id.text_progress);
|
||||||
_textError = findViewById(R.id.text_error);
|
_textError = findViewById(R.id.text_error);
|
||||||
_textResult = findViewById(R.id.text_result);
|
_textResult = findViewById(R.id.text_result);
|
||||||
@@ -110,6 +112,27 @@ class PluginUpdateDialog : AlertDialog {
|
|||||||
_updateSpinner = findViewById(R.id.update_spinner);
|
_updateSpinner = findViewById(R.id.update_spinner);
|
||||||
_iconPlugin = findViewById(R.id.icon_plugin);
|
_iconPlugin = findViewById(R.id.icon_plugin);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var changelogVersion = _newConfig.version.toString();
|
||||||
|
if (_newConfig.changelog != null && _newConfig.changelog?.containsKey(changelogVersion) == true) {
|
||||||
|
_textChangelog.movementMethod = ScrollingMovementMethod();
|
||||||
|
val changelog = _newConfig.changelog!![changelogVersion]!!;
|
||||||
|
if(changelog.size > 1) {
|
||||||
|
_textChangelog.text = "Changelog (${_newConfig.version})\n" + changelog.map { " - " + it.trim() }.joinToString("\n");
|
||||||
|
}
|
||||||
|
else if(changelog.size == 1) {
|
||||||
|
_textChangelog.text = "Changelog (${_newConfig.version})\n" + changelog[0].trim();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
_textChangelog.visibility = View.GONE;
|
||||||
|
} else
|
||||||
|
_textChangelog.visibility = View.GONE;
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
_textChangelog.visibility = View.GONE;
|
||||||
|
Logger.e(TAG, "Invalid changelog? ", ex);
|
||||||
|
}
|
||||||
|
|
||||||
_buttonCancel1.setOnClickListener {
|
_buttonCancel1.setOnClickListener {
|
||||||
dismiss();
|
dismiss();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -141,11 +141,17 @@ class VideoDownload {
|
|||||||
var error: String? = null;
|
var error: String? = null;
|
||||||
|
|
||||||
var videoFilePath: String? = null;
|
var videoFilePath: String? = null;
|
||||||
var videoFileName: String? = null;
|
var videoFileNameBase: String? = null;
|
||||||
|
var videoFileNameExt: String? = null;
|
||||||
|
val videoFileName: String? get() = if(videoFileNameBase.isNullOrEmpty()) null else videoFileNameBase + (if(!videoFileNameExt.isNullOrEmpty()) "." + videoFileNameExt else "");
|
||||||
|
var videoOverrideContainer: String? = null;
|
||||||
var videoFileSize: Long? = null;
|
var videoFileSize: Long? = null;
|
||||||
|
|
||||||
var audioFilePath: String? = null;
|
var audioFilePath: String? = null;
|
||||||
var audioFileName: String? = null;
|
var audioFileNameBase: String? = null;
|
||||||
|
var audioFileNameExt: String? = null;
|
||||||
|
val audioFileName: String? get() = if(audioFileNameBase.isNullOrEmpty()) null else audioFileNameBase + (if(!audioFileNameExt.isNullOrEmpty()) "." + audioFileNameExt else "");
|
||||||
|
var audioOverrideContainer: String? = null;
|
||||||
var audioFileSize: Long? = null;
|
var audioFileSize: Long? = null;
|
||||||
|
|
||||||
var subtitleFilePath: String? = null;
|
var subtitleFilePath: String? = null;
|
||||||
@@ -235,11 +241,13 @@ class VideoDownload {
|
|||||||
videoDetails = null;
|
videoDetails = null;
|
||||||
videoSource = null;
|
videoSource = null;
|
||||||
videoSourceLive = null;
|
videoSourceLive = null;
|
||||||
|
videoOverrideContainer = null;
|
||||||
}
|
}
|
||||||
if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
|
if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
|
||||||
videoDetails = null;
|
videoDetails = null;
|
||||||
audioSource = null;
|
audioSource = null;
|
||||||
videoSourceLive = null;
|
videoSourceLive = null;
|
||||||
|
audioOverrideContainer = null;
|
||||||
}
|
}
|
||||||
if(video == null && videoDetails == null)
|
if(video == null && videoDetails == null)
|
||||||
throw IllegalStateException("Missing information for download to complete");
|
throw IllegalStateException("Missing information for download to complete");
|
||||||
@@ -310,6 +318,10 @@ class VideoDownload {
|
|||||||
if(vsource == null)
|
if(vsource == null)
|
||||||
vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
|
vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
|
||||||
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
||||||
|
if(vsource is JSSource) {
|
||||||
|
this.hasVideoRequestExecutor = this.hasVideoRequestExecutor || vsource.hasRequestExecutor;
|
||||||
|
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (vsource is JSDashManifestRawSource && vsource.hasGenerate);
|
||||||
|
}
|
||||||
|
|
||||||
if(vsource == null) {
|
if(vsource == null) {
|
||||||
videoSource = null;
|
videoSource = null;
|
||||||
@@ -361,6 +373,12 @@ class VideoDownload {
|
|||||||
asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
||||||
?: if(videoSource != null ) null
|
?: if(videoSource != null ) null
|
||||||
else throw DownloadException("Could not find a valid video or audio source for download")
|
else throw DownloadException("Could not find a valid video or audio source for download")
|
||||||
|
|
||||||
|
if(asource is JSSource) {
|
||||||
|
this.hasAudioRequestExecutor = this.hasAudioRequestExecutor || asource.hasRequestExecutor;
|
||||||
|
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
|
||||||
|
}
|
||||||
|
|
||||||
if(asource == null) {
|
if(asource == null) {
|
||||||
audioSource = null;
|
audioSource = null;
|
||||||
if(!original.video.isUnMuxed || original.video.videoSources.size == 0)
|
if(!original.video.isUnMuxed || original.video.videoSources.size == 0)
|
||||||
@@ -400,11 +418,13 @@ class VideoDownload {
|
|||||||
else audioSource;
|
else audioSource;
|
||||||
|
|
||||||
if(actualVideoSource != null) {
|
if(actualVideoSource != null) {
|
||||||
videoFileName = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}].${videoContainerToExtension(actualVideoSource!!.container)}".sanitizeFileName();
|
videoFileNameBase = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}]".sanitizeFileName();
|
||||||
|
videoFileNameExt = videoContainerToExtension(actualVideoSource!!.container);
|
||||||
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
||||||
}
|
}
|
||||||
if(actualAudioSource != null) {
|
if(actualAudioSource != null) {
|
||||||
audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(actualAudioSource!!.container)}".sanitizeFileName();
|
audioFileNameBase = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}]".sanitizeFileName();
|
||||||
|
audioFileNameExt = audioContainerToExtension(actualAudioSource!!.container);
|
||||||
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
||||||
}
|
}
|
||||||
if(subtitleSource != null) {
|
if(subtitleSource != null) {
|
||||||
@@ -1052,8 +1072,8 @@ class VideoDownload {
|
|||||||
fun complete() {
|
fun complete() {
|
||||||
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
||||||
val existing = StateDownloads.instance.getCachedVideo(id);
|
val existing = StateDownloads.instance.getCachedVideo(id);
|
||||||
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0) };
|
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0, videoOverrideContainer) };
|
||||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0) };
|
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0, audioOverrideContainer) };
|
||||||
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
||||||
|
|
||||||
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
||||||
@@ -1082,7 +1102,7 @@ class VideoDownload {
|
|||||||
StateDownloads.instance.updateCachedVideo(existing);
|
StateDownloads.instance.updateCachedVideo(existing);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
val newVideo = VideoLocal(videoDetails!!);
|
val newVideo = VideoLocal(videoDetails!!, OffsetDateTime.now());
|
||||||
if(localVideoSource != null)
|
if(localVideoSource != null)
|
||||||
newVideo.videoSource.add(localVideoSource);
|
newVideo.videoSource.add(localVideoSource);
|
||||||
if(localAudioSource != null)
|
if(localAudioSource != null)
|
||||||
@@ -1134,7 +1154,7 @@ class VideoDownload {
|
|||||||
else if (container.contains("video/x-matroska"))
|
else if (container.contains("video/x-matroska"))
|
||||||
return "mkv";
|
return "mkv";
|
||||||
else
|
else
|
||||||
return "video";
|
return "video";//throw IllegalStateException("Unknown container: " + container)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun audioContainerToExtension(container: String): String {
|
fun audioContainerToExtension(container: String): String {
|
||||||
@@ -1145,11 +1165,11 @@ class VideoDownload {
|
|||||||
else if (container.contains("audio/mp3"))
|
else if (container.contains("audio/mp3"))
|
||||||
return "mp3";
|
return "mp3";
|
||||||
else if (container.contains("audio/webm"))
|
else if (container.contains("audio/webm"))
|
||||||
return "webma";
|
return "webm";
|
||||||
else if (container == "application/vnd.apple.mpegurl")
|
else if (container == "application/vnd.apple.mpegurl")
|
||||||
return "mp4";
|
return "mp4a";
|
||||||
else
|
else
|
||||||
return "audio";
|
return "audio";// throw IllegalStateException("Unknown container: " + container)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subtitleContainerToExtension(container: String?): String {
|
fun subtitleContainerToExtension(container: String?): String {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class VideoExport {
|
|||||||
this.subtitleSource = subtitleSource;
|
this.subtitleSource = subtitleSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope {
|
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null, documentRoot: DocumentFile? = null): DocumentFile = coroutineScope {
|
||||||
val v = videoSource;
|
val v = videoSource;
|
||||||
val a = audioSource;
|
val a = audioSource;
|
||||||
val s = subtitleSource;
|
val s = subtitleSource;
|
||||||
@@ -50,7 +50,7 @@ class VideoExport {
|
|||||||
if (s != null) sourceCount++;
|
if (s != null) sourceCount++;
|
||||||
|
|
||||||
val outputFile: DocumentFile?;
|
val outputFile: DocumentFile?;
|
||||||
val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
val downloadRoot = documentRoot ?: StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
||||||
if (sourceCount > 1) {
|
if (sourceCount > 1) {
|
||||||
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
||||||
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|||||||
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
import com.futo.platformplayer.api.media.models.ratings.IRating
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.LocalVideoMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.DownloadedVideoMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.LocalVideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.LocalVideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||||
@@ -23,6 +23,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
|||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
import com.futo.platformplayer.stores.v2.IStoreItem
|
import com.futo.platformplayer.stores.v2.IStoreItem
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
@@ -56,7 +57,7 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
|
|||||||
override val video: IVideoSourceDescriptor get() = if(audioSource.isNotEmpty())
|
override val video: IVideoSourceDescriptor get() = if(audioSource.isNotEmpty())
|
||||||
LocalVideoUnMuxedSourceDescriptor(this)
|
LocalVideoUnMuxedSourceDescriptor(this)
|
||||||
else
|
else
|
||||||
LocalVideoMuxedSourceDescriptor(this);
|
DownloadedVideoMuxedSourceDescriptor(this);
|
||||||
override val preview: IVideoSourceDescriptor? get() = videoSerialized.preview;
|
override val preview: IVideoSourceDescriptor? get() = videoSerialized.preview;
|
||||||
|
|
||||||
override val live: IVideoSource? get() = videoSerialized.live;
|
override val live: IVideoSource? get() = videoSerialized.live;
|
||||||
@@ -70,14 +71,21 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
|
|||||||
|
|
||||||
override val isLive: Boolean get() = videoSerialized.isLive;
|
override val isLive: Boolean get() = videoSerialized.isLive;
|
||||||
|
|
||||||
|
override val isShort: Boolean get() = videoSerialized.isShort;
|
||||||
|
|
||||||
//TODO: Offline subtitles
|
//TODO: Offline subtitles
|
||||||
override val subtitles: List<ISubtitleSource> = listOf();
|
override val subtitles: List<ISubtitleSource> = listOf();
|
||||||
|
|
||||||
constructor(video: SerializedPlatformVideoDetails) {
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
var downloadDate: OffsetDateTime? = null;
|
||||||
|
|
||||||
|
constructor(video: SerializedPlatformVideoDetails, downloadDate: OffsetDateTime? = null) {
|
||||||
this.videoSerialized = video;
|
this.videoSerialized = video;
|
||||||
|
this.downloadDate = downloadDate;
|
||||||
}
|
}
|
||||||
constructor(video: IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) {
|
constructor(video: IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) {
|
||||||
this.videoSerialized = SerializedPlatformVideoDetails.fromVideo(video, subtitleSources);
|
this.videoSerialized = SerializedPlatformVideoDetails.fromVideo(video, subtitleSources);
|
||||||
|
downloadDate = OffsetDateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import com.futo.platformplayer.engine.internal.V8Converter
|
|||||||
import com.futo.platformplayer.engine.packages.PackageBridge
|
import com.futo.platformplayer.engine.packages.PackageBridge
|
||||||
import com.futo.platformplayer.engine.packages.PackageDOMParser
|
import com.futo.platformplayer.engine.packages.PackageDOMParser
|
||||||
import com.futo.platformplayer.engine.packages.PackageHttp
|
import com.futo.platformplayer.engine.packages.PackageHttp
|
||||||
|
import com.futo.platformplayer.engine.packages.PackageJSDOM
|
||||||
import com.futo.platformplayer.engine.packages.PackageUtilities
|
import com.futo.platformplayer.engine.packages.PackageUtilities
|
||||||
import com.futo.platformplayer.engine.packages.V8Package
|
import com.futo.platformplayer.engine.packages.V8Package
|
||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
@@ -94,7 +95,11 @@ class V8Plugin {
|
|||||||
withDependency(PackageBridge(this, config));
|
withDependency(PackageBridge(this, config));
|
||||||
|
|
||||||
for(pack in config.packages)
|
for(pack in config.packages)
|
||||||
withDependency(getPackage(pack));
|
withDependency(getPackage(pack)!!);
|
||||||
|
for(pack in config.packagesOptional)
|
||||||
|
getPackage(pack, true)?.let {
|
||||||
|
withDependency(it);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeAllowDevSubmit(isAllowed: Boolean) {
|
fun changeAllowDevSubmit(isAllowed: Boolean) {
|
||||||
@@ -254,13 +259,14 @@ class V8Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPackage(packageName: String): V8Package {
|
private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? {
|
||||||
//TODO: Auto get all package types?
|
//TODO: Auto get all package types?
|
||||||
return when(packageName) {
|
return when(packageName) {
|
||||||
"DOMParser" -> PackageDOMParser(this)
|
"DOMParser" -> PackageDOMParser(this)
|
||||||
"Http" -> PackageHttp(this, config)
|
"Http" -> PackageHttp(this, config)
|
||||||
"Utilities" -> PackageUtilities(this, config)
|
"Utilities" -> PackageUtilities(this, config)
|
||||||
else -> throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
"JSDOM" -> PackageJSDOM(this, config)
|
||||||
|
else -> if(allowNull) null else throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ interface IV8PluginConfig {
|
|||||||
val allowEval: Boolean;
|
val allowEval: Boolean;
|
||||||
val allowUrls: List<String>;
|
val allowUrls: List<String>;
|
||||||
val packages: List<String>;
|
val packages: List<String>;
|
||||||
|
val packagesOptional: List<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
@@ -13,17 +14,20 @@ class V8PluginConfig : IV8PluginConfig {
|
|||||||
override val allowEval: Boolean;
|
override val allowEval: Boolean;
|
||||||
override val allowUrls: List<String>;
|
override val allowUrls: List<String>;
|
||||||
override val packages: List<String>;
|
override val packages: List<String>;
|
||||||
|
override val packagesOptional: List<String>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
name = "Unknown";
|
name = "Unknown";
|
||||||
allowEval = false;
|
allowEval = false;
|
||||||
allowUrls = listOf();
|
allowUrls = listOf();
|
||||||
packages = listOf();
|
packages = listOf();
|
||||||
|
packagesOptional = listOf();
|
||||||
}
|
}
|
||||||
constructor(name: String, allowEval: Boolean, allowUrls: List<String>, packages: List<String> = listOf()) {
|
constructor(name: String, allowEval: Boolean, allowUrls: List<String>, packages: List<String> = listOf(), packagesOptional: List<String> = listOf()) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.allowEval = allowEval;
|
this.allowEval = allowEval;
|
||||||
this.allowUrls = allowUrls;
|
this.allowUrls = allowUrls;
|
||||||
this.packages = packages;
|
this.packages = packages;
|
||||||
|
this.packagesOptional = packagesOptional;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.futo.platformplayer.engine.packages
|
package com.futo.platformplayer.engine.packages
|
||||||
|
|
||||||
|
import android.media.MediaCodec
|
||||||
|
import android.media.MediaCodecList
|
||||||
import com.caoccao.javet.annotations.V8Function
|
import com.caoccao.javet.annotations.V8Function
|
||||||
import com.caoccao.javet.annotations.V8Property
|
import com.caoccao.javet.annotations.V8Property
|
||||||
import com.caoccao.javet.utils.JavetResourceUtils
|
import com.caoccao.javet.utils.JavetResourceUtils
|
||||||
@@ -187,7 +189,44 @@ class PackageBridge : V8Package {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@V8Function
|
||||||
|
fun getHardwareCodecs(): List<String>{
|
||||||
|
return getSupportedHardwareMediaCodecs();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PackageBridge";
|
private const val TAG = "PackageBridge";
|
||||||
|
|
||||||
|
private var _mediaCodecList: MutableList<String> = mutableListOf();
|
||||||
|
private var _mediaCodecListHardware: MutableList<String> = mutableListOf();
|
||||||
|
|
||||||
|
fun getSupportedMediaCodecs(): List<String>{
|
||||||
|
synchronized(_mediaCodecList) {
|
||||||
|
if(_mediaCodecList.size <= 0)
|
||||||
|
updateMediaCodecList();
|
||||||
|
return _mediaCodecList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun getSupportedHardwareMediaCodecs(): List<String>{
|
||||||
|
synchronized(_mediaCodecList) {
|
||||||
|
if(_mediaCodecList.size <= 0)
|
||||||
|
updateMediaCodecList();
|
||||||
|
return _mediaCodecListHardware;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun updateMediaCodecList() {
|
||||||
|
_mediaCodecList.clear();
|
||||||
|
_mediaCodecListHardware.clear();
|
||||||
|
for(codec in MediaCodecList(MediaCodecList.ALL_CODECS).codecInfos) {
|
||||||
|
if(!codec.isEncoder) {
|
||||||
|
_mediaCodecList.add(codec.canonicalName);
|
||||||
|
if (codec.isHardwareAccelerated)
|
||||||
|
_mediaCodecListHardware.add(codec.canonicalName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,9 +21,13 @@ import com.futo.platformplayer.engine.internal.IV8Convertable
|
|||||||
import com.futo.platformplayer.engine.internal.V8BindObject
|
import com.futo.platformplayer.engine.internal.V8BindObject
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
|
import java.util.concurrent.ForkJoinPool
|
||||||
|
import java.util.concurrent.ForkJoinTask
|
||||||
|
import kotlin.concurrent.thread
|
||||||
import kotlin.streams.asSequence
|
import kotlin.streams.asSequence
|
||||||
|
|
||||||
class PackageHttp: V8Package {
|
class PackageHttp: V8Package {
|
||||||
@@ -42,6 +46,9 @@ class PackageHttp: V8Package {
|
|||||||
override val name: String get() = "Http";
|
override val name: String get() = "Http";
|
||||||
override val variableName: String get() = "http";
|
override val variableName: String get() = "http";
|
||||||
|
|
||||||
|
private var _batchPoolLock: Any = Any();
|
||||||
|
private var _batchPool: ForkJoinPool? = null;
|
||||||
|
|
||||||
|
|
||||||
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
|
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
|
||||||
_config = config;
|
_config = config;
|
||||||
@@ -51,6 +58,37 @@ class PackageHttp: V8Package {
|
|||||||
_packageClientAuth = PackageHttpClient(this, _clientAuth);
|
_packageClientAuth = PackageHttpClient(this, _clientAuth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Automatically adjusting threadpool dedicated per PackageHttp for batch requests.
|
||||||
|
*/
|
||||||
|
private fun <T, R> autoParallelPool(data: List<T>, parallelism: Int, handle: (T)->R): List<Pair<R?, Throwable?>> {
|
||||||
|
synchronized(_batchPoolLock) {
|
||||||
|
val threadsToUse = if (parallelism <= 0) data.size else Math.min(parallelism, data.size);
|
||||||
|
if(_batchPool == null)
|
||||||
|
_batchPool = ForkJoinPool(threadsToUse);
|
||||||
|
var pool = _batchPool ?: return listOf();
|
||||||
|
if(pool.poolSize < threadsToUse) { //Resize pool
|
||||||
|
pool.shutdown();
|
||||||
|
_batchPool = ForkJoinPool(threadsToUse);
|
||||||
|
pool = _batchPool ?: return listOf();
|
||||||
|
}
|
||||||
|
|
||||||
|
val resultTasks = mutableListOf<ForkJoinTask<Pair<R?, Throwable?>>>();
|
||||||
|
for(item in data){
|
||||||
|
resultTasks.add(pool.submit<Pair<R?, Throwable?>> {
|
||||||
|
try {
|
||||||
|
return@submit Pair<R?, Throwable?>(handle(item), null);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
return@submit Pair<R?, Throwable?>(null, ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return resultTasks.map { it.join() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@V8Function
|
@V8Function
|
||||||
fun newClient(withAuth: Boolean): PackageHttpClient {
|
fun newClient(withAuth: Boolean): PackageHttpClient {
|
||||||
val httpClient = if(withAuth) _clientAuth.clone() else _client.clone();
|
val httpClient = if(withAuth) _clientAuth.clone() else _client.clone();
|
||||||
@@ -176,8 +214,6 @@ class PackageHttp: V8Package {
|
|||||||
obj.set("url", url);
|
obj.set("url", url);
|
||||||
obj.set("code", code);
|
obj.set("code", code);
|
||||||
if(body != null) {
|
if(body != null) {
|
||||||
val buffer = runtime.createV8ValueArrayBuffer(body.size);
|
|
||||||
buffer.fromBytes(body);
|
|
||||||
obj.set("body", body);
|
obj.set("body", body);
|
||||||
}
|
}
|
||||||
obj.set("headers", headers);
|
obj.set("headers", headers);
|
||||||
@@ -236,16 +272,19 @@ class PackageHttp: V8Package {
|
|||||||
//Finalizer
|
//Finalizer
|
||||||
@V8Function
|
@V8Function
|
||||||
fun execute(): List<IBridgeHttpResponse?> {
|
fun execute(): List<IBridgeHttpResponse?> {
|
||||||
return _reqs.parallelStream().map {
|
return _package.autoParallelPool(_reqs, -1) {
|
||||||
if(it.second.method == "DUMMY")
|
if(it.second.method == "DUMMY")
|
||||||
return@map null;
|
return@autoParallelPool null;
|
||||||
if(it.second.body != null)
|
if(it.second.body != null)
|
||||||
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers, it.second.respType);
|
return@autoParallelPool it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers, it.second.respType);
|
||||||
else
|
else
|
||||||
return@map it.first.request(it.second.method, it.second.url, it.second.headers, it.second.respType);
|
return@autoParallelPool it.first.request(it.second.method, it.second.url, it.second.headers, it.second.respType);
|
||||||
}
|
}.map {
|
||||||
.asSequence()
|
if(it.second != null)
|
||||||
.toList();
|
throw it.second!!;
|
||||||
|
else
|
||||||
|
return@map it.first;
|
||||||
|
}.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,11 +478,8 @@ class PackageHttp: V8Package {
|
|||||||
else {
|
else {
|
||||||
headers?.forEach { (header, values) ->
|
headers?.forEach { (header, values) ->
|
||||||
val lowerCaseHeader = header.lowercase()
|
val lowerCaseHeader = header.lowercase()
|
||||||
if(lowerCaseHeader == "set-cookie") {
|
if(lowerCaseHeader == "set-cookie" && !values.any { it.lowercase().contains("httponly") })
|
||||||
result[lowerCaseHeader] = values.filter{
|
result[lowerCaseHeader] = values;
|
||||||
!it.lowercase().contains("httponly")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
result[lowerCaseHeader] = values;
|
result[lowerCaseHeader] = values;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.futo.platformplayer.engine.packages
|
||||||
|
|
||||||
|
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||||
|
import com.futo.platformplayer.engine.V8Plugin
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
|
||||||
|
|
||||||
|
class PackageJSDOM : V8Package {
|
||||||
|
@Transient
|
||||||
|
private val _config: IV8PluginConfig;
|
||||||
|
|
||||||
|
override val name: String get() = "JSDOM";
|
||||||
|
override val variableName: String get() = "packageJSDOM";
|
||||||
|
|
||||||
|
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
|
||||||
|
_config = config;
|
||||||
|
plugin.withDependency(StateApp.instance.contextOrNull ?: return, "scripts/JSDOM.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+2
-4
@@ -13,7 +13,6 @@ import com.futo.platformplayer.R
|
|||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fixHtmlLinks
|
import com.futo.platformplayer.fixHtmlLinks
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
@@ -21,6 +20,7 @@ import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
|||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.toHumanNumber
|
import com.futo.platformplayer.toHumanNumber
|
||||||
import com.futo.platformplayer.views.platform.PlatformLinkView
|
import com.futo.platformplayer.views.platform.PlatformLinkView
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
import com.futo.polycentric.core.toName
|
import com.futo.polycentric.core.toName
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
|
|
||||||
@@ -134,9 +134,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!map.containsKey("Harbor"))
|
if(!map.containsKey("Harbor"))
|
||||||
this.context?.let {
|
map.set("Harbor", polycentricProfile.getHarborUrl());
|
||||||
map.set("Harbor", polycentricProfile.getHarborUrl(it));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (map.isNotEmpty())
|
if (map.isNotEmpty())
|
||||||
setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "")
|
setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "")
|
||||||
|
|||||||
+9
-6
@@ -29,7 +29,6 @@ import com.futo.platformplayer.engine.exceptions.PluginException
|
|||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.exceptions.ChannelException
|
import com.futo.platformplayer.exceptions.ChannelException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateCache
|
import com.futo.platformplayer.states.StateCache
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
@@ -39,11 +38,12 @@ import com.futo.platformplayer.views.FeedStyle
|
|||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
class ChannelContentsFragment(private val subType: String? = null) : Fragment(), IChannelTabFragment {
|
||||||
private var _recyclerResults: RecyclerView? = null;
|
private var _recyclerResults: RecyclerView? = null;
|
||||||
private var _glmVideo: GridLayoutManager? = null;
|
private var _glmVideo: GridLayoutManager? = null;
|
||||||
private var _loading = false;
|
private var _loading = false;
|
||||||
@@ -73,9 +73,12 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
if (lastPolycentricProfile != null)
|
if (lastPolycentricProfile != null)
|
||||||
pager= StatePolycentric.instance.getChannelContent(lifecycleScope, lastPolycentricProfile);
|
pager= StatePolycentric.instance.getChannelContent(lifecycleScope, lastPolycentricProfile);
|
||||||
|
|
||||||
if(pager == null)
|
if(pager == null) {
|
||||||
pager = StatePlatform.instance.getChannelContent(channel.url);
|
if(subType != null)
|
||||||
|
pager = StatePlatform.instance.getChannelContent(channel.url, subType);
|
||||||
|
else
|
||||||
|
pager = StatePlatform.instance.getChannelContent(channel.url);
|
||||||
|
}
|
||||||
return pager;
|
return pager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,6 +370,6 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "VideoListFragment";
|
val TAG = "VideoListFragment";
|
||||||
fun newInstance() = ChannelContentsFragment().apply { }
|
fun newInstance(subType: String? = null) = ChannelContentsFragment(subType).apply { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -16,12 +16,12 @@ import com.futo.platformplayer.constructs.Event1
|
|||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.viewholders.CreatorViewHolder
|
import com.futo.platformplayer.views.adapters.viewholders.CreatorViewHolder
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
|
||||||
class ChannelListFragment : Fragment, IChannelTabFragment {
|
class ChannelListFragment : Fragment, IChannelTabFragment {
|
||||||
private var _channels: ArrayList<IPlatformChannel> = arrayListOf();
|
private var _channels: ArrayList<IPlatformChannel> = arrayListOf();
|
||||||
|
|||||||
+1
-1
@@ -8,8 +8,8 @@ import android.widget.TextView
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.views.SupportView
|
import com.futo.platformplayer.views.SupportView
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
|
||||||
|
|
||||||
class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package com.futo.platformplayer.fragment.channel.tab
|
package com.futo.platformplayer.fragment.channel.tab
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
|
||||||
interface IChannelTabFragment {
|
interface IChannelTabFragment {
|
||||||
fun setChannel(channel: IPlatformChannel)
|
fun setChannel(channel: IPlatformChannel)
|
||||||
|
|||||||
+27
-47
@@ -25,6 +25,7 @@ import com.futo.platformplayer.UISlideOverlays
|
|||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
@@ -41,7 +42,6 @@ import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
|||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.SearchType
|
import com.futo.platformplayer.models.SearchType
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.selectHighestResolutionImage
|
import com.futo.platformplayer.selectHighestResolutionImage
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
@@ -54,29 +54,14 @@ import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter
|
|||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||||
import com.futo.polycentric.core.OwnedClaim
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.PublicKey
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
import com.futo.polycentric.core.Store
|
|
||||||
import com.futo.polycentric.core.SystemState
|
|
||||||
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PolycentricProfile(
|
|
||||||
val system: PublicKey, val systemState: SystemState, val ownedClaims: List<OwnedClaim>
|
|
||||||
) {
|
|
||||||
fun getHarborUrl(context: Context): String{
|
|
||||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system));
|
|
||||||
val url = system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable());
|
|
||||||
return "https://harbor.social/" + url.substring("polycentric://".length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChannelFragment : MainFragment() {
|
class ChannelFragment : MainFragment() {
|
||||||
override val isMainView: Boolean = true
|
override val isMainView: Boolean = true
|
||||||
@@ -143,15 +128,14 @@ class ChannelFragment : MainFragment() {
|
|||||||
|
|
||||||
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
|
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
|
||||||
|
|
||||||
private val _taskLoadPolycentricProfile: TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>
|
private val _taskLoadPolycentricProfile: TaskHandler<PlatformID, PolycentricProfile?>
|
||||||
private val _taskGetChannel: TaskHandler<String, IPlatformChannel>
|
private val _taskGetChannel: TaskHandler<String, IPlatformChannel>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
inflater.inflate(R.layout.fragment_channel, this)
|
inflater.inflate(R.layout.fragment_channel, this)
|
||||||
_taskLoadPolycentricProfile =
|
_taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>({ fragment.lifecycleScope },
|
||||||
TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>({ fragment.lifecycleScope },
|
|
||||||
{ id ->
|
{ id ->
|
||||||
return@TaskHandler PolycentricCache.instance.getProfileAsync(id)
|
return@TaskHandler ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, id.claimFieldType.toLong(), id.claimType.toLong(), id.value!!)
|
||||||
}).success { setPolycentricProfile(it, animate = true) }.exception<Throwable> {
|
}).success { setPolycentricProfile(it, animate = true) }.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load polycentric profile.", it)
|
Logger.w(TAG, "Failed to load polycentric profile.", it)
|
||||||
}
|
}
|
||||||
@@ -237,8 +221,8 @@ class ChannelFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
||||||
if (content is IPlatformVideo) {
|
if (content is IPlatformVideo) {
|
||||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)
|
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
|
||||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
adapter.onUrlClicked.subscribe { url ->
|
adapter.onUrlClicked.subscribe { url ->
|
||||||
@@ -327,7 +311,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
_creatorThumbnail.setThumbnail(parameter.thumbnail, true)
|
_creatorThumbnail.setThumbnail(parameter.thumbnail, true)
|
||||||
Glide.with(_imageBanner).clear(_imageBanner)
|
Glide.with(_imageBanner).clear(_imageBanner)
|
||||||
|
|
||||||
loadPolycentricProfile(parameter.id, parameter.url)
|
loadPolycentricProfile(parameter.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
_url = parameter.url
|
_url = parameter.url
|
||||||
@@ -341,7 +325,7 @@ class ChannelFragment : MainFragment() {
|
|||||||
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true)
|
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true)
|
||||||
Glide.with(_imageBanner).clear(_imageBanner)
|
Glide.with(_imageBanner).clear(_imageBanner)
|
||||||
|
|
||||||
loadPolycentricProfile(parameter.channel.id, parameter.channel.url)
|
loadPolycentricProfile(parameter.channel.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
_url = parameter.channel.url
|
_url = parameter.channel.url
|
||||||
@@ -358,16 +342,8 @@ class ChannelFragment : MainFragment() {
|
|||||||
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
|
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadPolycentricProfile(id: PlatformID, url: String) {
|
private fun loadPolycentricProfile(id: PlatformID) {
|
||||||
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true)
|
_taskLoadPolycentricProfile.run(id)
|
||||||
if (cachedPolycentricProfile != null) {
|
|
||||||
setPolycentricProfile(cachedPolycentricProfile, animate = true)
|
|
||||||
if (cachedPolycentricProfile.expired) {
|
|
||||||
_taskLoadPolycentricProfile.run(id)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_taskLoadPolycentricProfile.run(id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setLoading(isLoading: Boolean) {
|
private fun setLoading(isLoading: Boolean) {
|
||||||
@@ -457,6 +433,12 @@ class ChannelFragment : MainFragment() {
|
|||||||
|
|
||||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons)
|
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons)
|
||||||
}
|
}
|
||||||
|
if(plugin != null && plugin.capabilities.hasGetChannelCapabilities) {
|
||||||
|
if(plugin.getChannelCapabilities()?.types?.contains(ResultCapabilities.TYPE_SHORTS) ?: false &&
|
||||||
|
!(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(ChannelTab.SHORTS.ordinal.toLong())) {
|
||||||
|
(_viewPager.adapter as ChannelViewPagerAdapter).insert(1, ChannelTab.SHORTS);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,8 +451,13 @@ class ChannelFragment : MainFragment() {
|
|||||||
R.string.subscribers
|
R.string.subscribers
|
||||||
).lowercase() else ""
|
).lowercase() else ""
|
||||||
|
|
||||||
val supportsPlaylists =
|
var supportsPlaylists = false;
|
||||||
StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists
|
try {
|
||||||
|
supportsPlaylists = StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
//Ignore error
|
||||||
|
Logger.e(TAG, "Failed to check if supports playlists", ex);
|
||||||
|
}
|
||||||
val playlistPosition = 1
|
val playlistPosition = 1
|
||||||
if (supportsPlaylists && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
|
if (supportsPlaylists && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
|
||||||
ChannelTab.PLAYLISTS.ordinal.toLong()
|
ChannelTab.PLAYLISTS.ordinal.toLong()
|
||||||
@@ -521,20 +508,13 @@ class ChannelFragment : MainFragment() {
|
|||||||
|
|
||||||
private fun setPolycentricProfileOr(url: String, or: () -> Unit) {
|
private fun setPolycentricProfileOr(url: String, or: () -> Unit) {
|
||||||
setPolycentricProfile(null, animate = false)
|
setPolycentricProfile(null, animate = false)
|
||||||
|
or()
|
||||||
val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(url) }
|
|
||||||
if (cachedProfile != null) {
|
|
||||||
setPolycentricProfile(cachedProfile, animate = false)
|
|
||||||
} else {
|
|
||||||
or()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPolycentricProfile(
|
private fun setPolycentricProfile(
|
||||||
cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean
|
profile: PolycentricProfile?, animate: Boolean
|
||||||
) {
|
) {
|
||||||
val dp35 = 35.dp(resources)
|
val dp35 = 35.dp(resources)
|
||||||
val profile = cachedPolycentricProfile?.profile
|
|
||||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)?.let {
|
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)?.let {
|
||||||
it.toURLInfoSystemLinkUrl(
|
it.toURLInfoSystemLinkUrl(
|
||||||
profile.system.toProto(), it.process, profile.systemState.servers.toList()
|
profile.system.toProto(), it.process, profile.systemState.servers.toList()
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,6 @@ import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
|||||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
@@ -32,6 +31,7 @@ import com.futo.platformplayer.views.adapters.CommentWithReferenceViewHolder
|
|||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||||
import com.futo.polycentric.core.PublicKey
|
import com.futo.polycentric.core.PublicKey
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
|
|||||||
+2
-2
@@ -82,8 +82,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
};
|
};
|
||||||
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
||||||
if(it is IPlatformVideo) {
|
if(it is IPlatformVideo) {
|
||||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
|
||||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
adapter.onLongPress.subscribe(this) {
|
adapter.onLongPress.subscribe(this) {
|
||||||
|
|||||||
+16
-2
@@ -9,6 +9,7 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.UISlideOverlays
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
@@ -17,6 +18,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
import com.futo.platformplayer.isHttpUrl
|
import com.futo.platformplayer.isHttpUrl
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -159,8 +161,14 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
navigate<RemotePlaylistFragment>(it);
|
navigate<RemotePlaylistFragment>(it);
|
||||||
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
||||||
navigate<ChannelFragment>(it);
|
navigate<ChannelFragment>(it);
|
||||||
else
|
else {
|
||||||
navigate<VideoDetailFragment>(it);
|
val url = it;
|
||||||
|
activity?.let {
|
||||||
|
close()
|
||||||
|
if(it is MainActivity)
|
||||||
|
it.navigate(it.getFragment<VideoDetailFragment>(), url);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
setQuery(it, true);
|
setQuery(it, true);
|
||||||
@@ -222,6 +230,12 @@ class ContentSearchResultsFragment : MainFragment() {
|
|||||||
setSortByOptions(null);
|
setSortByOptions(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
|
if(Settings.instance.search.hidefromSearch)
|
||||||
|
return super.filterResults(results.filter { !StateMeta.instance.isVideoHidden(it.url) && !StateMeta.instance.isCreatorHidden(it.author.url) });
|
||||||
|
return super.filterResults(results)
|
||||||
|
}
|
||||||
|
|
||||||
override fun reload() {
|
override fun reload() {
|
||||||
loadResults();
|
loadResults();
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -10,6 +10,7 @@ import android.widget.EditText
|
|||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -26,6 +27,7 @@ class CreatorsFragment : MainFragment() {
|
|||||||
private var _overlayContainer: FrameLayout? = null;
|
private var _overlayContainer: FrameLayout? = null;
|
||||||
private var _containerSearch: FrameLayout? = null;
|
private var _containerSearch: FrameLayout? = null;
|
||||||
private var _editSearch: EditText? = null;
|
private var _editSearch: EditText? = null;
|
||||||
|
private var _textMeta: TextView? = null;
|
||||||
private var _buttonClearSearch: ImageButton? = null
|
private var _buttonClearSearch: ImageButton? = null
|
||||||
|
|
||||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
@@ -34,6 +36,7 @@ class CreatorsFragment : MainFragment() {
|
|||||||
val editSearch: EditText = view.findViewById(R.id.edit_search);
|
val editSearch: EditText = view.findViewById(R.id.edit_search);
|
||||||
val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search)
|
val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search)
|
||||||
_editSearch = editSearch
|
_editSearch = editSearch
|
||||||
|
_textMeta = view.findViewById(R.id.text_meta);
|
||||||
_buttonClearSearch = buttonClearSearch
|
_buttonClearSearch = buttonClearSearch
|
||||||
buttonClearSearch.setOnClickListener {
|
buttonClearSearch.setOnClickListener {
|
||||||
editSearch.text.clear()
|
editSearch.text.clear()
|
||||||
@@ -41,7 +44,11 @@ class CreatorsFragment : MainFragment() {
|
|||||||
_buttonClearSearch?.visibility = View.INVISIBLE;
|
_buttonClearSearch?.visibility = View.INVISIBLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
|
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription)) { subs ->
|
||||||
|
_textMeta?.let {
|
||||||
|
it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}";
|
||||||
|
}
|
||||||
|
};
|
||||||
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
|
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
|
||||||
adapter.onSettings.subscribe { sub -> _overlayContainer?.let { UISlideOverlays.showSubscriptionOptionsOverlay(sub, it) } }
|
adapter.onSettings.subscribe { sub -> _overlayContainer?.let { UISlideOverlays.showSubscriptionOptionsOverlay(sub, it) } }
|
||||||
|
|
||||||
|
|||||||
+60
-3
@@ -4,8 +4,13 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.Spinner
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
@@ -17,6 +22,7 @@ import com.futo.platformplayer.states.StateDownloads
|
|||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.toHumanBytesSize
|
import com.futo.platformplayer.toHumanBytesSize
|
||||||
|
import com.futo.platformplayer.toHumanDuration
|
||||||
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
||||||
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
||||||
import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder
|
import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder
|
||||||
@@ -25,6 +31,7 @@ import com.futo.platformplayer.views.items.PlaylistDownloadItem
|
|||||||
import com.futo.platformplayer.views.others.ProgressBar
|
import com.futo.platformplayer.views.others.ProgressBar
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
class DownloadsFragment : MainFragment() {
|
class DownloadsFragment : MainFragment() {
|
||||||
private val TAG = "DownloadsFragment";
|
private val TAG = "DownloadsFragment";
|
||||||
@@ -92,8 +99,12 @@ class DownloadsFragment : MainFragment() {
|
|||||||
|
|
||||||
private val _listDownloadedHeader: LinearLayout;
|
private val _listDownloadedHeader: LinearLayout;
|
||||||
private val _listDownloadedMeta: TextView;
|
private val _listDownloadedMeta: TextView;
|
||||||
|
private val _listDownloadSearch: EditText;
|
||||||
private val _listDownloaded: AnyInsertedAdapterView<VideoLocal, VideoDownloadViewHolder>;
|
private val _listDownloaded: AnyInsertedAdapterView<VideoLocal, VideoDownloadViewHolder>;
|
||||||
|
|
||||||
|
private var lastDownloads: List<VideoLocal>? = null;
|
||||||
|
private var ordering: String? = "nameAsc";
|
||||||
|
|
||||||
constructor(frag: DownloadsFragment, inflater: LayoutInflater): super(frag.requireContext()) {
|
constructor(frag: DownloadsFragment, inflater: LayoutInflater): super(frag.requireContext()) {
|
||||||
inflater.inflate(R.layout.fragment_downloads, this);
|
inflater.inflate(R.layout.fragment_downloads, this);
|
||||||
_frag = frag;
|
_frag = frag;
|
||||||
@@ -104,6 +115,7 @@ class DownloadsFragment : MainFragment() {
|
|||||||
|
|
||||||
_listActiveDownloadsContainer = findViewById(R.id.downloads_active_downloads_container);
|
_listActiveDownloadsContainer = findViewById(R.id.downloads_active_downloads_container);
|
||||||
_listActiveDownloadsMeta = findViewById(R.id.downloads_active_downloads_meta);
|
_listActiveDownloadsMeta = findViewById(R.id.downloads_active_downloads_meta);
|
||||||
|
_listDownloadSearch = findViewById(R.id.downloads_search);
|
||||||
_listActiveDownloads = findViewById(R.id.downloads_active_downloads_list);
|
_listActiveDownloads = findViewById(R.id.downloads_active_downloads_list);
|
||||||
|
|
||||||
_listPlaylistsContainer = findViewById(R.id.downloads_playlist_container);
|
_listPlaylistsContainer = findViewById(R.id.downloads_playlist_container);
|
||||||
@@ -113,6 +125,30 @@ class DownloadsFragment : MainFragment() {
|
|||||||
_listDownloadedHeader = findViewById(R.id.downloads_videos_header);
|
_listDownloadedHeader = findViewById(R.id.downloads_videos_header);
|
||||||
_listDownloadedMeta = findViewById(R.id.downloads_videos_meta);
|
_listDownloadedMeta = findViewById(R.id.downloads_videos_meta);
|
||||||
|
|
||||||
|
_listDownloadSearch.addTextChangedListener {
|
||||||
|
updateContentFilters();
|
||||||
|
}
|
||||||
|
val spinnerSortBy: Spinner = findViewById(R.id.spinner_sortby);
|
||||||
|
spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.downloads_sortby_array)).also {
|
||||||
|
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
||||||
|
};
|
||||||
|
spinnerSortBy.setSelection(0);
|
||||||
|
spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
|
||||||
|
when(pos) {
|
||||||
|
0 -> ordering = "nameAsc"
|
||||||
|
1 -> ordering = "nameDesc"
|
||||||
|
2 -> ordering = "downloadDateAsc"
|
||||||
|
3 -> ordering = "downloadDateDesc"
|
||||||
|
4 -> ordering = "releasedAsc"
|
||||||
|
5 -> ordering = "releasedDesc"
|
||||||
|
else -> ordering = null
|
||||||
|
}
|
||||||
|
updateContentFilters()
|
||||||
|
}
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||||
|
};
|
||||||
|
|
||||||
_listDownloaded = findViewById<RecyclerView>(R.id.list_downloaded)
|
_listDownloaded = findViewById<RecyclerView>(R.id.list_downloaded)
|
||||||
.asAnyWithTop(findViewById(R.id.downloads_top)) {
|
.asAnyWithTop(findViewById(R.id.downloads_top)) {
|
||||||
it.onClick.subscribe {
|
it.onClick.subscribe {
|
||||||
@@ -125,7 +161,6 @@ class DownloadsFragment : MainFragment() {
|
|||||||
reloadUI();
|
reloadUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun reloadUI() {
|
fun reloadUI() {
|
||||||
val usage = StateDownloads.instance.getTotalUsage(true);
|
val usage = StateDownloads.instance.getTotalUsage(true);
|
||||||
_usageUsed.text = "${usage.usage.toHumanBytesSize()} " + context.getString(R.string.used);
|
_usageUsed.text = "${usage.usage.toHumanBytesSize()} " + context.getString(R.string.used);
|
||||||
@@ -181,10 +216,32 @@ class DownloadsFragment : MainFragment() {
|
|||||||
_listDownloadedHeader.visibility = GONE;
|
_listDownloadedHeader.visibility = GONE;
|
||||||
} else {
|
} else {
|
||||||
_listDownloadedHeader.visibility = VISIBLE;
|
_listDownloadedHeader.visibility = VISIBLE;
|
||||||
_listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()})";
|
_listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()}${if(downloaded.size > 0) ", ${downloaded.sumOf { it.duration }.toHumanDuration(false)}" else ""})";
|
||||||
}
|
}
|
||||||
|
|
||||||
_listDownloaded.setData(downloaded);
|
lastDownloads = downloaded;
|
||||||
|
_listDownloaded.setData(filterDownloads(downloaded));
|
||||||
|
}
|
||||||
|
fun updateContentFilters(){
|
||||||
|
val toFilter = lastDownloads ?: return;
|
||||||
|
_listDownloaded.setData(filterDownloads(toFilter));
|
||||||
|
}
|
||||||
|
fun filterDownloads(vids: List<VideoLocal>): List<VideoLocal>{
|
||||||
|
var vidsToReturn = vids;
|
||||||
|
if(!_listDownloadSearch.text.isNullOrEmpty())
|
||||||
|
vidsToReturn = vids.filter { it.name.contains(_listDownloadSearch.text, true) || it.author.name.contains(_listDownloadSearch.text, true) };
|
||||||
|
if(!ordering.isNullOrEmpty()) {
|
||||||
|
vidsToReturn = when(ordering){
|
||||||
|
"downloadDateAsc" -> vidsToReturn.sortedBy { it.downloadDate ?: OffsetDateTime.MAX };
|
||||||
|
"downloadDateDesc" -> vidsToReturn.sortedByDescending { it.downloadDate ?: OffsetDateTime.MIN };
|
||||||
|
"nameAsc" -> vidsToReturn.sortedBy { it.name.lowercase() }
|
||||||
|
"nameDesc" -> vidsToReturn.sortedByDescending { it.name.lowercase() }
|
||||||
|
"releasedAsc" -> vidsToReturn.sortedBy { it.datetime ?: OffsetDateTime.MAX }
|
||||||
|
"releasedDesc" -> vidsToReturn.sortedByDescending { it.datetime ?: OffsetDateTime.MIN }
|
||||||
|
else -> vidsToReturn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vidsToReturn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+39
-4
@@ -23,6 +23,7 @@ import com.futo.platformplayer.states.StateMeta
|
|||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.NoResultsView
|
import com.futo.platformplayer.views.NoResultsView
|
||||||
|
import com.futo.platformplayer.views.ToggleBar
|
||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||||
@@ -94,6 +95,8 @@ class HomeFragment : MainFragment() {
|
|||||||
class HomeView : ContentFeedView<HomeFragment> {
|
class HomeView : ContentFeedView<HomeFragment> {
|
||||||
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
|
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
|
||||||
|
|
||||||
|
private var _toggleBar: ToggleBar? = null;
|
||||||
|
|
||||||
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
|
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
|
||||||
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
|
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
|
||||||
|
|
||||||
@@ -127,6 +130,8 @@ class HomeFragment : MainFragment() {
|
|||||||
}, fragment);
|
}, fragment);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
initializeToolbarContent();
|
||||||
|
|
||||||
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||||
showAnnouncementView()
|
showAnnouncementView()
|
||||||
}
|
}
|
||||||
@@ -201,13 +206,43 @@ class HomeFragment : MainFragment() {
|
|||||||
loadResults();
|
loadResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
private val _filterLock = Object();
|
||||||
return results.filter { !StateMeta.instance.isVideoHidden(it.url) && !StateMeta.instance.isCreatorHidden(it.author.url) };
|
private var _toggleRecent = false;
|
||||||
|
fun initializeToolbarContent() {
|
||||||
|
//Not stable enough with current viewport paging, doesn't work with less results, and reloads content instead of just re-filtering existing
|
||||||
|
/*
|
||||||
|
_toggleBar = ToggleBar(context).apply {
|
||||||
|
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
||||||
|
}
|
||||||
|
synchronized(_filterLock) {
|
||||||
|
_toggleBar?.setToggles(
|
||||||
|
//TODO: loadResults needs to be replaced with an internal reload of the current content
|
||||||
|
ToggleBar.Toggle("Recent", _toggleRecent) { _toggleRecent = it; loadResults(false) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_toolbarContentView.addView(_toggleBar, 0);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadResults() {
|
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||||
|
return results.filter {
|
||||||
|
if(StateMeta.instance.isVideoHidden(it.url))
|
||||||
|
return@filter false;
|
||||||
|
if(StateMeta.instance.isCreatorHidden(it.author.url))
|
||||||
|
return@filter false;
|
||||||
|
|
||||||
|
if(_toggleRecent && (it.datetime?.getNowDiffHours() ?: 0) > 23) {
|
||||||
|
return@filter false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return@filter true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadResults(withRefetch: Boolean = true) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
_taskGetPager.run(true);
|
_taskGetPager.run(withRefetch);
|
||||||
}
|
}
|
||||||
private fun loadedResult(pager : IPager<IPlatformContent>) {
|
private fun loadedResult(pager : IPager<IPlatformContent>) {
|
||||||
if (pager is EmptyPager<IPlatformContent>) {
|
if (pager is EmptyPager<IPlatformContent>) {
|
||||||
|
|||||||
+15
-5
@@ -8,6 +8,7 @@ import android.view.ViewGroup
|
|||||||
import androidx.core.app.ShareCompat
|
import androidx.core.app.ShareCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
@@ -78,6 +79,14 @@ class PlaylistFragment : MainFragment() {
|
|||||||
val nameInput = SlideUpMenuTextInput(context, context.getString(R.string.name));
|
val nameInput = SlideUpMenuTextInput(context, context.getString(R.string.name));
|
||||||
val editPlaylistOverlay = SlideUpMenuOverlay(context, overlayContainer, context.getString(R.string.edit_playlist), context.getString(R.string.ok), false, nameInput);
|
val editPlaylistOverlay = SlideUpMenuOverlay(context, overlayContainer, context.getString(R.string.edit_playlist), context.getString(R.string.ok), false, nameInput);
|
||||||
|
|
||||||
|
_buttonExport.setOnClickListener {
|
||||||
|
_playlist?.let {
|
||||||
|
val context = StateApp.instance.contextOrNull ?: return@let;
|
||||||
|
if(context is IWithResultLauncher)
|
||||||
|
StateDownloads.instance.exportPlaylist(context, it.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
_buttonDownload.visibility = View.VISIBLE;
|
_buttonDownload.visibility = View.VISIBLE;
|
||||||
editPlaylistOverlay.onOK.subscribe {
|
editPlaylistOverlay.onOK.subscribe {
|
||||||
val text = nameInput.text;
|
val text = nameInput.text;
|
||||||
@@ -146,7 +155,7 @@ class PlaylistFragment : MainFragment() {
|
|||||||
setName(it.name);
|
setName(it.name);
|
||||||
//TODO: Implement support for pagination
|
//TODO: Implement support for pagination
|
||||||
setVideos(it.videos, false);
|
setVideos(it.videos, false);
|
||||||
setVideoCount(it.videos.size);
|
setMetadata(it.videos.size, it.videos.sumOf { it.duration });
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
@@ -174,8 +183,9 @@ class PlaylistFragment : MainFragment() {
|
|||||||
if (parameter != null) {
|
if (parameter != null) {
|
||||||
setName(parameter.name)
|
setName(parameter.name)
|
||||||
setVideos(parameter.videos, true)
|
setVideos(parameter.videos, true)
|
||||||
setVideoCount(parameter.videos.size)
|
setMetadata(parameter.videos.size, parameter.videos.sumOf { it.duration })
|
||||||
setButtonDownloadVisible(true)
|
setButtonDownloadVisible(true)
|
||||||
|
setButtonExportVisible(false)
|
||||||
setButtonEditVisible(true)
|
setButtonEditVisible(true)
|
||||||
|
|
||||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
|
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
|
||||||
@@ -187,7 +197,7 @@ class PlaylistFragment : MainFragment() {
|
|||||||
} else {
|
} else {
|
||||||
setName(null)
|
setName(null)
|
||||||
setVideos(null, false)
|
setVideos(null, false)
|
||||||
setVideoCount(-1)
|
setMetadata(-1, -1);
|
||||||
setButtonDownloadVisible(false)
|
setButtonDownloadVisible(false)
|
||||||
setButtonEditVisible(false)
|
setButtonEditVisible(false)
|
||||||
}
|
}
|
||||||
@@ -195,7 +205,7 @@ class PlaylistFragment : MainFragment() {
|
|||||||
_playlist = null
|
_playlist = null
|
||||||
_url = parameter.url
|
_url = parameter.url
|
||||||
|
|
||||||
setVideoCount(parameter.videoCount)
|
setMetadata(parameter.videoCount, -1);
|
||||||
setName(parameter.name)
|
setName(parameter.name)
|
||||||
setVideos(null, false)
|
setVideos(null, false)
|
||||||
setButtonDownloadVisible(false)
|
setButtonDownloadVisible(false)
|
||||||
@@ -208,7 +218,7 @@ class PlaylistFragment : MainFragment() {
|
|||||||
|
|
||||||
setName(null)
|
setName(null)
|
||||||
setVideos(null, false)
|
setVideos(null, false)
|
||||||
setVideoCount(-1)
|
setMetadata(-1, -1);
|
||||||
setButtonDownloadVisible(false)
|
setButtonDownloadVisible(false)
|
||||||
setButtonEditVisible(false)
|
setButtonEditVisible(false)
|
||||||
|
|
||||||
|
|||||||
+76
-3
@@ -6,12 +6,17 @@ import android.util.TypedValue
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.Spinner
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -21,11 +26,13 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
|||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
|
import com.futo.platformplayer.views.SearchView
|
||||||
import com.futo.platformplayer.views.adapters.*
|
import com.futo.platformplayer.views.adapters.*
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
|
||||||
class PlaylistsFragment : MainFragment() {
|
class PlaylistsFragment : MainFragment() {
|
||||||
@@ -65,6 +72,7 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
private val _fragment: PlaylistsFragment;
|
private val _fragment: PlaylistsFragment;
|
||||||
|
|
||||||
var watchLater: ArrayList<IPlatformVideo> = arrayListOf();
|
var watchLater: ArrayList<IPlatformVideo> = arrayListOf();
|
||||||
|
var allPlaylists: ArrayList<Playlist> = arrayListOf();
|
||||||
var playlists: ArrayList<Playlist> = arrayListOf();
|
var playlists: ArrayList<Playlist> = arrayListOf();
|
||||||
private var _appBar: AppBarLayout;
|
private var _appBar: AppBarLayout;
|
||||||
private var _adapterWatchLater: VideoListHorizontalAdapter;
|
private var _adapterWatchLater: VideoListHorizontalAdapter;
|
||||||
@@ -72,12 +80,20 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
private var _layoutWatchlist: ConstraintLayout;
|
private var _layoutWatchlist: ConstraintLayout;
|
||||||
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
|
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
|
||||||
|
|
||||||
|
private var _listPlaylistsSearch: EditText;
|
||||||
|
|
||||||
|
private var _ordering: String? = null;
|
||||||
|
|
||||||
|
|
||||||
constructor(fragment: PlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) {
|
constructor(fragment: PlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) {
|
||||||
_fragment = fragment;
|
_fragment = fragment;
|
||||||
inflater.inflate(R.layout.fragment_playlists, this);
|
inflater.inflate(R.layout.fragment_playlists, this);
|
||||||
|
|
||||||
|
_listPlaylistsSearch = findViewById(R.id.playlists_search);
|
||||||
|
|
||||||
watchLater = ArrayList();
|
watchLater = ArrayList();
|
||||||
playlists = ArrayList();
|
playlists = ArrayList();
|
||||||
|
allPlaylists = ArrayList();
|
||||||
|
|
||||||
val recyclerWatchLater = findViewById<RecyclerView>(R.id.recycler_watch_later);
|
val recyclerWatchLater = findViewById<RecyclerView>(R.id.recycler_watch_later);
|
||||||
|
|
||||||
@@ -105,6 +121,7 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
buttonCreatePlaylist.setOnClickListener {
|
buttonCreatePlaylist.setOnClickListener {
|
||||||
_slideUpOverlay = UISlideOverlays.showCreatePlaylistOverlay(findViewById<FrameLayout>(R.id.overlay_create_playlist)) {
|
_slideUpOverlay = UISlideOverlays.showCreatePlaylistOverlay(findViewById<FrameLayout>(R.id.overlay_create_playlist)) {
|
||||||
val playlist = Playlist(it, arrayListOf());
|
val playlist = Playlist(it, arrayListOf());
|
||||||
|
allPlaylists.add(0, playlist);
|
||||||
playlists.add(0, playlist);
|
playlists.add(0, playlist);
|
||||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
|
|
||||||
@@ -120,6 +137,34 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
_appBar = findViewById(R.id.app_bar);
|
_appBar = findViewById(R.id.app_bar);
|
||||||
_layoutWatchlist = findViewById(R.id.layout_watchlist);
|
_layoutWatchlist = findViewById(R.id.layout_watchlist);
|
||||||
|
|
||||||
|
|
||||||
|
_listPlaylistsSearch.addTextChangedListener {
|
||||||
|
updatePlaylistsFiltering();
|
||||||
|
}
|
||||||
|
val spinnerSortBy: Spinner = findViewById(R.id.spinner_sortby);
|
||||||
|
spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.playlists_sortby_array)).also {
|
||||||
|
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
||||||
|
};
|
||||||
|
spinnerSortBy.setSelection(0);
|
||||||
|
spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
|
||||||
|
when(pos) {
|
||||||
|
0 -> _ordering = "nameAsc"
|
||||||
|
1 -> _ordering = "nameDesc"
|
||||||
|
2 -> _ordering = "dateEditAsc"
|
||||||
|
3 -> _ordering = "dateEditDesc"
|
||||||
|
4 -> _ordering = "dateCreateAsc"
|
||||||
|
5 -> _ordering = "dateCreateDesc"
|
||||||
|
6 -> _ordering = "datePlayAsc"
|
||||||
|
7 -> _ordering = "datePlayDesc"
|
||||||
|
else -> _ordering = null
|
||||||
|
}
|
||||||
|
updatePlaylistsFiltering()
|
||||||
|
}
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
findViewById<TextView>(R.id.text_view_all).setOnClickListener { _fragment.navigate<WatchLaterFragment>(context.getString(R.string.watch_later)); };
|
findViewById<TextView>(R.id.text_view_all).setOnClickListener { _fragment.navigate<WatchLaterFragment>(context.getString(R.string.watch_later)); };
|
||||||
StatePlaylists.instance.onWatchLaterChanged.subscribe(this) {
|
StatePlaylists.instance.onWatchLaterChanged.subscribe(this) {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
@@ -134,10 +179,12 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun onShown() {
|
fun onShown() {
|
||||||
|
allPlaylists.clear();
|
||||||
playlists.clear()
|
playlists.clear()
|
||||||
playlists.addAll(
|
allPlaylists.addAll(
|
||||||
StatePlaylists.instance.getPlaylists().sortedByDescending { maxOf(it.datePlayed, it.dateUpdate, it.dateCreation) }
|
StatePlaylists.instance.getPlaylists().sortedByDescending { maxOf(it.datePlayed, it.dateUpdate, it.dateCreation) }
|
||||||
);
|
);
|
||||||
|
playlists.addAll(filterPlaylists(allPlaylists));
|
||||||
_adapterPlaylist.notifyDataSetChanged();
|
_adapterPlaylist.notifyDataSetChanged();
|
||||||
|
|
||||||
updateWatchLater();
|
updateWatchLater();
|
||||||
@@ -157,6 +204,32 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updatePlaylistsFiltering() {
|
||||||
|
val toFilter = allPlaylists ?: return;
|
||||||
|
playlists.clear();
|
||||||
|
playlists.addAll(filterPlaylists(toFilter));
|
||||||
|
_adapterPlaylist.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
private fun filterPlaylists(pls: List<Playlist>): List<Playlist> {
|
||||||
|
var playlistsToReturn = pls;
|
||||||
|
if(!_listPlaylistsSearch.text.isNullOrEmpty())
|
||||||
|
playlistsToReturn = playlistsToReturn.filter { it.name.contains(_listPlaylistsSearch.text, true) };
|
||||||
|
if(!_ordering.isNullOrEmpty()){
|
||||||
|
playlistsToReturn = when(_ordering){
|
||||||
|
"nameAsc" -> playlistsToReturn.sortedBy { it.name.lowercase() }
|
||||||
|
"nameDesc" -> playlistsToReturn.sortedByDescending { it.name.lowercase() };
|
||||||
|
"dateEditAsc" -> playlistsToReturn.sortedBy { it.dateUpdate ?: OffsetDateTime.MAX };
|
||||||
|
"dateEditDesc" -> playlistsToReturn.sortedByDescending { it.dateUpdate ?: OffsetDateTime.MIN }
|
||||||
|
"dateCreateAsc" -> playlistsToReturn.sortedBy { it.dateCreation ?: OffsetDateTime.MAX };
|
||||||
|
"dateCreateDesc" -> playlistsToReturn.sortedByDescending { it.dateCreation ?: OffsetDateTime.MIN }
|
||||||
|
"datePlayAsc" -> playlistsToReturn.sortedBy { it.datePlayed ?: OffsetDateTime.MAX };
|
||||||
|
"datePlayDesc" -> playlistsToReturn.sortedByDescending { it.datePlayed ?: OffsetDateTime.MIN }
|
||||||
|
else -> playlistsToReturn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return playlistsToReturn;
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateWatchLater() {
|
private fun updateWatchLater() {
|
||||||
val watchList = StatePlaylists.instance.getWatchLater();
|
val watchList = StatePlaylists.instance.getWatchLater();
|
||||||
if (watchList.isNotEmpty()) {
|
if (watchList.isNotEmpty()) {
|
||||||
@@ -164,7 +237,7 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
|
|
||||||
_appBar.let { appBar ->
|
_appBar.let { appBar ->
|
||||||
val layoutParams: CoordinatorLayout.LayoutParams = appBar.layoutParams as CoordinatorLayout.LayoutParams;
|
val layoutParams: CoordinatorLayout.LayoutParams = appBar.layoutParams as CoordinatorLayout.LayoutParams;
|
||||||
layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 230.0f, resources.displayMetrics).toInt();
|
layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 315.0f, resources.displayMetrics).toInt();
|
||||||
appBar.layoutParams = layoutParams;
|
appBar.layoutParams = layoutParams;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -172,7 +245,7 @@ class PlaylistsFragment : MainFragment() {
|
|||||||
|
|
||||||
_appBar.let { appBar ->
|
_appBar.let { appBar ->
|
||||||
val layoutParams: CoordinatorLayout.LayoutParams = appBar.layoutParams as CoordinatorLayout.LayoutParams;
|
val layoutParams: CoordinatorLayout.LayoutParams = appBar.layoutParams as CoordinatorLayout.LayoutParams;
|
||||||
layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25.0f, resources.displayMetrics).toInt();
|
layoutParams.height = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 110.0f, resources.displayMetrics).toInt();
|
||||||
appBar.layoutParams = layoutParams;
|
appBar.layoutParams = layoutParams;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-19
@@ -33,10 +33,8 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
|||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fixHtmlWhitespace
|
import com.futo.platformplayer.fixHtmlWhitespace
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
@@ -47,7 +45,6 @@ import com.futo.platformplayer.views.adapters.ChannelTab
|
|||||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewPostView
|
import com.futo.platformplayer.views.adapters.feedtypes.PreviewPostView
|
||||||
import com.futo.platformplayer.views.comments.AddCommentView
|
import com.futo.platformplayer.views.comments.AddCommentView
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.others.Toggle
|
|
||||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
@@ -57,6 +54,8 @@ import com.futo.polycentric.core.ApiMethods
|
|||||||
import com.futo.polycentric.core.ContentType
|
import com.futo.polycentric.core.ContentType
|
||||||
import com.futo.polycentric.core.Models
|
import com.futo.polycentric.core.Models
|
||||||
import com.futo.polycentric.core.Opinion
|
import com.futo.polycentric.core.Opinion
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import com.google.android.flexbox.FlexboxLayout
|
import com.google.android.flexbox.FlexboxLayout
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.google.android.material.shape.CornerFamily
|
import com.google.android.material.shape.CornerFamily
|
||||||
@@ -112,7 +111,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
private var _isLoading = false;
|
private var _isLoading = false;
|
||||||
private var _post: IPlatformPostDetails? = null;
|
private var _post: IPlatformPostDetails? = null;
|
||||||
private var _postOverview: IPlatformPost? = null;
|
private var _postOverview: IPlatformPost? = null;
|
||||||
private var _polycentricProfile: PolycentricCache.CachedPolycentricProfile? = null;
|
private var _polycentricProfile: PolycentricProfile? = null;
|
||||||
private var _version = 0;
|
private var _version = 0;
|
||||||
private var _isRepliesVisible: Boolean = false;
|
private var _isRepliesVisible: Boolean = false;
|
||||||
private var _repliesAnimator: ViewPropertyAnimator? = null;
|
private var _repliesAnimator: ViewPropertyAnimator? = null;
|
||||||
@@ -169,7 +168,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost, null, _fragment);
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost, null, _fragment);
|
||||||
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
|
||||||
|
|
||||||
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) })
|
||||||
.success { it -> setPolycentricProfile(it, animate = true) }
|
.success { it -> setPolycentricProfile(it, animate = true) }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load claims.", it);
|
Logger.w(TAG, "Failed to load claims.", it);
|
||||||
@@ -274,7 +273,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_buttonStore.setOnClickListener {
|
_buttonStore.setOnClickListener {
|
||||||
_polycentricProfile?.profile?.systemState?.store?.let {
|
_polycentricProfile?.systemState?.store?.let {
|
||||||
try {
|
try {
|
||||||
val uri = Uri.parse(it);
|
val uri = Uri.parse(it);
|
||||||
val intent = Intent(Intent.ACTION_VIEW);
|
val intent = Intent(Intent.ACTION_VIEW);
|
||||||
@@ -334,7 +333,7 @@ class PostDetailFragment : MainFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
|
val queryReferencesResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, ref, null,null,
|
||||||
arrayListOf(
|
arrayListOf(
|
||||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
||||||
ContentType.OPINION.value).setValue(
|
ContentType.OPINION.value).setValue(
|
||||||
@@ -604,16 +603,8 @@ class PostDetailFragment : MainFragment {
|
|||||||
|
|
||||||
private fun fetchPolycentricProfile() {
|
private fun fetchPolycentricProfile() {
|
||||||
val author = _post?.author ?: _postOverview?.author ?: return;
|
val author = _post?.author ?: _postOverview?.author ?: return;
|
||||||
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(author.url, true);
|
|
||||||
if (cachedPolycentricProfile != null) {
|
|
||||||
setPolycentricProfile(cachedPolycentricProfile, animate = false);
|
|
||||||
if (cachedPolycentricProfile.expired) {
|
|
||||||
_taskLoadPolycentricProfile.run(author.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setPolycentricProfile(null, animate = false);
|
setPolycentricProfile(null, animate = false);
|
||||||
_taskLoadPolycentricProfile.run(author.id);
|
_taskLoadPolycentricProfile.run(author.id);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setChannelMeta(value: IPlatformPost?) {
|
private fun setChannelMeta(value: IPlatformPost?) {
|
||||||
@@ -639,17 +630,18 @@ class PostDetailFragment : MainFragment {
|
|||||||
_repliesOverlay.cleanup();
|
_repliesOverlay.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
private fun setPolycentricProfile(polycentricProfile: PolycentricProfile?, animate: Boolean) {
|
||||||
_polycentricProfile = cachedPolycentricProfile;
|
_polycentricProfile = polycentricProfile;
|
||||||
|
|
||||||
if (cachedPolycentricProfile?.profile == null) {
|
val pp = _polycentricProfile;
|
||||||
|
if (pp == null) {
|
||||||
_layoutMonetization.visibility = View.GONE;
|
_layoutMonetization.visibility = View.GONE;
|
||||||
_creatorThumbnail.setHarborAvailable(false, animate, null);
|
_creatorThumbnail.setHarborAvailable(false, animate, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_layoutMonetization.visibility = View.VISIBLE;
|
_layoutMonetization.visibility = View.VISIBLE;
|
||||||
_creatorThumbnail.setHarborAvailable(true, animate, cachedPolycentricProfile.profile.system.toProto());
|
_creatorThumbnail.setHarborAvailable(true, animate, pp.system.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchPost() {
|
private fun fetchPost() {
|
||||||
|
|||||||
+13
-1
@@ -237,7 +237,19 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
BigButtonGroup(c, context.getString(R.string.update),
|
BigButtonGroup(c, context.getString(R.string.update),
|
||||||
BigButton(c, context.getString(R.string.check_for_updates), context.getString(R.string.checks_for_new_versions_of_the_source), R.drawable.ic_update) {
|
BigButton(c, context.getString(R.string.check_for_updates), context.getString(R.string.checks_for_new_versions_of_the_source), R.drawable.ic_update) {
|
||||||
checkForUpdatesSource();
|
checkForUpdatesSource();
|
||||||
}
|
},
|
||||||
|
if(config.changelog?.any() == true)
|
||||||
|
BigButton(c, context.getString(R.string.changelog), context.getString(R.string.changelog_plugin_description), R.drawable.ic_list) {
|
||||||
|
UIDialogs.showChangelogDialog(context, config.version, config.changelog!!.filterKeys { it.toIntOrNull() != null }
|
||||||
|
.mapKeys { it.key.toInt() }
|
||||||
|
.mapValues { config.getChangelogString(it.key.toString()) ?: "" });
|
||||||
|
}.apply {
|
||||||
|
this.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
null
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+2
@@ -256,6 +256,8 @@ class SubscriptionGroupFragment : MainFragment() {
|
|||||||
val sub = StateSubscriptions.instance.getSubscription(sub) ?: StateSubscriptions.instance.getSubscriptionOther(sub);
|
val sub = StateSubscriptions.instance.getSubscription(sub) ?: StateSubscriptions.instance.getSubscriptionOther(sub);
|
||||||
if(sub != null && sub.channel.thumbnail != null) {
|
if(sub != null && sub.channel.thumbnail != null) {
|
||||||
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
|
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
|
||||||
|
if(g.image != null)
|
||||||
|
g.image!!.subscriptionUrl = sub.channel.url;
|
||||||
g.image?.setImageView(_imageGroup);
|
g.image?.setImageView(_imageGroup);
|
||||||
g.image?.setImageView(_imageGroupBackground);
|
g.image?.setImageView(_imageGroupBackground);
|
||||||
break;
|
break;
|
||||||
|
|||||||
+4
-2
@@ -204,8 +204,10 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
val feed = StateSubscriptions.instance.getFeed(group?.id);
|
val feed = StateSubscriptions.instance.getFeed(group?.id);
|
||||||
|
|
||||||
val currentExs = feed?.exceptions ?: listOf();
|
val currentExs = feed?.exceptions ?: listOf();
|
||||||
if(currentExs != _lastExceptions && currentExs.any())
|
if(currentExs != _lastExceptions && currentExs.any()) {
|
||||||
handleExceptions(currentExs);
|
handleExceptions(currentExs)
|
||||||
|
feed?.exceptions = listOf()
|
||||||
|
}
|
||||||
|
|
||||||
return@TaskHandler resp;
|
return@TaskHandler resp;
|
||||||
})
|
})
|
||||||
|
|||||||
+9
-2
@@ -9,6 +9,7 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -122,8 +123,14 @@ class SuggestionsFragment : MainFragment {
|
|||||||
navigate<RemotePlaylistFragment>(it);
|
navigate<RemotePlaylistFragment>(it);
|
||||||
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
||||||
navigate<ChannelFragment>(it);
|
navigate<ChannelFragment>(it);
|
||||||
else
|
else {
|
||||||
navigate<VideoDetailFragment>(it);
|
val url = it;
|
||||||
|
activity?.let {
|
||||||
|
close()
|
||||||
|
if(it is MainActivity)
|
||||||
|
it.navigate(it.getFragment<VideoDetailFragment>(), url);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl));
|
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl));
|
||||||
|
|||||||
+1
@@ -151,6 +151,7 @@ class TutorialFragment : MainFragment() {
|
|||||||
override val rating: IRating = RatingLikes(-1)
|
override val rating: IRating = RatingLikes(-1)
|
||||||
override val viewCount: Long = -1
|
override val viewCount: Long = -1
|
||||||
override val video: IVideoSourceDescriptor = TutorialVideoSourceDescriptor(videoUrl, duration, width, height)
|
override val video: IVideoSourceDescriptor = TutorialVideoSourceDescriptor(videoUrl, duration, width, height)
|
||||||
|
override val isShort: Boolean = false;
|
||||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
|
||||||
return EmptyPager()
|
return EmptyPager()
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-2
@@ -11,12 +11,14 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.OrientationEventListener
|
import android.view.OrientationEventListener
|
||||||
|
import android.view.Surface
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import android.view.WindowInsetsController
|
import android.view.WindowInsetsController
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
|
import androidx.core.view.ViewCompat.getDisplay
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
@@ -37,7 +39,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
//region Fragment
|
//region Fragment
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@@ -205,7 +207,37 @@ class VideoDetailFragment() : MainFragment() {
|
|||||||
} else if (rotationLock) {
|
} else if (rotationLock) {
|
||||||
_portraitOrientationListener?.disableListener()
|
_portraitOrientationListener?.disableListener()
|
||||||
_landscapeOrientationListener?.disableListener()
|
_landscapeOrientationListener?.disableListener()
|
||||||
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
val display = getDisplay(_viewDetail!!)
|
||||||
|
val rotation = display!!.rotation
|
||||||
|
val orientation = resources.configuration.orientation
|
||||||
|
|
||||||
|
a.requestedOrientation = when (orientation) {
|
||||||
|
Configuration.ORIENTATION_PORTRAIT -> {
|
||||||
|
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
|
||||||
|
if (rotation == Surface.ROTATION_0) {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||||
|
if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
|
||||||
|
if (rotation == Surface.ROTATION_90) {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_portraitOrientationListener?.disableListener()
|
_portraitOrientationListener?.disableListener()
|
||||||
_landscapeOrientationListener?.disableListener()
|
_landscapeOrientationListener?.disableListener()
|
||||||
|
|||||||
+183
-93
@@ -94,12 +94,10 @@ import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
|||||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||||
import com.futo.platformplayer.fixHtmlLinks
|
import com.futo.platformplayer.fixHtmlLinks
|
||||||
import com.futo.platformplayer.fixHtmlWhitespace
|
import com.futo.platformplayer.fixHtmlWhitespace
|
||||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
|
||||||
import com.futo.platformplayer.getNowDiffSeconds
|
import com.futo.platformplayer.getNowDiffSeconds
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.AnnouncementType
|
import com.futo.platformplayer.states.AnnouncementType
|
||||||
@@ -134,6 +132,7 @@ import com.futo.platformplayer.views.behavior.TouchInterceptFrameLayout
|
|||||||
import com.futo.platformplayer.views.casting.CastView
|
import com.futo.platformplayer.views.casting.CastView
|
||||||
import com.futo.platformplayer.views.comments.AddCommentView
|
import com.futo.platformplayer.views.comments.AddCommentView
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.overlays.ChaptersOverlay
|
||||||
import com.futo.platformplayer.views.overlays.DescriptionOverlay
|
import com.futo.platformplayer.views.overlays.DescriptionOverlay
|
||||||
import com.futo.platformplayer.views.overlays.LiveChatOverlay
|
import com.futo.platformplayer.views.overlays.LiveChatOverlay
|
||||||
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
import com.futo.platformplayer.views.overlays.QueueEditorOverlay
|
||||||
@@ -149,6 +148,7 @@ import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
|||||||
import com.futo.platformplayer.views.pills.RoundButton
|
import com.futo.platformplayer.views.pills.RoundButton
|
||||||
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
import com.futo.platformplayer.views.pills.RoundButtonGroup
|
||||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
import com.futo.platformplayer.views.segments.ChaptersList
|
||||||
import com.futo.platformplayer.views.segments.CommentsList
|
import com.futo.platformplayer.views.segments.CommentsList
|
||||||
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||||
import com.futo.platformplayer.views.video.FutoVideoPlayer
|
import com.futo.platformplayer.views.video.FutoVideoPlayer
|
||||||
@@ -158,6 +158,8 @@ import com.futo.polycentric.core.ApiMethods
|
|||||||
import com.futo.polycentric.core.ContentType
|
import com.futo.polycentric.core.ContentType
|
||||||
import com.futo.polycentric.core.Models
|
import com.futo.polycentric.core.Models
|
||||||
import com.futo.polycentric.core.Opinion
|
import com.futo.polycentric.core.Opinion
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -171,7 +173,6 @@ import kotlinx.coroutines.withContext
|
|||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@@ -196,6 +197,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private var _liveChat: LiveChatManager? = null;
|
private var _liveChat: LiveChatManager? = null;
|
||||||
private var _videoResumePositionMilliseconds : Long = 0L;
|
private var _videoResumePositionMilliseconds : Long = 0L;
|
||||||
|
|
||||||
|
private var _chapters: List<IChapter>? = null;
|
||||||
|
|
||||||
private val _player: FutoVideoPlayer;
|
private val _player: FutoVideoPlayer;
|
||||||
private val _cast: CastView;
|
private val _cast: CastView;
|
||||||
private val _playerProgress: PlayerControlView;
|
private val _playerProgress: PlayerControlView;
|
||||||
@@ -264,6 +267,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private val _container_content_liveChat: LiveChatOverlay;
|
private val _container_content_liveChat: LiveChatOverlay;
|
||||||
private val _container_content_browser: WebviewOverlay;
|
private val _container_content_browser: WebviewOverlay;
|
||||||
private val _container_content_support: SupportOverlay;
|
private val _container_content_support: SupportOverlay;
|
||||||
|
private val _container_content_chapters: ChaptersOverlay;
|
||||||
|
|
||||||
private var _container_content_current: View;
|
private var _container_content_current: View;
|
||||||
|
|
||||||
@@ -295,7 +299,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
private set;
|
private set;
|
||||||
private var _historicalPosition: Long = 0;
|
private var _historicalPosition: Long = 0;
|
||||||
private var _commentsCount = 0;
|
private var _commentsCount = 0;
|
||||||
private var _polycentricProfile: PolycentricCache.CachedPolycentricProfile? = null;
|
private var _polycentricProfile: PolycentricProfile? = null;
|
||||||
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
|
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
|
||||||
private var _autoplayVideo: IPlatformVideo? = null
|
private var _autoplayVideo: IPlatformVideo? = null
|
||||||
|
|
||||||
@@ -375,6 +379,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
||||||
_container_content_support = findViewById(R.id.videodetail_container_support);
|
_container_content_support = findViewById(R.id.videodetail_container_support);
|
||||||
_container_content_browser = findViewById(R.id.videodetail_container_webview)
|
_container_content_browser = findViewById(R.id.videodetail_container_webview)
|
||||||
|
_container_content_chapters = findViewById(R.id.videodetail_container_chapters);
|
||||||
|
|
||||||
_addCommentView = findViewById(R.id.add_comment_view);
|
_addCommentView = findViewById(R.id.add_comment_view);
|
||||||
_commentsList = findViewById(R.id.comments_list);
|
_commentsList = findViewById(R.id.comments_list);
|
||||||
@@ -399,6 +404,10 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_monetization = findViewById(R.id.monetization);
|
_monetization = findViewById(R.id.monetization);
|
||||||
_player.attachPlayer();
|
_player.attachPlayer();
|
||||||
|
|
||||||
|
_player.onChapterClicked.subscribe {
|
||||||
|
showChaptersUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
_buttonSubscribe.onSubscribed.subscribe {
|
_buttonSubscribe.onSubscribed.subscribe {
|
||||||
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||||
@@ -410,12 +419,12 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_monetization.onSupportTap.subscribe {
|
_monetization.onSupportTap.subscribe {
|
||||||
_container_content_support.setPolycentricProfile(_polycentricProfile?.profile);
|
_container_content_support.setPolycentricProfile(_polycentricProfile);
|
||||||
switchContentView(_container_content_support);
|
switchContentView(_container_content_support);
|
||||||
};
|
};
|
||||||
|
|
||||||
_monetization.onStoreTap.subscribe {
|
_monetization.onStoreTap.subscribe {
|
||||||
_polycentricProfile?.profile?.systemState?.store?.let {
|
_polycentricProfile?.systemState?.store?.let {
|
||||||
try {
|
try {
|
||||||
val uri = Uri.parse(it);
|
val uri = Uri.parse(it);
|
||||||
val intent = Intent(Intent.ACTION_VIEW);
|
val intent = Intent(Intent.ACTION_VIEW);
|
||||||
@@ -580,6 +589,14 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_minimize_title.setOnClickListener { onMaximize.emit(false) };
|
_minimize_title.setOnClickListener { onMaximize.emit(false) };
|
||||||
_minimize_meta.setOnClickListener { onMaximize.emit(false) };
|
_minimize_meta.setOnClickListener { onMaximize.emit(false) };
|
||||||
|
|
||||||
|
_player.onStateChange.subscribe {
|
||||||
|
if (_player.activelyPlaying) {
|
||||||
|
Logger.i(TAG, "Play changed, resetting error counter _didTriggerDatasourceErrorCount = 0 (_player.activelyPlaying: ${_player.activelyPlaying})")
|
||||||
|
_didTriggerDatasourceErrorCount = 0;
|
||||||
|
_didTriggerDatasourceError = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_player.onPlayChanged.subscribe {
|
_player.onPlayChanged.subscribe {
|
||||||
if (StateCasting.instance.activeDevice == null) {
|
if (StateCasting.instance.activeDevice == null) {
|
||||||
handlePlayChanged(it);
|
handlePlayChanged(it);
|
||||||
@@ -679,6 +696,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_replies.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_support.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
_container_content_browser.onClose.subscribe { switchContentView(_container_content_main); };
|
_container_content_browser.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
|
_container_content_chapters.onClose.subscribe { switchContentView(_container_content_main); };
|
||||||
|
|
||||||
|
_container_content_chapters.onClick.subscribe {
|
||||||
|
handleSeek(it.timeStart.toLong() * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
_description_viewMore.setOnClickListener {
|
_description_viewMore.setOnClickListener {
|
||||||
switchContentView(_container_content_description);
|
switchContentView(_container_content_description);
|
||||||
@@ -845,6 +867,22 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_cast.stopAllGestures();
|
_cast.stopAllGestures();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showChaptersUI(){
|
||||||
|
video?.let {
|
||||||
|
try {
|
||||||
|
_chapters?.let {
|
||||||
|
if(it.size == 0)
|
||||||
|
return@let;
|
||||||
|
_container_content_chapters.setChapters(_chapters);
|
||||||
|
switchContentView(_container_content_chapters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun updateMoreButtons() {
|
fun updateMoreButtons() {
|
||||||
val isLimitedVersion = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
|
val isLimitedVersion = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
|
||||||
if (it is JSClient)
|
if (it is JSClient)
|
||||||
@@ -858,6 +896,13 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
_chapters?.let {
|
||||||
|
if(it != null && it.size > 0)
|
||||||
|
RoundButton(context, R.drawable.ic_list, "Chapters", TAG_CHAPTERS) {
|
||||||
|
showChaptersUI();
|
||||||
|
}
|
||||||
|
else null
|
||||||
|
},
|
||||||
if(video?.isLive ?: false)
|
if(video?.isLive ?: false)
|
||||||
RoundButton(context, R.drawable.ic_chat, context.getString(R.string.live_chat), TAG_LIVECHAT) {
|
RoundButton(context, R.drawable.ic_chat, context.getString(R.string.live_chat), TAG_LIVECHAT) {
|
||||||
video?.let {
|
video?.let {
|
||||||
@@ -870,22 +915,20 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
_slideUpOverlay?.hide();
|
_slideUpOverlay?.hide();
|
||||||
} else null,
|
} else null,
|
||||||
if(!isLimitedVersion)
|
if (!isLimitedVersion) RoundButton(context, R.drawable.ic_screen_share, if (allowBackground) context.getString(R.string.background_revert) else context.getString(R.string.background), TAG_BACKGROUND) {
|
||||||
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.background), TAG_BACKGROUND) {
|
if (!allowBackground) {
|
||||||
if(!allowBackground) {
|
_player.switchToAudioMode();
|
||||||
_player.switchToAudioMode();
|
allowBackground = true;
|
||||||
allowBackground = true;
|
it.text.text = resources.getString(R.string.background_revert);
|
||||||
it.text.text = resources.getString(R.string.background_revert);
|
} else {
|
||||||
}
|
_player.switchToVideoMode();
|
||||||
else {
|
allowBackground = false;
|
||||||
_player.switchToVideoMode();
|
it.text.text = resources.getString(R.string.background);
|
||||||
allowBackground = false;
|
|
||||||
it.text.text = resources.getString(R.string.background);
|
|
||||||
}
|
|
||||||
_slideUpOverlay?.hide();
|
|
||||||
}
|
}
|
||||||
|
_slideUpOverlay?.hide();
|
||||||
|
}
|
||||||
else null,
|
else null,
|
||||||
if(!isLimitedVersion)
|
if(!isLimitedVersion && !(video?.isLive ?: false))
|
||||||
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
||||||
video?.let {
|
video?.let {
|
||||||
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
||||||
@@ -925,7 +968,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
} else if(devices.size == 1){
|
} else if(devices.size == 1){
|
||||||
val device = devices.first();
|
val device = devices.first();
|
||||||
Logger.i(TAG, "Send to device? (public key: ${device.remotePublicKey}): " + videoToSend.url)
|
Logger.i(TAG, "Send to device? (public key: ${device.remotePublicKey}): " + videoToSend.url)
|
||||||
UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non ${device.remotePublicKey}" , {
|
UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non '${device.displayName}'" , {
|
||||||
Logger.i(TAG, "Send to device confirmed (public key: ${device.remotePublicKey}): " + videoToSend.url)
|
Logger.i(TAG, "Send to device confirmed (public key: ${device.remotePublicKey}): " + videoToSend.url)
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
@@ -966,6 +1009,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
throw IllegalStateException("Expected media content, found ${video.contentType}");
|
throw IllegalStateException("Expected media content, found ${video.contentType}");
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
_videoResumePositionMilliseconds = _player.position
|
||||||
setVideoDetails(video);
|
setVideoDetails(video);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1230,16 +1274,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
||||||
_channelName.text = video.author.name;
|
_channelName.text = video.author.name;
|
||||||
|
|
||||||
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url, true);
|
setPolycentricProfile(null, animate = false);
|
||||||
if (cachedPolycentricProfile != null) {
|
_taskLoadPolycentricProfile.run(video.author.id);
|
||||||
setPolycentricProfile(cachedPolycentricProfile, animate = false);
|
|
||||||
if (cachedPolycentricProfile.expired) {
|
|
||||||
_taskLoadPolycentricProfile.run(video.author.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setPolycentricProfile(null, animate = false);
|
|
||||||
_taskLoadPolycentricProfile.run(video.author.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
_player.clear();
|
_player.clear();
|
||||||
|
|
||||||
@@ -1268,8 +1304,6 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
||||||
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
||||||
_didTriggerDatasourceErrroCount = 0;
|
|
||||||
_didTriggerDatasourceError = false;
|
|
||||||
_autoplayVideo = null
|
_autoplayVideo = null
|
||||||
Logger.i(TAG, "Autoplay video cleared (setVideoDetails)")
|
Logger.i(TAG, "Autoplay video cleared (setVideoDetails)")
|
||||||
|
|
||||||
@@ -1280,6 +1314,10 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_lastVideoSource = null;
|
_lastVideoSource = null;
|
||||||
_lastAudioSource = null;
|
_lastAudioSource = null;
|
||||||
_lastSubtitleSource = null;
|
_lastSubtitleSource = null;
|
||||||
|
|
||||||
|
Logger.i(TAG, "_didTriggerDatasourceErrorCount reset to 0 because new video")
|
||||||
|
_didTriggerDatasourceErrorCount = 0;
|
||||||
|
_didTriggerDatasourceError = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now())
|
if (videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now())
|
||||||
@@ -1340,10 +1378,12 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
val chapters = null ?: StatePlatform.instance.getContentChapters(video.url);
|
val chapters = null ?: StatePlatform.instance.getContentChapters(video.url);
|
||||||
_player.setChapters(chapters);
|
_player.setChapters(chapters);
|
||||||
_cast.setChapters(chapters);
|
_cast.setChapters(chapters);
|
||||||
|
_chapters = _player.getChapters();
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
Logger.e(TAG, "Failed to get chapters", ex);
|
Logger.e(TAG, "Failed to get chapters", ex);
|
||||||
_player.setChapters(null);
|
_player.setChapters(null);
|
||||||
_cast.setChapters(null);
|
_cast.setChapters(null);
|
||||||
|
_chapters = null;
|
||||||
|
|
||||||
/*withContext(Dispatchers.Main) {
|
/*withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(context, "Failed to get chapters\n" + ex.message);
|
UIDialogs.toast(context, "Failed to get chapters\n" + ex.message);
|
||||||
@@ -1382,6 +1422,10 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
updateMoreButtons();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1397,11 +1441,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
setTabIndex(2, true)
|
setTabIndex(2, true)
|
||||||
} else {
|
} else {
|
||||||
when (Settings.instance.comments.defaultCommentSection) {
|
when (Settings.instance.comments.defaultCommentSection) {
|
||||||
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(
|
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(0, true) else setTabIndex(1, true)
|
||||||
0,
|
1 -> setTabIndex(1, true)
|
||||||
true
|
|
||||||
) else setTabIndex(1, true);
|
|
||||||
1 -> setTabIndex(1, true);
|
|
||||||
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
|
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1439,16 +1480,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_buttonSubscribe.setSubscribeChannel(video.author.url);
|
_buttonSubscribe.setSubscribeChannel(video.author.url);
|
||||||
setDescription(video.description.fixHtmlLinks());
|
setDescription(video.description.fixHtmlLinks());
|
||||||
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
||||||
|
setPolycentricProfile(null, animate = false);
|
||||||
|
_taskLoadPolycentricProfile.run(video.author.id);
|
||||||
val cachedPolycentricProfile =
|
|
||||||
PolycentricCache.instance.getCachedProfile(video.author.url, true);
|
|
||||||
if (cachedPolycentricProfile != null) {
|
|
||||||
setPolycentricProfile(cachedPolycentricProfile, animate = false);
|
|
||||||
} else {
|
|
||||||
setPolycentricProfile(null, animate = false);
|
|
||||||
_taskLoadPolycentricProfile.run(video.author.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
_platform.setPlatformFromClientID(video.id.pluginId);
|
_platform.setPlatformFromClientID(video.id.pluginId);
|
||||||
val subTitleSegments: ArrayList<String> = ArrayList();
|
val subTitleSegments: ArrayList<String> = ArrayList();
|
||||||
@@ -1477,7 +1510,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val queryReferencesResponse = ApiMethods.getQueryReferences(
|
val queryReferencesResponse = ApiMethods.getQueryReferences(
|
||||||
PolycentricCache.SERVER, ref, null, null,
|
ApiMethods.SERVER, ref, null, null,
|
||||||
arrayListOf(
|
arrayListOf(
|
||||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
||||||
.setFromType(ContentType.OPINION.value).setValue(
|
.setFromType(ContentType.OPINION.value).setValue(
|
||||||
@@ -1493,10 +1526,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
val likes = queryReferencesResponse.countsList[0];
|
val likes = queryReferencesResponse.countsList[0];
|
||||||
val dislikes = queryReferencesResponse.countsList[1];
|
val dislikes = queryReferencesResponse.countsList[1];
|
||||||
val hasLiked =
|
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
|
||||||
StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
|
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
|
||||||
val hasDisliked =
|
|
||||||
StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
_rating.visibility = View.VISIBLE;
|
_rating.visibility = View.VISIBLE;
|
||||||
@@ -1834,7 +1865,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var _didTriggerDatasourceErrroCount = 0;
|
private var _didTriggerDatasourceErrorCount = 0;
|
||||||
private var _didTriggerDatasourceError = false;
|
private var _didTriggerDatasourceError = false;
|
||||||
private fun onDataSourceError(exception: Throwable) {
|
private fun onDataSourceError(exception: Throwable) {
|
||||||
Logger.e(TAG, "onDataSourceError", exception);
|
Logger.e(TAG, "onDataSourceError", exception);
|
||||||
@@ -1844,32 +1875,53 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
return;
|
return;
|
||||||
val config = currentVideo.sourceConfig;
|
val config = currentVideo.sourceConfig;
|
||||||
|
|
||||||
if(_didTriggerDatasourceErrroCount <= 3) {
|
if(_didTriggerDatasourceErrorCount <= 3) {
|
||||||
_didTriggerDatasourceError = true;
|
_didTriggerDatasourceError = true;
|
||||||
_didTriggerDatasourceErrroCount++;
|
_didTriggerDatasourceErrorCount++;
|
||||||
|
|
||||||
|
UIDialogs.toast("Detected video error, attempting automatic reload (${_didTriggerDatasourceErrorCount})");
|
||||||
|
Logger.i(TAG, "Block detected, attempting bypass (_didTriggerDatasourceErrorCount = ${_didTriggerDatasourceErrorCount})");
|
||||||
|
|
||||||
UIDialogs.toast("Block detected, attempting bypass");
|
|
||||||
//return;
|
//return;
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await();
|
try {
|
||||||
val previousVideoSource = _lastVideoSource;
|
val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await();
|
||||||
val previousAudioSource = _lastAudioSource;
|
val previousVideoSource = _lastVideoSource;
|
||||||
|
val previousAudioSource = _lastAudioSource;
|
||||||
|
|
||||||
if(newDetails is IPlatformVideoDetails) {
|
if (newDetails is IPlatformVideoDetails) {
|
||||||
val newVideoSource = if(previousVideoSource != null)
|
val newVideoSource = if (previousVideoSource != null)
|
||||||
VideoHelper.selectBestVideoSource(newDetails.video, previousVideoSource.height * previousVideoSource.width, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS);
|
VideoHelper.selectBestVideoSource(
|
||||||
else null;
|
newDetails.video,
|
||||||
val newAudioSource = if(previousAudioSource != null)
|
previousVideoSource.height * previousVideoSource.width,
|
||||||
VideoHelper.selectBestAudioSource(newDetails.video, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS, previousAudioSource.language, previousAudioSource.bitrate.toLong());
|
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
||||||
else null;
|
);
|
||||||
withContext(Dispatchers.Main) {
|
else null;
|
||||||
video = newDetails;
|
val newAudioSource = if (previousAudioSource != null)
|
||||||
_player.setSource(newVideoSource, newAudioSource, true, true);
|
VideoHelper.selectBestAudioSource(
|
||||||
|
newDetails.video,
|
||||||
|
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
||||||
|
previousAudioSource.language,
|
||||||
|
previousAudioSource.bitrate.toLong()
|
||||||
|
);
|
||||||
|
else null;
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
video = newDetails;
|
||||||
|
_player.setSource(newVideoSource, newAudioSource, true, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to get video details, attempting retrying without reloading.", e)
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
video?.let {
|
||||||
|
_videoResumePositionMilliseconds = _player.position
|
||||||
|
setVideoDetails(it, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(_didTriggerDatasourceErrroCount > 3) {
|
else if(_didTriggerDatasourceErrorCount > 3) {
|
||||||
UIDialogs.showDialog(context, R.drawable.ic_error_pred,
|
UIDialogs.showDialog(context, R.drawable.ic_error_pred,
|
||||||
context.getString(R.string.media_error),
|
context.getString(R.string.media_error),
|
||||||
context.getString(R.string.the_media_source_encountered_an_unauthorized_error_this_might_be_solved_by_a_plugin_reload_would_you_like_to_reload_experimental),
|
context.getString(R.string.the_media_source_encountered_an_unauthorized_error_this_might_be_solved_by_a_plugin_reload_would_you_like_to_reload_experimental),
|
||||||
@@ -1901,13 +1953,45 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
return super.onInterceptTouchEvent(ev);
|
return super.onInterceptTouchEvent(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//Actions
|
//Actions
|
||||||
private fun showVideoSettings() {
|
private fun showVideoSettings() {
|
||||||
Logger.i(TAG, "showVideoSettings")
|
Logger.i(TAG, "showVideoSettings")
|
||||||
_overlay_quality_selector?.selectOption("video", _lastVideoSource);
|
_overlay_quality_selector?.selectOption("video", _lastVideoSource);
|
||||||
_overlay_quality_selector?.selectOption("audio", _lastAudioSource);
|
_overlay_quality_selector?.selectOption("audio", _lastAudioSource);
|
||||||
_overlay_quality_selector?.selectOption("subtitles", _lastSubtitleSource);
|
_overlay_quality_selector?.selectOption("subtitles", _lastSubtitleSource);
|
||||||
|
|
||||||
|
if (_lastVideoSource is IDashManifestSource || _lastVideoSource is IHLSManifestSource) {
|
||||||
|
|
||||||
|
val videoTracks =
|
||||||
|
_player.exoPlayer?.player?.currentTracks?.groups?.firstOrNull { it.mediaTrackGroup.type == C.TRACK_TYPE_VIDEO }
|
||||||
|
|
||||||
|
var selectedQuality: Format? = null
|
||||||
|
|
||||||
|
if (videoTracks != null) {
|
||||||
|
for (i in 0 until videoTracks.mediaTrackGroup.length) {
|
||||||
|
if (videoTracks.mediaTrackGroup.getFormat(i).height == _player.targetTrackVideoHeight) {
|
||||||
|
selectedQuality = videoTracks.mediaTrackGroup.getFormat(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var videoMenuGroup: SlideUpMenuGroup? = null
|
||||||
|
for (view in _overlay_quality_selector!!.groupItems) {
|
||||||
|
if (view is SlideUpMenuGroup && view.groupTag == "video") {
|
||||||
|
videoMenuGroup = view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedQuality != null) {
|
||||||
|
videoMenuGroup?.getItem("auto")?.setSubText("")
|
||||||
|
_overlay_quality_selector?.selectOption("video", selectedQuality)
|
||||||
|
} else {
|
||||||
|
videoMenuGroup?.getItem("auto")
|
||||||
|
?.setSubText("${_player.exoPlayer?.player?.videoFormat?.width}x${_player.exoPlayer?.player?.videoFormat?.height}")
|
||||||
|
_overlay_quality_selector?.selectOption("video", "auto")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val currentPlaybackRate = (if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()) ?: 1.0
|
val currentPlaybackRate = (if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()) ?: 1.0
|
||||||
_overlay_quality_selector?.groupItems?.firstOrNull { it is SlideUpMenuButtonList && it.id == "playback_rate" }?.let {
|
_overlay_quality_selector?.groupItems?.firstOrNull { it is SlideUpMenuButtonList && it.id == "playback_rate" }?.let {
|
||||||
(it as SlideUpMenuButtonList).setSelected(currentPlaybackRate.toString())
|
(it as SlideUpMenuButtonList).setSelected(currentPlaybackRate.toString())
|
||||||
@@ -2081,17 +2165,15 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
call = { handleSelectSubtitleTrack(it) })
|
call = { handleSelectSubtitleTrack(it) })
|
||||||
}.toList().toTypedArray())
|
}.toList().toTypedArray())
|
||||||
else null,
|
else null,
|
||||||
if(liveStreamVideoFormats?.isEmpty() == false)
|
if (liveStreamVideoFormats?.isEmpty() == false) SlideUpMenuGroup(
|
||||||
SlideUpMenuGroup(this.context, context.getString(R.string.stream_video), "video",
|
this.context, context.getString(R.string.stream_video), "video", (listOf(
|
||||||
*liveStreamVideoFormats
|
SlideUpMenuItem(this.context, R.drawable.ic_movie, "Auto", tag = "auto", call = { _player.selectVideoTrack(-1) })
|
||||||
.map {
|
) + (liveStreamVideoFormats.map {
|
||||||
SlideUpMenuItem(this.context,
|
SlideUpMenuItem(this.context, R.drawable.ic_movie, it.label
|
||||||
R.drawable.ic_movie,
|
?: it.containerMimeType
|
||||||
it.label ?: it.containerMimeType ?: it.bitrate.toString(),
|
?: it.bitrate.toString(), "${it.width}x${it.height}", tag = it, call = { _player.selectVideoTrack(it.height) });
|
||||||
"${it.width}x${it.height}",
|
}))
|
||||||
tag = it,
|
)
|
||||||
call = { _player.selectVideoTrack(it.height) });
|
|
||||||
}.toList().toTypedArray())
|
|
||||||
else null,
|
else null,
|
||||||
if(liveStreamAudioFormats?.isEmpty() == false)
|
if(liveStreamAudioFormats?.isEmpty() == false)
|
||||||
SlideUpMenuGroup(this.context, context.getString(R.string.stream_audio), "audio",
|
SlideUpMenuGroup(this.context, context.getString(R.string.stream_audio), "audio",
|
||||||
@@ -2563,13 +2645,21 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onChannelClicked.subscribe {
|
onChannelClicked.subscribe {
|
||||||
fragment.navigate<ChannelFragment>(it)
|
if(it.url.isNotBlank())
|
||||||
|
fragment.navigate<ChannelFragment>(it)
|
||||||
|
else
|
||||||
|
UIDialogs.appToast("No author url present");
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddToWatchLaterClicked.subscribe(this) {
|
onAddToWatchLaterClicked.subscribe(this) {
|
||||||
if(it is IPlatformVideo) {
|
if(it is IPlatformVideo) {
|
||||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
|
||||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onAddToQueueClicked.subscribe(this) {
|
||||||
|
if(it is IPlatformVideo) {
|
||||||
|
StatePlayer.instance.addToQueue(it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -2741,13 +2831,12 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
|
||||||
_polycentricProfile = cachedPolycentricProfile;
|
_polycentricProfile = profile
|
||||||
|
|
||||||
val dp_35 = 35.dp(context.resources)
|
val dp_35 = 35.dp(context.resources)
|
||||||
val profile = cachedPolycentricProfile?.profile;
|
|
||||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
|
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
|
||||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }
|
||||||
|
|
||||||
if (avatar != null) {
|
if (avatar != null) {
|
||||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||||
@@ -2756,12 +2845,12 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||||
}
|
}
|
||||||
|
|
||||||
val username = cachedPolycentricProfile?.profile?.systemState?.username
|
val username = profile?.systemState?.username
|
||||||
if (username != null) {
|
if (username != null) {
|
||||||
_channelName.text = username
|
_channelName.text = username
|
||||||
}
|
}
|
||||||
|
|
||||||
_monetization.setPolycentricProfile(cachedPolycentricProfile);
|
_monetization.setPolycentricProfile(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
|
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
|
||||||
@@ -2949,7 +3038,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
Logger.w(TAG, "Failed to load recommendations.", it);
|
Logger.w(TAG, "Failed to load recommendations.", it);
|
||||||
};
|
};
|
||||||
|
|
||||||
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
|
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) })
|
||||||
.success { it -> setPolycentricProfile(it, animate = true) }
|
.success { it -> setPolycentricProfile(it, animate = true) }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load claims.", it);
|
Logger.w(TAG, "Failed to load claims.", it);
|
||||||
@@ -3035,6 +3124,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
const val TAG_SHARE = "share";
|
const val TAG_SHARE = "share";
|
||||||
const val TAG_OVERLAY = "overlay";
|
const val TAG_OVERLAY = "overlay";
|
||||||
const val TAG_LIVECHAT = "livechat";
|
const val TAG_LIVECHAT = "livechat";
|
||||||
|
const val TAG_CHAPTERS = "chapters";
|
||||||
const val TAG_OPEN = "open";
|
const val TAG_OPEN = "open";
|
||||||
const val TAG_SEND_TO_DEVICE = "send_to_device";
|
const val TAG_SEND_TO_DEVICE = "send_to_device";
|
||||||
const val TAG_MORE = "MORE";
|
const val TAG_MORE = "MORE";
|
||||||
|
|||||||
+62
-3
@@ -9,6 +9,7 @@ import android.widget.ImageButton
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.setPadding
|
import androidx.core.view.setPadding
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
@@ -20,6 +21,9 @@ import com.futo.platformplayer.downloads.VideoDownload
|
|||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
|
import com.futo.platformplayer.toHumanDuration
|
||||||
|
import com.futo.platformplayer.toHumanTime
|
||||||
|
import com.futo.platformplayer.views.SearchView
|
||||||
import com.futo.platformplayer.views.lists.VideoListEditorView
|
import com.futo.platformplayer.views.lists.VideoListEditorView
|
||||||
|
|
||||||
abstract class VideoListEditorView : LinearLayout {
|
abstract class VideoListEditorView : LinearLayout {
|
||||||
@@ -32,11 +36,18 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
protected var overlayContainer: FrameLayout
|
protected var overlayContainer: FrameLayout
|
||||||
private set;
|
private set;
|
||||||
protected var _buttonDownload: ImageButton;
|
protected var _buttonDownload: ImageButton;
|
||||||
|
protected var _buttonExport: ImageButton;
|
||||||
private var _buttonShare: ImageButton;
|
private var _buttonShare: ImageButton;
|
||||||
private var _buttonEdit: ImageButton;
|
private var _buttonEdit: ImageButton;
|
||||||
|
private var _buttonSearch: ImageButton;
|
||||||
|
|
||||||
|
private var _search: SearchView;
|
||||||
|
|
||||||
private var _onShare: (()->Unit)? = null;
|
private var _onShare: (()->Unit)? = null;
|
||||||
|
|
||||||
|
private var _loadedVideos: List<IPlatformVideo>? = null;
|
||||||
|
private var _loadedVideosCanEdit: Boolean = false;
|
||||||
|
|
||||||
constructor(inflater: LayoutInflater) : super(inflater.context) {
|
constructor(inflater: LayoutInflater) : super(inflater.context) {
|
||||||
inflater.inflate(R.layout.fragment_video_list_editor, this);
|
inflater.inflate(R.layout.fragment_video_list_editor, this);
|
||||||
|
|
||||||
@@ -52,6 +63,28 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
_buttonEdit = findViewById(R.id.button_edit);
|
_buttonEdit = findViewById(R.id.button_edit);
|
||||||
_buttonDownload = findViewById(R.id.button_download);
|
_buttonDownload = findViewById(R.id.button_download);
|
||||||
_buttonDownload.visibility = View.GONE;
|
_buttonDownload.visibility = View.GONE;
|
||||||
|
_buttonExport = findViewById(R.id.button_export);
|
||||||
|
_buttonExport.visibility = View.GONE;
|
||||||
|
_buttonSearch = findViewById(R.id.button_search);
|
||||||
|
|
||||||
|
_search = findViewById(R.id.search_bar);
|
||||||
|
_search.visibility = View.GONE;
|
||||||
|
_search.onSearchChanged.subscribe {
|
||||||
|
updateVideoFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
_buttonSearch.setOnClickListener {
|
||||||
|
if(_search.isVisible) {
|
||||||
|
_search.visibility = View.GONE;
|
||||||
|
_search.textSearch.text = "";
|
||||||
|
updateVideoFilters();
|
||||||
|
_buttonSearch.setImageResource(R.drawable.ic_search);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_search.visibility = View.VISIBLE;
|
||||||
|
_buttonSearch.setImageResource(R.drawable.ic_search_off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_buttonShare = findViewById(R.id.button_share);
|
_buttonShare = findViewById(R.id.button_share);
|
||||||
val onShare = _onShare;
|
val onShare = _onShare;
|
||||||
@@ -66,6 +99,7 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
buttonShuffle.setOnClickListener { onShuffleClick(); };
|
buttonShuffle.setOnClickListener { onShuffleClick(); };
|
||||||
|
|
||||||
_buttonEdit.setOnClickListener { onEditClick(); };
|
_buttonEdit.setOnClickListener { onEditClick(); };
|
||||||
|
setButtonExportVisible(false);
|
||||||
setButtonDownloadVisible(canEdit());
|
setButtonDownloadVisible(canEdit());
|
||||||
|
|
||||||
videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged);
|
videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged);
|
||||||
@@ -106,6 +140,7 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
|
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
|
||||||
|
|
||||||
if(isDownloading) {
|
if(isDownloading) {
|
||||||
|
setButtonExportVisible(false);
|
||||||
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
||||||
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
||||||
_buttonDownload.setOnClickListener {
|
_buttonDownload.setOnClickListener {
|
||||||
@@ -115,6 +150,7 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(isDownloaded) {
|
else if(isDownloaded) {
|
||||||
|
setButtonExportVisible(true)
|
||||||
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
||||||
_buttonDownload.setOnClickListener {
|
_buttonDownload.setOnClickListener {
|
||||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||||
@@ -123,6 +159,7 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
setButtonExportVisible(false);
|
||||||
_buttonDownload.setImageResource(R.drawable.ic_download);
|
_buttonDownload.setImageResource(R.drawable.ic_download);
|
||||||
_buttonDownload.setOnClickListener {
|
_buttonDownload.setOnClickListener {
|
||||||
onDownload();
|
onDownload();
|
||||||
@@ -136,8 +173,14 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
_textName.text = name ?: "";
|
_textName.text = name ?: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun setVideoCount(videoCount: Int = -1) {
|
protected fun setMetadata(videoCount: Int = -1, duration: Long = -1) {
|
||||||
_textMetadata.text = if (videoCount == -1) "" else "${videoCount} " + context.getString(R.string.videos);
|
val parts = mutableListOf<String>()
|
||||||
|
if(videoCount >= 0)
|
||||||
|
parts.add("${videoCount} " + context.getString(R.string.videos));
|
||||||
|
if(duration > 0)
|
||||||
|
parts.add("${duration.toHumanDuration(false)} ");
|
||||||
|
|
||||||
|
_textMetadata.text = parts.joinToString(" • ");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun setVideos(videos: List<IPlatformVideo>?, canEdit: Boolean) {
|
protected fun setVideos(videos: List<IPlatformVideo>?, canEdit: Boolean) {
|
||||||
@@ -156,13 +199,29 @@ abstract class VideoListEditorView : LinearLayout {
|
|||||||
.load(R.drawable.placeholder_video_thumbnail)
|
.load(R.drawable.placeholder_video_thumbnail)
|
||||||
.into(_imagePlaylistThumbnail)
|
.into(_imagePlaylistThumbnail)
|
||||||
}
|
}
|
||||||
|
_loadedVideos = videos;
|
||||||
|
_loadedVideosCanEdit = canEdit;
|
||||||
_videoListEditorView.setVideos(videos, canEdit);
|
_videoListEditorView.setVideos(videos, canEdit);
|
||||||
}
|
}
|
||||||
|
fun filterVideos(videos: List<IPlatformVideo>): List<IPlatformVideo> {
|
||||||
|
var toReturn = videos;
|
||||||
|
val searchStr = _search.textSearch.text
|
||||||
|
if(!searchStr.isNullOrBlank())
|
||||||
|
toReturn = toReturn.filter { it.name.contains(searchStr, true) || it.author.name.contains(searchStr, true) };
|
||||||
|
return toReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateVideoFilters() {
|
||||||
|
val videos = _loadedVideos ?: return;
|
||||||
|
_videoListEditorView.setVideos(filterVideos(videos), _loadedVideosCanEdit);
|
||||||
|
}
|
||||||
|
|
||||||
protected fun setButtonDownloadVisible(isVisible: Boolean) {
|
protected fun setButtonDownloadVisible(isVisible: Boolean) {
|
||||||
_buttonDownload.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
_buttonDownload.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
||||||
}
|
}
|
||||||
|
protected fun setButtonExportVisible(isVisible: Boolean) {
|
||||||
|
_buttonExport.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
||||||
|
}
|
||||||
|
|
||||||
protected fun setButtonEditVisible(isVisible: Boolean) {
|
protected fun setButtonEditVisible(isVisible: Boolean) {
|
||||||
_buttonEdit.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
_buttonEdit.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
||||||
|
|||||||
+1
-1
@@ -14,9 +14,9 @@ import com.futo.platformplayer.R
|
|||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.models.Playlist
|
import com.futo.platformplayer.models.Playlist
|
||||||
import com.futo.platformplayer.views.casting.CastButton
|
import com.futo.platformplayer.views.casting.CastButton
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
|
||||||
class NavigationTopBarFragment : TopFragment() {
|
class NavigationTopBarFragment : TopFragment() {
|
||||||
private var _buttonBack: ImageButton? = null;
|
private var _buttonBack: ImageButton? = null;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.media3.datasource.ResolvingDataSource
|
|||||||
import androidx.media3.exoplayer.dash.DashMediaSource
|
import androidx.media3.exoplayer.dash.DashMediaSource
|
||||||
import androidx.media3.exoplayer.dash.manifest.DashManifestParser
|
import androidx.media3.exoplayer.dash.manifest.DashManifestParser
|
||||||
import androidx.media3.exoplayer.source.MediaSource
|
import androidx.media3.exoplayer.source.MediaSource
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
@@ -85,12 +86,17 @@ class VideoHelper {
|
|||||||
|
|
||||||
return selectBestAudioSource((desc as VideoUnMuxedSourceDescriptor).audioSources.toList(), prefContainers, prefLanguage, targetBitrate);
|
return selectBestAudioSource((desc as VideoUnMuxedSourceDescriptor).audioSources.toList(), prefContainers, prefLanguage, targetBitrate);
|
||||||
}
|
}
|
||||||
fun selectBestAudioSource(altSources : Iterable<IAudioSource>, prefContainers : Array<String>, preferredLanguage: String? = null, targetBitrate: Long? = null) : IAudioSource? {
|
fun selectBestAudioSource(sources : Iterable<IAudioSource>, prefContainers : Array<String>, preferredLanguage: String? = null, targetBitrate: Long? = null) : IAudioSource? {
|
||||||
|
val hasPriority = sources.any { it.priority };
|
||||||
|
var altSources = if(hasPriority) sources.filter { it.priority } else sources;
|
||||||
|
val hasOriginal = altSources.any { it.original };
|
||||||
|
if(hasOriginal && Settings.instance.playback.preferOriginalAudio)
|
||||||
|
altSources = altSources.filter { it.original };
|
||||||
val languageToFilter = if(preferredLanguage != null && altSources.any { it.language == preferredLanguage }) {
|
val languageToFilter = if(preferredLanguage != null && altSources.any { it.language == preferredLanguage }) {
|
||||||
preferredLanguage
|
preferredLanguage
|
||||||
} else {
|
} else {
|
||||||
if(altSources.any { it.language == Language.ENGLISH })
|
if(altSources.any { it.language == Language.ENGLISH })
|
||||||
Language.ENGLISH
|
Language.ENGLISH;
|
||||||
else
|
else
|
||||||
Language.UNKNOWN;
|
Language.UNKNOWN;
|
||||||
}
|
}
|
||||||
@@ -208,5 +214,38 @@ class VideoHelper {
|
|||||||
}
|
}
|
||||||
else return 0;
|
else return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun mediaExtensionToMimetype(extension: String): String? {
|
||||||
|
return videoExtensionToMimetype(extension) ?: audioExtensionToMimetype(extension);
|
||||||
|
}
|
||||||
|
fun videoExtensionToMimetype(extension: String): String? {
|
||||||
|
val extensionTrimmed = extension.trim('.').lowercase();
|
||||||
|
return when (extensionTrimmed) {
|
||||||
|
"mp4" -> return "video/mp4";
|
||||||
|
"webm" -> return "video/webm";
|
||||||
|
"m3u8" -> return "video/x-mpegURL";
|
||||||
|
"3gp" -> return "video/3gpp";
|
||||||
|
"mov" -> return "video/quicktime";
|
||||||
|
"mkv" -> return "video/x-matroska";
|
||||||
|
"mp4a" -> return "audio/vnd.apple.mpegurl";
|
||||||
|
"mpga" -> return "audio/mpga";
|
||||||
|
"mp3" -> return "audio/mp3";
|
||||||
|
"webm" -> return "audio/webm";
|
||||||
|
"3gp" -> return "audio/3gpp";
|
||||||
|
else -> null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun audioExtensionToMimetype(extension: String): String? {
|
||||||
|
val extensionTrimmed = extension.trim('.').lowercase();
|
||||||
|
return when (extensionTrimmed) {
|
||||||
|
"mkv" -> return "audio/x-matroska";
|
||||||
|
"mp4a" -> return "audio/vnd.apple.mpegurl";
|
||||||
|
"mpga" -> return "audio/mpga";
|
||||||
|
"mp3" -> return "audio/mp3";
|
||||||
|
"webm" -> return "audio/webm";
|
||||||
|
"3gp" -> return "audio/3gpp";
|
||||||
|
else -> null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.futo.platformplayer.images;
|
package com.futo.platformplayer.images;
|
||||||
|
|
||||||
|
import static com.futo.platformplayer.Extensions_PolycentricKt.getDataLinkFromUrl;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -12,10 +14,14 @@ import com.bumptech.glide.load.model.ModelLoader;
|
|||||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||||
import com.bumptech.glide.signature.ObjectKey;
|
import com.bumptech.glide.signature.ObjectKey;
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache;
|
import com.futo.polycentric.core.ApiMethods;
|
||||||
|
|
||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
|
import kotlinx.coroutines.CoroutineScopeKt;
|
||||||
import kotlinx.coroutines.Deferred;
|
import kotlinx.coroutines.Deferred;
|
||||||
|
import kotlinx.coroutines.Dispatchers;
|
||||||
|
import userpackage.Protocol;
|
||||||
|
|
||||||
import java.lang.Exception;
|
import java.lang.Exception;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
@@ -60,7 +66,14 @@ public class PolycentricModelLoader implements ModelLoader<String, ByteBuffer> {
|
|||||||
@Override
|
@Override
|
||||||
public void loadData(@NonNull Priority priority, @NonNull DataFetcher.DataCallback<? super ByteBuffer> callback) {
|
public void loadData(@NonNull Priority priority, @NonNull DataFetcher.DataCallback<? super ByteBuffer> callback) {
|
||||||
Log.i("PolycentricModelLoader", this._model);
|
Log.i("PolycentricModelLoader", this._model);
|
||||||
_deferred = PolycentricCache.getInstance().getDataAsync(_model);
|
|
||||||
|
Protocol.URLInfoDataLink dataLink = getDataLinkFromUrl(_model);
|
||||||
|
if (dataLink == null) {
|
||||||
|
callback.onLoadFailed(new Exception("Data link cannot be null"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_deferred = ApiMethods.Companion.getDataFromServerAndReassemble(CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()), dataLink);
|
||||||
_deferred.invokeOnCompletion(throwable -> {
|
_deferred.invokeOnCompletion(throwable -> {
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
Log.e("PolycentricModelLoader", "getDataAsync failed throwable: " + throwable.toString());
|
Log.e("PolycentricModelLoader", "getDataAsync failed throwable: " + throwable.toString());
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.futo.platformplayer.models
|
|||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@@ -46,6 +47,7 @@ class HistoryVideo {
|
|||||||
val name = str.substring(indexNext + 3);
|
val name = str.substring(indexNext + 3);
|
||||||
|
|
||||||
val video = resolve?.invoke(url) ?: SerializedPlatformVideo(
|
val video = resolve?.invoke(url) ?: SerializedPlatformVideo(
|
||||||
|
ContentType.MEDIA,
|
||||||
id = PlatformID.asUrlID(url),
|
id = PlatformID.asUrlID(url),
|
||||||
name = name,
|
name = name,
|
||||||
thumbnails = Thumbnails(),
|
thumbnails = Thumbnails(),
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import android.widget.ImageView
|
|||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.futo.platformplayer.PresetImages
|
import com.futo.platformplayer.PresetImages
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -18,7 +20,8 @@ data class ImageVariable(
|
|||||||
@Transient
|
@Transient
|
||||||
@Contextual
|
@Contextual
|
||||||
private val bitmap: Bitmap? = null,
|
private val bitmap: Bitmap? = null,
|
||||||
val presetName: String? = null) {
|
val presetName: String? = null,
|
||||||
|
var subscriptionUrl: String? = null) {
|
||||||
|
|
||||||
@SuppressLint("DiscouragedApi")
|
@SuppressLint("DiscouragedApi")
|
||||||
fun setImageView(imageView: ImageView, fallbackResId: Int = -1) {
|
fun setImageView(imageView: ImageView, fallbackResId: Int = -1) {
|
||||||
@@ -33,6 +36,12 @@ data class ImageVariable(
|
|||||||
} else if(!url.isNullOrEmpty()) {
|
} else if(!url.isNullOrEmpty()) {
|
||||||
Glide.with(imageView)
|
Glide.with(imageView)
|
||||||
.load(url)
|
.load(url)
|
||||||
|
.error(if(!subscriptionUrl.isNullOrBlank()) StateSubscriptions.instance.getSubscription(subscriptionUrl!!)?.channel?.thumbnail else null)
|
||||||
|
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||||
|
.into(imageView);
|
||||||
|
} else if(!subscriptionUrl.isNullOrEmpty()) {
|
||||||
|
Glide.with(imageView)
|
||||||
|
.load(StateSubscriptions.instance.getSubscription(subscriptionUrl!!)?.channel?.thumbnail)
|
||||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||||
.into(imageView);
|
.into(imageView);
|
||||||
} else if(!presetName.isNullOrEmpty()) {
|
} else if(!presetName.isNullOrEmpty()) {
|
||||||
@@ -63,7 +72,13 @@ data class ImageVariable(
|
|||||||
return ImageVariable(null, null, null, str);
|
return ImageVariable(null, null, null, str);
|
||||||
}
|
}
|
||||||
fun fromFile(file: File): ImageVariable {
|
fun fromFile(file: File): ImageVariable {
|
||||||
return ImageVariable.fromBitmap(BitmapFactory.decodeFile(file.absolutePath));
|
try {
|
||||||
|
return ImageVariable.fromBitmap(BitmapFactory.decodeFile(file.absolutePath));
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("ImageVariable", "Unsupported image format? " + ex.message, ex);
|
||||||
|
return fromResource(R.drawable.ic_error_pred);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,7 +119,7 @@ class HLS {
|
|||||||
return if (source is IHLSManifestSource) {
|
return if (source is IHLSManifestSource) {
|
||||||
listOf()
|
listOf()
|
||||||
} else if (source is IHLSManifestAudioSource) {
|
} else if (source is IHLSManifestAudioSource) {
|
||||||
listOf(HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, url))
|
listOf(HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, url))
|
||||||
} else {
|
} else {
|
||||||
throw NotImplementedError()
|
throw NotImplementedError()
|
||||||
}
|
}
|
||||||
@@ -340,7 +340,7 @@ class HLS {
|
|||||||
|
|
||||||
val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
||||||
return@mapNotNull when (it.type) {
|
return@mapNotNull when (it.type) {
|
||||||
"AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, it.uri)
|
"AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, false, it.uri)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,353 +0,0 @@
|
|||||||
package com.futo.platformplayer.polycentric
|
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
|
||||||
import com.futo.platformplayer.constructs.BatchedTaskHandler
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.getNowDiffSeconds
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
|
||||||
import com.futo.platformplayer.resolveChannelUrls
|
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
|
||||||
import com.futo.platformplayer.stores.CachedPolycentricProfileStorage
|
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
|
||||||
import com.futo.polycentric.core.ApiMethods
|
|
||||||
import com.futo.polycentric.core.ContentType
|
|
||||||
import com.futo.polycentric.core.OwnedClaim
|
|
||||||
import com.futo.polycentric.core.PublicKey
|
|
||||||
import com.futo.polycentric.core.SignedEvent
|
|
||||||
import com.futo.polycentric.core.StorageTypeSystemState
|
|
||||||
import com.futo.polycentric.core.SystemState
|
|
||||||
import com.futo.polycentric.core.base64ToByteArray
|
|
||||||
import com.futo.polycentric.core.base64UrlToByteArray
|
|
||||||
import com.futo.polycentric.core.getClaimIfValid
|
|
||||||
import com.futo.polycentric.core.getValidClaims
|
|
||||||
import com.google.protobuf.ByteString
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Deferred
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import userpackage.Protocol
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import kotlin.system.measureTimeMillis
|
|
||||||
|
|
||||||
class PolycentricCache {
|
|
||||||
data class CachedOwnedClaims(val ownedClaims: List<OwnedClaim>?, val creationTime: OffsetDateTime = OffsetDateTime.now()) {
|
|
||||||
val expired get() = creationTime.getNowDiffSeconds() > CACHE_EXPIRATION_SECONDS
|
|
||||||
}
|
|
||||||
@Serializable
|
|
||||||
data class CachedPolycentricProfile(val profile: PolycentricProfile?, @Serializable(with = OffsetDateTimeSerializer::class) val creationTime: OffsetDateTime = OffsetDateTime.now()) {
|
|
||||||
val expired get() = creationTime.getNowDiffSeconds() > CACHE_EXPIRATION_SECONDS
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _cache = hashMapOf<PlatformID, CachedOwnedClaims>()
|
|
||||||
private val _profileCache = hashMapOf<PublicKey, CachedPolycentricProfile>()
|
|
||||||
private val _profileUrlCache: CachedPolycentricProfileStorage;
|
|
||||||
private val _scope = CoroutineScope(Dispatchers.IO);
|
|
||||||
init {
|
|
||||||
Logger.i(TAG, "Initializing Polycentric cache");
|
|
||||||
val time = measureTimeMillis {
|
|
||||||
_profileUrlCache = FragmentedStorage.get<CachedPolycentricProfileStorage>("profileUrlCache")
|
|
||||||
}
|
|
||||||
Logger.i(TAG, "Initialized Polycentric cache (${_profileUrlCache.map.size}, ${time}ms)");
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_scope,
|
|
||||||
{ system ->
|
|
||||||
val signedEventsList = ApiMethods.getQueryLatest(
|
|
||||||
SERVER,
|
|
||||||
system.toProto(),
|
|
||||||
listOf(
|
|
||||||
ContentType.BANNER.value,
|
|
||||||
ContentType.AVATAR.value,
|
|
||||||
ContentType.USERNAME.value,
|
|
||||||
ContentType.DESCRIPTION.value,
|
|
||||||
ContentType.STORE.value,
|
|
||||||
ContentType.SERVER.value,
|
|
||||||
ContentType.STORE_DATA.value,
|
|
||||||
ContentType.PROMOTION_BANNER.value,
|
|
||||||
ContentType.PROMOTION.value,
|
|
||||||
ContentType.MEMBERSHIP_URLS.value,
|
|
||||||
ContentType.DONATION_DESTINATIONS.value
|
|
||||||
)
|
|
||||||
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
|
||||||
|
|
||||||
val signedProfileEvents = signedEventsList.groupBy { e -> e.event.contentType }
|
|
||||||
.map { (_, events) -> events.maxBy { it.event.unixMilliseconds ?: 0 } };
|
|
||||||
|
|
||||||
val storageSystemState = StorageTypeSystemState.create()
|
|
||||||
for (signedEvent in signedProfileEvents) {
|
|
||||||
storageSystemState.update(signedEvent.event)
|
|
||||||
}
|
|
||||||
|
|
||||||
val signedClaimEvents = ApiMethods.getQueryIndex(
|
|
||||||
SERVER,
|
|
||||||
system.toProto(),
|
|
||||||
ContentType.CLAIM.value,
|
|
||||||
limit = 200
|
|
||||||
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
|
||||||
|
|
||||||
val ownedClaims: ArrayList<OwnedClaim> = arrayListOf()
|
|
||||||
for (signedEvent in signedClaimEvents) {
|
|
||||||
if (signedEvent.event.contentType != ContentType.CLAIM.value) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = ApiMethods.getQueryReferences(
|
|
||||||
SERVER,
|
|
||||||
Protocol.Reference.newBuilder()
|
|
||||||
.setReference(signedEvent.toPointer().toProto().toByteString())
|
|
||||||
.setReferenceType(2)
|
|
||||||
.build(),
|
|
||||||
null,
|
|
||||||
Protocol.QueryReferencesRequestEvents.newBuilder()
|
|
||||||
.setFromType(ContentType.VOUCH.value)
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
|
|
||||||
val ownedClaim = response.itemsList.map { SignedEvent.fromProto(it.event) }.getClaimIfValid(signedEvent);
|
|
||||||
if (ownedClaim != null) {
|
|
||||||
ownedClaims.add(ownedClaim);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Retrieved profile (ownedClaims = $ownedClaims)");
|
|
||||||
val systemState = SystemState.fromStorageTypeSystemState(storageSystemState);
|
|
||||||
return@BatchedTaskHandler CachedPolycentricProfile(PolycentricProfile(system, systemState, ownedClaims));
|
|
||||||
},
|
|
||||||
{ system -> return@BatchedTaskHandler getCachedProfile(system); },
|
|
||||||
{ system, result ->
|
|
||||||
synchronized(_cache) {
|
|
||||||
_profileCache[system] = result;
|
|
||||||
|
|
||||||
if (result.profile != null) {
|
|
||||||
for (claim in result.profile.ownedClaims) {
|
|
||||||
val urls = claim.claim.resolveChannelUrls();
|
|
||||||
for (url in urls)
|
|
||||||
_profileUrlCache.map[url] = result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_profileUrlCache.save();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
private val _batchTaskGetClaims = BatchedTaskHandler<PlatformID, CachedOwnedClaims>(_scope,
|
|
||||||
{ id ->
|
|
||||||
val resolved = if (id.claimFieldType == -1) ApiMethods.getResolveClaim(SERVER, system, id.claimType.toLong(), id.value!!)
|
|
||||||
else ApiMethods.getResolveClaim(SERVER, system, id.claimType.toLong(), id.claimFieldType.toLong(), id.value!!);
|
|
||||||
Logger.v(TAG, "getResolveClaim(url = $SERVER, system = $system, id = $id, claimType = ${id.claimType}, matchAnyField = ${id.value})");
|
|
||||||
val protoEvents = resolved.matchesList.flatMap { arrayListOf(it.claim).apply { addAll(it.proofChainList) } }
|
|
||||||
val resolvedEvents = protoEvents.map { i -> SignedEvent.fromProto(i) };
|
|
||||||
return@BatchedTaskHandler CachedOwnedClaims(resolvedEvents.getValidClaims());
|
|
||||||
},
|
|
||||||
{ id -> return@BatchedTaskHandler getCachedValidClaims(id); },
|
|
||||||
{ id, result ->
|
|
||||||
synchronized(_cache) {
|
|
||||||
_cache[id] = result;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
private val _batchTaskGetData = BatchedTaskHandler<String, ByteBuffer>(_scope,
|
|
||||||
{
|
|
||||||
val dataLink = getDataLinkFromUrl(it) ?: throw Exception("Only URLInfoDataLink is supported");
|
|
||||||
return@BatchedTaskHandler ApiMethods.getDataFromServerAndReassemble(dataLink);
|
|
||||||
},
|
|
||||||
{ return@BatchedTaskHandler null },
|
|
||||||
{ _, _ -> });
|
|
||||||
|
|
||||||
fun getCachedValidClaims(id: PlatformID, ignoreExpired: Boolean = false): CachedOwnedClaims? {
|
|
||||||
if (!StatePolycentric.instance.enabled || id.claimType <= 0) {
|
|
||||||
return CachedOwnedClaims(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_cache) {
|
|
||||||
val cached = _cache[id]
|
|
||||||
if (cached == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ignoreExpired && cached.expired) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: Review all return null in this file, perhaps it should be CachedX(null) instead
|
|
||||||
fun getValidClaimsAsync(id: PlatformID): Deferred<CachedOwnedClaims> {
|
|
||||||
if (!StatePolycentric.instance.enabled || id.value == null || id.claimType <= 0) {
|
|
||||||
return _scope.async { CachedOwnedClaims(null) };
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.v(TAG, "getValidClaims (id: $id)")
|
|
||||||
val def = _batchTaskGetClaims.execute(id);
|
|
||||||
def.invokeOnCompletion {
|
|
||||||
if (it == null) {
|
|
||||||
return@invokeOnCompletion
|
|
||||||
}
|
|
||||||
|
|
||||||
handleException(it, handleNetworkException = { /* Do nothing (do not cache) */ }, handleOtherException = {
|
|
||||||
//Cache failed result
|
|
||||||
synchronized(_cache) {
|
|
||||||
_cache[id] = CachedOwnedClaims(null);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
return def;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDataAsync(url: String): Deferred<ByteBuffer> {
|
|
||||||
StatePolycentric.instance.ensureEnabled()
|
|
||||||
return _batchTaskGetData.execute(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCachedProfile(url: String, ignoreExpired: Boolean = false): CachedPolycentricProfile? {
|
|
||||||
if (!StatePolycentric.instance.enabled) {
|
|
||||||
return CachedPolycentricProfile(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized (_profileCache) {
|
|
||||||
val cached = _profileUrlCache.get(url) ?: return null;
|
|
||||||
if (!ignoreExpired && cached.expired) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCachedProfile(system: PublicKey, ignoreExpired: Boolean = false): CachedPolycentricProfile? {
|
|
||||||
if (!StatePolycentric.instance.enabled) {
|
|
||||||
return CachedPolycentricProfile(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(_profileCache) {
|
|
||||||
val cached = _profileCache[system] ?: return null;
|
|
||||||
if (!ignoreExpired && cached.expired) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getProfileAsync(id: PlatformID, urlNullCache: String? = null): CachedPolycentricProfile? {
|
|
||||||
if (!StatePolycentric.instance.enabled || id.claimType <= 0) {
|
|
||||||
return CachedPolycentricProfile(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
val cachedClaims = getCachedValidClaims(id);
|
|
||||||
if (cachedClaims != null) {
|
|
||||||
if (!cachedClaims.ownedClaims.isNullOrEmpty()) {
|
|
||||||
Logger.v(TAG, "getProfileAsync (id: $id) != null (with cached valid claims)")
|
|
||||||
return getProfileAsync(cachedClaims.ownedClaims.first().system).await();
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Logger.v(TAG, "getProfileAsync (id: $id) no cached valid claims, will be retrieved")
|
|
||||||
|
|
||||||
val claims = getValidClaimsAsync(id).await()
|
|
||||||
if (!claims.ownedClaims.isNullOrEmpty()) {
|
|
||||||
Logger.v(TAG, "getProfileAsync (id: $id) != null (with retrieved valid claims)")
|
|
||||||
return getProfileAsync(claims.ownedClaims.first().system).await()
|
|
||||||
} else {
|
|
||||||
synchronized (_cache) {
|
|
||||||
if (urlNullCache != null) {
|
|
||||||
_profileUrlCache.setAndSave(urlNullCache, CachedPolycentricProfile(null))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getProfileAsync(system: PublicKey): Deferred<CachedPolycentricProfile?> {
|
|
||||||
if (!StatePolycentric.instance.enabled) {
|
|
||||||
return _scope.async { CachedPolycentricProfile(null) };
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "getProfileAsync (system: ${system})")
|
|
||||||
val def = _taskGetProfile.execute(system);
|
|
||||||
def.invokeOnCompletion {
|
|
||||||
if (it == null) {
|
|
||||||
return@invokeOnCompletion
|
|
||||||
}
|
|
||||||
|
|
||||||
handleException(it, handleNetworkException = { /* Do nothing (do not cache) */ }, handleOtherException = {
|
|
||||||
//Cache failed result
|
|
||||||
synchronized(_cache) {
|
|
||||||
val cachedProfile = CachedPolycentricProfile(null);
|
|
||||||
_profileCache[system] = cachedProfile;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
return def;
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleException(e: Throwable, handleNetworkException: () -> Unit, handleOtherException: () -> Unit) {
|
|
||||||
val isNetworkException = when(e) {
|
|
||||||
is java.net.UnknownHostException,
|
|
||||||
is java.net.SocketTimeoutException,
|
|
||||||
is java.net.ConnectException -> true
|
|
||||||
else -> when(e.cause) {
|
|
||||||
is java.net.UnknownHostException,
|
|
||||||
is java.net.SocketTimeoutException,
|
|
||||||
is java.net.ConnectException -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isNetworkException) {
|
|
||||||
handleNetworkException()
|
|
||||||
} else {
|
|
||||||
handleOtherException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val system = Protocol.PublicKey.newBuilder()
|
|
||||||
.setKeyType(1)
|
|
||||||
.setKey(ByteString.copyFrom("gX0eCWctTm6WHVGot4sMAh7NDAIwWsIM5tRsOz9dX04=".base64ToByteArray())) //Production key
|
|
||||||
//.setKey(ByteString.copyFrom("LeQkzn1j625YZcZHayfCmTX+6ptrzsA+CdAyq+BcEdQ".base64ToByteArray())) //Test key koen-futo
|
|
||||||
.build();
|
|
||||||
|
|
||||||
private const val TAG = "PolycentricCache"
|
|
||||||
const val SERVER = "https://srv1-prod.polycentric.io"
|
|
||||||
private var _instance: PolycentricCache? = null;
|
|
||||||
private val CACHE_EXPIRATION_SECONDS = 60 * 5;
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
val instance: PolycentricCache
|
|
||||||
get(){
|
|
||||||
if(_instance == null)
|
|
||||||
_instance = PolycentricCache();
|
|
||||||
return _instance!!;
|
|
||||||
};
|
|
||||||
|
|
||||||
fun finish() {
|
|
||||||
_instance?.let {
|
|
||||||
_instance = null;
|
|
||||||
it._scope.cancel("PolycentricCache finished");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDataLinkFromUrl(it: String): Protocol.URLInfoDataLink? {
|
|
||||||
val urlData = if (it.startsWith("polycentric://")) {
|
|
||||||
it.substring("polycentric://".length)
|
|
||||||
} else it;
|
|
||||||
|
|
||||||
val urlBytes = urlData.base64UrlToByteArray();
|
|
||||||
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
|
|
||||||
if (urlInfo.urlType != 4L) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
|
||||||
return dataLink
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -39,4 +39,16 @@ class OffsetDateTimeSerializer : KSerializer<OffsetDateTime> {
|
|||||||
return OffsetDateTime.MIN;
|
return OffsetDateTime.MIN;
|
||||||
return OffsetDateTime.of(LocalDateTime.ofEpochSecond(epochSecond, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
return OffsetDateTime.of(LocalDateTime.ofEpochSecond(epochSecond, 0, ZoneOffset.UTC), ZoneOffset.UTC);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
class OffsetDateTimeStringSerializer : KSerializer<OffsetDateTime> {
|
||||||
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("OffsetDateTime", PrimitiveKind.STRING)
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: OffsetDateTime) {
|
||||||
|
encoder.encodeString(value.toString());
|
||||||
|
}
|
||||||
|
override fun deserialize(decoder: Decoder): OffsetDateTime {
|
||||||
|
val str = decoder.decodeString();
|
||||||
|
|
||||||
|
return OffsetDateTime.parse(str);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,6 @@ import com.futo.platformplayer.api.media.structures.DedupContentPager
|
|||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.serializers.PlatformContentSerializer
|
import com.futo.platformplayer.serializers.PlatformContentSerializer
|
||||||
import com.futo.platformplayer.stores.db.ManagedDBStore
|
import com.futo.platformplayer.stores.db.ManagedDBStore
|
||||||
@@ -50,14 +49,7 @@ class StateCache {
|
|||||||
val subs = StateSubscriptions.instance.getSubscriptions();
|
val subs = StateSubscriptions.instance.getSubscriptions();
|
||||||
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
|
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
|
||||||
val allUrls = subs
|
val allUrls = subs
|
||||||
.map {
|
.map { it.channel.url }
|
||||||
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
|
|
||||||
if(!otherUrls.contains(it.channel.url))
|
|
||||||
return@map listOf(listOf(it.channel.url), otherUrls).flatten();
|
|
||||||
else
|
|
||||||
return@map otherUrls;
|
|
||||||
}
|
|
||||||
.flatten()
|
|
||||||
.distinct()
|
.distinct()
|
||||||
.filter { StatePlatform.instance.hasEnabledChannelClient(it) };
|
.filter { StatePlatform.instance.hasEnabledChannelClient(it) };
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package com.futo.platformplayer.states
|
|||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.StatFs
|
import android.os.StatFs
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
@@ -46,6 +48,17 @@ class StateDownloads {
|
|||||||
private val _downloadsStat = StatFs(_downloadsDirectory.absolutePath);
|
private val _downloadsStat = StatFs(_downloadsDirectory.absolutePath);
|
||||||
|
|
||||||
private val _downloaded = FragmentedStorage.storeJson<VideoLocal>("downloaded")
|
private val _downloaded = FragmentedStorage.storeJson<VideoLocal>("downloaded")
|
||||||
|
.withOnModified({
|
||||||
|
synchronized(_downloadedSet) {
|
||||||
|
if(!_downloadedSet.contains(it.id))
|
||||||
|
_downloadedSet.add(it.id);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
synchronized(_downloadedSet) {
|
||||||
|
if(_downloadedSet.contains(it.id))
|
||||||
|
_downloadedSet.remove(it.id);
|
||||||
|
}
|
||||||
|
})
|
||||||
.load()
|
.load()
|
||||||
.apply { afterLoadingDownloaded(this) };
|
.apply { afterLoadingDownloaded(this) };
|
||||||
private val _downloading = FragmentedStorage.storeJson<VideoDownload>("downloading")
|
private val _downloading = FragmentedStorage.storeJson<VideoDownload>("downloading")
|
||||||
@@ -85,9 +98,6 @@ class StateDownloads {
|
|||||||
Logger.i("StateDownloads", "Deleting local video ${id.value}");
|
Logger.i("StateDownloads", "Deleting local video ${id.value}");
|
||||||
val downloaded = getCachedVideo(id);
|
val downloaded = getCachedVideo(id);
|
||||||
if(downloaded != null) {
|
if(downloaded != null) {
|
||||||
synchronized(_downloadedSet) {
|
|
||||||
_downloadedSet.remove(id);
|
|
||||||
}
|
|
||||||
_downloaded.delete(downloaded);
|
_downloaded.delete(downloaded);
|
||||||
}
|
}
|
||||||
onDownloadedChanged.emit();
|
onDownloadedChanged.emit();
|
||||||
@@ -261,9 +271,6 @@ class StateDownloads {
|
|||||||
if(existing.groupID == null) {
|
if(existing.groupID == null) {
|
||||||
existing.groupID = VideoDownload.GROUP_WATCHLATER;
|
existing.groupID = VideoDownload.GROUP_WATCHLATER;
|
||||||
existing.groupType = VideoDownload.GROUP_WATCHLATER;
|
existing.groupType = VideoDownload.GROUP_WATCHLATER;
|
||||||
synchronized(_downloadedSet) {
|
|
||||||
_downloadedSet.add(existing.id);
|
|
||||||
}
|
|
||||||
_downloaded.save(existing);
|
_downloaded.save(existing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,9 +313,6 @@ class StateDownloads {
|
|||||||
if(existing.groupID == null) {
|
if(existing.groupID == null) {
|
||||||
existing.groupID = playlist.id;
|
existing.groupID = playlist.id;
|
||||||
existing.groupType = VideoDownload.GROUP_PLAYLIST;
|
existing.groupType = VideoDownload.GROUP_PLAYLIST;
|
||||||
synchronized(_downloadedSet) {
|
|
||||||
_downloadedSet.add(existing.id);
|
|
||||||
}
|
|
||||||
_downloaded.save(existing);
|
_downloaded.save(existing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -466,6 +470,65 @@ class StateDownloads {
|
|||||||
return _downloadsDirectory;
|
return _downloadsDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun exportPlaylist(context: Context, playlistId: String) {
|
||||||
|
if(context is IWithResultLauncher)
|
||||||
|
StateApp.instance.requestDirectoryAccess(context, "Export Playlist", "To export playlist to directory", null) {
|
||||||
|
if (it == null)
|
||||||
|
return@requestDirectoryAccess;
|
||||||
|
|
||||||
|
val root = DocumentFile.fromTreeUri(context, it!!);
|
||||||
|
|
||||||
|
val playlist = StatePlaylists.instance.getPlaylist(playlistId);
|
||||||
|
var localVideos = StateDownloads.instance.getDownloadedVideosPlaylist(playlistId);
|
||||||
|
if(playlist != null) {
|
||||||
|
val missing = playlist.videos
|
||||||
|
.filter { vid -> !localVideos.any { it.id.value == null || it.id.value == vid.id.value } }
|
||||||
|
.map { getCachedVideo(it.id) }
|
||||||
|
.filterNotNull();
|
||||||
|
if(missing.size > 0)
|
||||||
|
localVideos = localVideos + missing;
|
||||||
|
};
|
||||||
|
|
||||||
|
var lastNotifyTime = -1L;
|
||||||
|
|
||||||
|
UIDialogs.showDialogProgress(context) {
|
||||||
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
|
it.setText("Exporting videos..");
|
||||||
|
var i = 0;
|
||||||
|
var success = 0;
|
||||||
|
for (video in localVideos) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
it.setText("Exporting videos...(${i}/${localVideos.size})");
|
||||||
|
//it.setProgress(i.toDouble() / localVideos.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val export = VideoExport(video, video.videoSource.firstOrNull(), video.audioSource.firstOrNull(), video.subtitlesSources.firstOrNull());
|
||||||
|
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
|
||||||
|
|
||||||
|
val file = export.export(context, { progress ->
|
||||||
|
val now = System.currentTimeMillis();
|
||||||
|
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
|
||||||
|
it.setProgress(progress);
|
||||||
|
lastNotifyTime = now;
|
||||||
|
}
|
||||||
|
}, root);
|
||||||
|
success++;
|
||||||
|
} catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed export [${video.name}]: ${ex.message}", ex);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
it.setProgress(1f);
|
||||||
|
it.dismiss();
|
||||||
|
UIDialogs.appToast("Finished exporting playlist (${success} videos${if(i < success) ", ${i} errors" else ""})");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
|
fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
|
||||||
var lastNotifyTime = -1L;
|
var lastNotifyTime = -1L;
|
||||||
|
|
||||||
@@ -477,13 +540,13 @@ class StateDownloads {
|
|||||||
try {
|
try {
|
||||||
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
|
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
|
||||||
|
|
||||||
val file = export.export(context) { progress ->
|
val file = export.export(context, { progress ->
|
||||||
val now = System.currentTimeMillis();
|
val now = System.currentTimeMillis();
|
||||||
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
|
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
|
||||||
it.setProgress(progress);
|
it.setProgress(progress);
|
||||||
lastNotifyTime = now;
|
lastNotifyTime = now;
|
||||||
}
|
}
|
||||||
}
|
}, null);
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
it.setProgress(100.0f)
|
it.setProgress(100.0f)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class StateMeta {
|
|||||||
return when(lastCommentSection.value){
|
return when(lastCommentSection.value){
|
||||||
"Polycentric" -> 0;
|
"Polycentric" -> 0;
|
||||||
"Platform" -> 1;
|
"Platform" -> 1;
|
||||||
else -> 1
|
else -> 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun setLastCommentSection(value: Int) {
|
fun setLastCommentSection(value: Int) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.futo.platformplayer.UIDialogs
|
|||||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -130,6 +131,12 @@ class StatePlayer {
|
|||||||
closeMediaSession();
|
closeMediaSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveQueueAsPlaylist(name: String){
|
||||||
|
val videos = _queue.toList();
|
||||||
|
val playlist = Playlist(name, videos.map { SerializedPlatformVideo.fromVideo(it) });
|
||||||
|
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||||
|
}
|
||||||
|
|
||||||
//Notifications
|
//Notifications
|
||||||
fun hasMediaSession() : Boolean {
|
fun hasMediaSession() : Boolean {
|
||||||
return MediaPlaybackService.getService() != null;
|
return MediaPlaybackService.getService() != null;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.net.Uri
|
|||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
@@ -176,11 +177,14 @@ class StatePlaylists {
|
|||||||
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1) {
|
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1): Boolean {
|
||||||
|
var wasNew = false;
|
||||||
synchronized(_watchlistStore) {
|
synchronized(_watchlistStore) {
|
||||||
|
if(!_watchlistStore.hasItem { it.url == video.url })
|
||||||
|
wasNew = true;
|
||||||
_watchlistStore.saveAsync(video);
|
_watchlistStore.saveAsync(video);
|
||||||
if(orderPosition == -1)
|
if(orderPosition == -1)
|
||||||
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values) .toTypedArray());
|
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray());
|
||||||
else {
|
else {
|
||||||
val existing = _watchlistOrderStore.getAllValues().toMutableList();
|
val existing = _watchlistOrderStore.getAllValues().toMutableList();
|
||||||
existing.add(orderPosition, video.url);
|
existing.add(orderPosition, video.url);
|
||||||
@@ -197,6 +201,7 @@ class StatePlaylists {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||||
|
return wasNew;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLastPlayedPlaylist() : Playlist? {
|
fun getLastPlayedPlaylist() : Playlist? {
|
||||||
@@ -225,17 +230,20 @@ class StatePlaylists {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fun getWatchLaterSyncPacket(orderOnly: Boolean = false): SyncWatchLaterPackage{
|
||||||
|
return SyncWatchLaterPackage(
|
||||||
|
if (orderOnly) listOf() else getWatchLater(),
|
||||||
|
if (orderOnly) mapOf() else _watchLaterAdds.all(),
|
||||||
|
if (orderOnly) mapOf() else _watchLaterRemovals.all(),
|
||||||
|
getWatchLaterLastReorderTime().toEpochSecond(),
|
||||||
|
_watchlistOrderStore.values.toList()
|
||||||
|
)
|
||||||
|
}
|
||||||
private fun broadcastWatchLater(orderOnly: Boolean = false) {
|
private fun broadcastWatchLater(orderOnly: Boolean = false) {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
StateSync.instance.broadcastJsonData(
|
StateSync.instance.broadcastJsonData(
|
||||||
GJSyncOpcodes.syncWatchLater, SyncWatchLaterPackage(
|
GJSyncOpcodes.syncWatchLater, getWatchLaterSyncPacket(orderOnly)
|
||||||
if (orderOnly) listOf() else getWatchLater(),
|
|
||||||
if (orderOnly) mapOf() else _watchLaterAdds.all(),
|
|
||||||
if (orderOnly) mapOf() else _watchLaterRemovals.all(),
|
|
||||||
getWatchLaterLastReorderTime().toEpochSecond(),
|
|
||||||
_watchlistOrderStore.values.toList()
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to broadcast watch later", e)
|
Logger.w(TAG, "Failed to broadcast watch later", e)
|
||||||
@@ -306,14 +314,19 @@ class StatePlaylists {
|
|||||||
broadcastSyncPlaylist(playlist);
|
broadcastSyncPlaylist(playlist);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun addToPlaylist(id: String, video: IPlatformVideo) {
|
fun addToPlaylist(id: String, video: IPlatformVideo): Boolean {
|
||||||
synchronized(playlistStore) {
|
synchronized(playlistStore) {
|
||||||
val playlist = getPlaylist(id) ?: return;
|
val playlist = getPlaylist(id) ?: return false;
|
||||||
|
if(!Settings.instance.other.playlistAllowDups && playlist.videos.any { it.url == video.url })
|
||||||
|
return false;
|
||||||
|
|
||||||
|
|
||||||
playlist.videos.add(SerializedPlatformVideo.fromVideo(video));
|
playlist.videos.add(SerializedPlatformVideo.fromVideo(video));
|
||||||
playlist.dateUpdate = OffsetDateTime.now();
|
playlist.dateUpdate = OffsetDateTime.now();
|
||||||
playlistStore.saveAsync(playlist, true);
|
playlistStore.saveAsync(playlist, true);
|
||||||
|
|
||||||
broadcastSyncPlaylist(playlist);
|
broadcastSyncPlaylist(playlist);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.states
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.LoginActivity
|
import com.futo.platformplayer.activities.LoginActivity
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
@@ -101,6 +102,8 @@ class StatePlugins {
|
|||||||
if (availableClient !is JSClient) {
|
if (availableClient !is JSClient) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if(!Settings.instance.plugins.checkDisabledPluginsForUpdates && !StatePlatform.instance.isClientEnabled(availableClient.id))
|
||||||
|
continue;
|
||||||
|
|
||||||
val newConfig = checkForUpdates(availableClient.config);
|
val newConfig = checkForUpdates(availableClient.config);
|
||||||
if (newConfig != null) {
|
if (newConfig != null) {
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ import com.futo.platformplayer.api.media.structures.IPager
|
|||||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||||
import com.futo.platformplayer.awaitFirstDeferred
|
import com.futo.platformplayer.awaitFirstDeferred
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
@@ -33,6 +31,7 @@ import com.futo.polycentric.core.ApiMethods
|
|||||||
import com.futo.polycentric.core.ClaimType
|
import com.futo.polycentric.core.ClaimType
|
||||||
import com.futo.polycentric.core.ContentType
|
import com.futo.polycentric.core.ContentType
|
||||||
import com.futo.polycentric.core.Opinion
|
import com.futo.polycentric.core.Opinion
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
import com.futo.polycentric.core.ProcessHandle
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import com.futo.polycentric.core.PublicKey
|
import com.futo.polycentric.core.PublicKey
|
||||||
import com.futo.polycentric.core.SignedEvent
|
import com.futo.polycentric.core.SignedEvent
|
||||||
@@ -234,34 +233,7 @@ class StatePolycentric {
|
|||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return Pair(false, listOf(url));
|
return Pair(false, listOf(url));
|
||||||
}
|
}
|
||||||
var polycentricProfile: PolycentricProfile? = null;
|
return Pair(didUpdate, listOf(url));
|
||||||
try {
|
|
||||||
val polycentricCached = PolycentricCache.instance.getCachedProfile(url, cacheOnly)
|
|
||||||
polycentricProfile = polycentricCached?.profile;
|
|
||||||
if (polycentricCached == null && channelId != null) {
|
|
||||||
Logger.i("StateSubscriptions", "Get polycentric profile not cached");
|
|
||||||
if(!cacheOnly) {
|
|
||||||
polycentricProfile = runBlocking { PolycentricCache.instance.getProfileAsync(channelId, if(doCacheNull) url else null) }?.profile;
|
|
||||||
didUpdate = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Logger.i("StateSubscriptions", "Get polycentric profile cached");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.w(StateSubscriptions.TAG, "Polycentric getCachedProfile failed for subscriptions", ex);
|
|
||||||
//TODO: Some way to communicate polycentric failing without blocking here
|
|
||||||
}
|
|
||||||
if(polycentricProfile != null) {
|
|
||||||
val urls = polycentricProfile.ownedClaims.groupBy { it.claim.claimType }
|
|
||||||
.mapNotNull { it.value.firstOrNull()?.claim?.resolveChannelUrl() }.toMutableList();
|
|
||||||
if(urls.any { it.equals(url, true) })
|
|
||||||
return Pair(didUpdate, urls);
|
|
||||||
else
|
|
||||||
return Pair(didUpdate, listOf(url) + urls);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return Pair(didUpdate, listOf(url));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager<IPlatformContent>? {
|
fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager<IPlatformContent>? {
|
||||||
@@ -325,7 +297,7 @@ class StatePolycentric {
|
|||||||
id = PlatformID("polycentric", author, null, ClaimType.POLYCENTRIC.value.toInt()),
|
id = PlatformID("polycentric", author, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||||
name = systemState.username,
|
name = systemState.username,
|
||||||
url = author,
|
url = author,
|
||||||
thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
|
||||||
subscribers = null
|
subscribers = null
|
||||||
),
|
),
|
||||||
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||||
@@ -349,7 +321,7 @@ class StatePolycentric {
|
|||||||
suspend fun getLikesDislikesReplies(reference: Protocol.Reference): LikesDislikesReplies {
|
suspend fun getLikesDislikesReplies(reference: Protocol.Reference): LikesDislikesReplies {
|
||||||
ensureEnabled()
|
ensureEnabled()
|
||||||
|
|
||||||
val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null,
|
val response = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, null,
|
||||||
null,
|
null,
|
||||||
listOf(
|
listOf(
|
||||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
||||||
@@ -382,7 +354,7 @@ class StatePolycentric {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val pointer = Protocol.Pointer.parseFrom(reference.reference)
|
val pointer = Protocol.Pointer.parseFrom(reference.reference)
|
||||||
val events = ApiMethods.getEvents(PolycentricCache.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder()
|
val events = ApiMethods.getEvents(ApiMethods.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder()
|
||||||
.addRangesForProcesses(Protocol.RangesForProcess.newBuilder()
|
.addRangesForProcesses(Protocol.RangesForProcess.newBuilder()
|
||||||
.setProcess(pointer.process)
|
.setProcess(pointer.process)
|
||||||
.addRanges(Protocol.Range.newBuilder()
|
.addRanges(Protocol.Range.newBuilder()
|
||||||
@@ -400,11 +372,11 @@ class StatePolycentric {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val post = Protocol.Post.parseFrom(ev.content);
|
val post = Protocol.Post.parseFrom(ev.content);
|
||||||
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
|
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(ApiMethods.SERVER));
|
||||||
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
||||||
|
|
||||||
val profileEvents = ApiMethods.getQueryLatest(
|
val profileEvents = ApiMethods.getQueryLatest(
|
||||||
PolycentricCache.SERVER,
|
ApiMethods.SERVER,
|
||||||
ev.system.toProto(),
|
ev.system.toProto(),
|
||||||
listOf(
|
listOf(
|
||||||
ContentType.AVATAR.value,
|
ContentType.AVATAR.value,
|
||||||
@@ -433,7 +405,7 @@ class StatePolycentric {
|
|||||||
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||||
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
||||||
url = systemLinkUrl,
|
url = systemLinkUrl,
|
||||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
|
||||||
subscribers = null
|
subscribers = null
|
||||||
),
|
),
|
||||||
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||||
@@ -445,12 +417,12 @@ class StatePolycentric {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
|
suspend fun getCommentPager(contextUrl: String, reference: Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return EmptyPager()
|
return EmptyPager()
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null,
|
val response = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, null,
|
||||||
Protocol.QueryReferencesRequestEvents.newBuilder()
|
Protocol.QueryReferencesRequestEvents.newBuilder()
|
||||||
.setFromType(ContentType.POST.value)
|
.setFromType(ContentType.POST.value)
|
||||||
.addAllCountLwwElementReferences(arrayListOf(
|
.addAllCountLwwElementReferences(arrayListOf(
|
||||||
@@ -486,7 +458,7 @@ class StatePolycentric {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun nextPageAsync() {
|
override suspend fun nextPageAsync() {
|
||||||
val nextPageResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, _cursor,
|
val nextPageResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, _cursor,
|
||||||
Protocol.QueryReferencesRequestEvents.newBuilder()
|
Protocol.QueryReferencesRequestEvents.newBuilder()
|
||||||
.setFromType(ContentType.POST.value)
|
.setFromType(ContentType.POST.value)
|
||||||
.addAllCountLwwElementReferences(arrayListOf(
|
.addAllCountLwwElementReferences(arrayListOf(
|
||||||
@@ -534,7 +506,7 @@ class StatePolycentric {
|
|||||||
return@mapNotNull LazyComment(scope.async(_commentPoolDispatcher){
|
return@mapNotNull LazyComment(scope.async(_commentPoolDispatcher){
|
||||||
Logger.i(TAG, "Fetching comment data for [" + ev.system.key.toBase64() + "]");
|
Logger.i(TAG, "Fetching comment data for [" + ev.system.key.toBase64() + "]");
|
||||||
val profileEvents = ApiMethods.getQueryLatest(
|
val profileEvents = ApiMethods.getQueryLatest(
|
||||||
PolycentricCache.SERVER,
|
ApiMethods.SERVER,
|
||||||
ev.system.toProto(),
|
ev.system.toProto(),
|
||||||
listOf(
|
listOf(
|
||||||
ContentType.AVATAR.value,
|
ContentType.AVATAR.value,
|
||||||
@@ -558,7 +530,7 @@ class StatePolycentric {
|
|||||||
|
|
||||||
val unixMilliseconds = ev.unixMilliseconds
|
val unixMilliseconds = ev.unixMilliseconds
|
||||||
//TODO: Don't use single hardcoded sderver here
|
//TODO: Don't use single hardcoded sderver here
|
||||||
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
|
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(ApiMethods.SERVER));
|
||||||
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
||||||
return@async PolycentricPlatformComment(
|
return@async PolycentricPlatformComment(
|
||||||
contextUrl = contextUrl,
|
contextUrl = contextUrl,
|
||||||
@@ -566,7 +538,7 @@ class StatePolycentric {
|
|||||||
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||||
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
||||||
url = systemLinkUrl,
|
url = systemLinkUrl,
|
||||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
|
||||||
subscribers = null
|
subscribers = null
|
||||||
),
|
),
|
||||||
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||||
|
|||||||
@@ -1,54 +1,17 @@
|
|||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
import com.futo.platformplayer.Settings
|
|
||||||
import com.futo.platformplayer.UIDialogs
|
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
|
||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
|
||||||
import com.futo.platformplayer.api.media.structures.*
|
|
||||||
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
|
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
|
||||||
import com.futo.platformplayer.constructs.Event2
|
|
||||||
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.exceptions.ChannelException
|
|
||||||
import com.futo.platformplayer.findNonRuntimeException
|
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
|
||||||
import com.futo.platformplayer.getNowDiffDays
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.Subscription
|
|
||||||
import com.futo.platformplayer.models.SubscriptionGroup
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
|
||||||
import com.futo.platformplayer.states.StateHistory.Companion
|
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringDateMapStorage
|
import com.futo.platformplayer.stores.StringDateMapStorage
|
||||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
|
||||||
import com.futo.platformplayer.stores.v2.ReconstructStore
|
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
|
||||||
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
|
|
||||||
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
|
|
||||||
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
|
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
|
||||||
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
|
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
|
||||||
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.ExecutionException
|
|
||||||
import java.util.concurrent.ForkJoinPool
|
|
||||||
import java.util.concurrent.ForkJoinTask
|
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
import kotlin.streams.asSequence
|
|
||||||
import kotlin.streams.toList
|
|
||||||
import kotlin.system.measureTimeMillis
|
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* Used to maintain subscription groups
|
* Used to maintain subscription groups
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.states
|
package com.futo.platformplayer.states
|
||||||
|
|
||||||
|
import SubsExchangeClient
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
@@ -15,10 +16,10 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import com.futo.platformplayer.models.ImportCache
|
import com.futo.platformplayer.models.ImportCache
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.models.SubscriptionGroup
|
import com.futo.platformplayer.models.SubscriptionGroup
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import com.futo.platformplayer.resolveChannelUrl
|
import com.futo.platformplayer.resolveChannelUrl
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.StringDateMapStorage
|
import com.futo.platformplayer.stores.StringDateMapStorage
|
||||||
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
import com.futo.platformplayer.stores.StringStringMapStorage
|
import com.futo.platformplayer.stores.StringStringMapStorage
|
||||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||||
import com.futo.platformplayer.stores.v2.ReconstructStore
|
import com.futo.platformplayer.stores.v2.ReconstructStore
|
||||||
@@ -68,10 +69,24 @@ class StateSubscriptions {
|
|||||||
|
|
||||||
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
||||||
|
|
||||||
|
private val _subsExchangeServer = "https://exchange.grayjay.app/";
|
||||||
|
private val _subscriptionKey = FragmentedStorage.get<StringStorage>("sub_exchange_key");
|
||||||
|
|
||||||
init {
|
init {
|
||||||
global.onUpdateProgress.subscribe { progress, total ->
|
global.onUpdateProgress.subscribe { progress, total ->
|
||||||
onFeedProgress.emit(null, progress, total);
|
onFeedProgress.emit(null, progress, total);
|
||||||
}
|
}
|
||||||
|
if(_subscriptionKey.value.isNullOrBlank())
|
||||||
|
generateNewSubsExchangeKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateNewSubsExchangeKey(){
|
||||||
|
_subscriptionKey.setAndSave(SubsExchangeClient.createPrivateKey());
|
||||||
|
}
|
||||||
|
fun getSubsExchangeClient(): SubsExchangeClient {
|
||||||
|
if(_subscriptionKey.value.isNullOrBlank())
|
||||||
|
throw IllegalStateException("No valid subscription exchange key set");
|
||||||
|
return SubsExchangeClient(_subsExchangeServer, _subscriptionKey.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOldestUpdateTime(): OffsetDateTime {
|
fun getOldestUpdateTime(): OffsetDateTime {
|
||||||
@@ -335,12 +350,6 @@ class StateSubscriptions {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: This causes issues, because what if the profile is not cached yet when the susbcribe button is loaded for example?
|
|
||||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(urls.first(), true)?.profile;
|
|
||||||
if (cachedProfile != null) {
|
|
||||||
return cachedProfile.ownedClaims.any { c -> _subscriptions.hasItem { s -> c.claim.resolveChannelUrl() == s.channel.url } };
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,7 +375,17 @@ class StateSubscriptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null, subGroup: SubscriptionGroup? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
fun getSubscriptionsFeedWithExceptions(allowFailure: Boolean = false, withCacheFallback: Boolean = false, cacheScope: CoroutineScope, onProgress: ((Int, Int)->Unit)? = null, onNewCacheHit: ((Subscription, IPlatformContent)->Unit)? = null, subGroup: SubscriptionGroup? = null): Pair<IPager<IPlatformContent>, List<Throwable>> {
|
||||||
val algo = SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, cacheScope, allowFailure, withCacheFallback, _subscriptionsPool);
|
var exchangeClient: SubsExchangeClient? = null;
|
||||||
|
if(Settings.instance.subscriptions.useSubscriptionExchange) {
|
||||||
|
try {
|
||||||
|
exchangeClient = getSubsExchangeClient();
|
||||||
|
}
|
||||||
|
catch(ex: Throwable){
|
||||||
|
Logger.e(TAG, "Failed to get subs exchange client: ${ex.message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val algo = SubscriptionFetchAlgorithm.getAlgorithm(_algorithmSubscriptions, cacheScope, allowFailure, withCacheFallback, _subscriptionsPool, exchangeClient);
|
||||||
if(onNewCacheHit != null)
|
if(onNewCacheHit != null)
|
||||||
algo.onNewCacheHit.subscribe(onNewCacheHit)
|
algo.onNewCacheHit.subscribe(onNewCacheHit)
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import kotlin.system.measureTimeMillis
|
|||||||
|
|
||||||
class StateSync {
|
class StateSync {
|
||||||
private val _authorizedDevices = FragmentedStorage.get<StringArrayStorage>("authorized_devices")
|
private val _authorizedDevices = FragmentedStorage.get<StringArrayStorage>("authorized_devices")
|
||||||
|
private val _nameStorage = FragmentedStorage.get<StringStringMapStorage>("sync_remembered_name_storage")
|
||||||
private val _syncKeyPair = FragmentedStorage.get<StringStorage>("sync_key_pair")
|
private val _syncKeyPair = FragmentedStorage.get<StringStorage>("sync_key_pair")
|
||||||
private val _lastAddressStorage = FragmentedStorage.get<StringStringMapStorage>("sync_last_address_storage")
|
private val _lastAddressStorage = FragmentedStorage.get<StringStringMapStorage>("sync_last_address_storage")
|
||||||
private val _syncSessionData = FragmentedStorage.get<StringTMapStorage<SyncSessionData>>("syncSessionData")
|
private val _syncSessionData = FragmentedStorage.get<StringTMapStorage<SyncSessionData>>("syncSessionData")
|
||||||
@@ -305,12 +306,22 @@ class StateSync {
|
|||||||
synchronized(_sessions) {
|
synchronized(_sessions) {
|
||||||
session = _sessions[s.remotePublicKey]
|
session = _sessions[s.remotePublicKey]
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
|
val remoteDeviceName = synchronized(_nameStorage) {
|
||||||
|
_nameStorage.get(remotePublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
session = SyncSession(remotePublicKey, onAuthorized = { it, isNewlyAuthorized, isNewSession ->
|
session = SyncSession(remotePublicKey, onAuthorized = { it, isNewlyAuthorized, isNewSession ->
|
||||||
if (!isNewSession) {
|
if (!isNewSession) {
|
||||||
return@SyncSession
|
return@SyncSession
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "${s.remotePublicKey} authorized")
|
it.remoteDeviceName?.let { remoteDeviceName ->
|
||||||
|
synchronized(_nameStorage) {
|
||||||
|
_nameStorage.setAndSave(remotePublicKey, remoteDeviceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "${s.remotePublicKey} authorized (name: ${it.displayName})")
|
||||||
synchronized(_lastAddressStorage) {
|
synchronized(_lastAddressStorage) {
|
||||||
_lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress)
|
_lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress)
|
||||||
}
|
}
|
||||||
@@ -341,7 +352,7 @@ class StateSync {
|
|||||||
|
|
||||||
deviceRemoved.emit(it.remotePublicKey)
|
deviceRemoved.emit(it.remotePublicKey)
|
||||||
|
|
||||||
})
|
}, remoteDeviceName)
|
||||||
_sessions[remotePublicKey] = session!!
|
_sessions[remotePublicKey] = session!!
|
||||||
}
|
}
|
||||||
session!!.addSocketSession(s)
|
session!!.addSocketSession(s)
|
||||||
@@ -469,6 +480,12 @@ class StateSync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCachedName(publicKey: String): String? {
|
||||||
|
return synchronized(_nameStorage) {
|
||||||
|
_nameStorage.get(publicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun delete(publicKey: String) {
|
suspend fun delete(publicKey: String) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
package com.futo.platformplayer.stores
|
|
||||||
|
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
|
||||||
class CachedPolycentricProfileStorage : FragmentedStorageFileJson() {
|
|
||||||
var map: HashMap<String, PolycentricCache.CachedPolycentricProfile> = hashMapOf();
|
|
||||||
|
|
||||||
override fun encode(): String {
|
|
||||||
val encoded = Json.encodeToString(this);
|
|
||||||
return encoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun get(key: String) : PolycentricCache.CachedPolycentricProfile? {
|
|
||||||
return map[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAndSave(key: String, value: PolycentricCache.CachedPolycentricProfile) : PolycentricCache.CachedPolycentricProfile {
|
|
||||||
map[key] = value;
|
|
||||||
save();
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAndSaveBlocking(key: String, value: PolycentricCache.CachedPolycentricProfile) : PolycentricCache.CachedPolycentricProfile {
|
|
||||||
map[key] = value;
|
|
||||||
saveBlocking();
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,6 +33,9 @@ class ManagedStore<T>{
|
|||||||
|
|
||||||
val className: String? get() = _class.classifier?.assume<KClass<*>>()?.simpleName;
|
val className: String? get() = _class.classifier?.assume<KClass<*>>()?.simpleName;
|
||||||
|
|
||||||
|
private var _onModificationCreate: ((T) -> Unit)? = null;
|
||||||
|
private var _onModificationDelete: ((T) -> Unit)? = null;
|
||||||
|
|
||||||
val name: String;
|
val name: String;
|
||||||
|
|
||||||
constructor(name: String, dir: File, clazz: KType, serializer: StoreSerializer<T>, niceName: String? = null) {
|
constructor(name: String, dir: File, clazz: KType, serializer: StoreSerializer<T>, niceName: String? = null) {
|
||||||
@@ -62,6 +65,12 @@ class ManagedStore<T>{
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun withOnModified(created: (T)->Unit, deleted: (T)->Unit): ManagedStore<T> {
|
||||||
|
_onModificationCreate = created;
|
||||||
|
_onModificationDelete = deleted;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
fun load(): ManagedStore<T> {
|
fun load(): ManagedStore<T> {
|
||||||
synchronized(_files) {
|
synchronized(_files) {
|
||||||
_files.clear();
|
_files.clear();
|
||||||
@@ -265,6 +274,7 @@ class ManagedStore<T>{
|
|||||||
file = saveNew(obj);
|
file = saveNew(obj);
|
||||||
if(_reconstructStore != null && (_reconstructStore!!.backupOnCreate || withReconstruction))
|
if(_reconstructStore != null && (_reconstructStore!!.backupOnCreate || withReconstruction))
|
||||||
saveReconstruction(file, obj);
|
saveReconstruction(file, obj);
|
||||||
|
_onModificationCreate?.invoke(obj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,6 +310,7 @@ class ManagedStore<T>{
|
|||||||
_files.remove(item);
|
_files.remove(item);
|
||||||
Logger.v(TAG, "Deleting file ${logName(file.id)}");
|
Logger.v(TAG, "Deleting file ${logName(file.id)}");
|
||||||
file.delete();
|
file.delete();
|
||||||
|
_onModificationDelete?.invoke(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.subscription
|
package com.futo.platformplayer.subscription
|
||||||
|
|
||||||
|
import SubsExchangeClient
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
@@ -15,8 +16,9 @@ class SmartSubscriptionAlgorithm(
|
|||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
allowFailure: Boolean = false,
|
allowFailure: Boolean = false,
|
||||||
withCacheFallback: Boolean = true,
|
withCacheFallback: Boolean = true,
|
||||||
threadPool: ForkJoinPool? = null
|
threadPool: ForkJoinPool? = null,
|
||||||
): SubscriptionsTaskFetchAlgorithm(scope, allowFailure, withCacheFallback, threadPool) {
|
subsExchangeClient: SubsExchangeClient? = null
|
||||||
|
): SubscriptionsTaskFetchAlgorithm(scope, allowFailure, withCacheFallback, threadPool, subsExchangeClient) {
|
||||||
override fun getSubscriptionTasks(subs: Map<Subscription, List<String>>): List<SubscriptionTask> {
|
override fun getSubscriptionTasks(subs: Map<Subscription, List<String>>): List<SubscriptionTask> {
|
||||||
val allTasks: List<SubscriptionTask> = subs.flatMap { entry ->
|
val allTasks: List<SubscriptionTask> = subs.flatMap { entry ->
|
||||||
val sub = entry.key;
|
val sub = entry.key;
|
||||||
|
|||||||
+3
-2
@@ -1,5 +1,6 @@
|
|||||||
package com.futo.platformplayer.subscription
|
package com.futo.platformplayer.subscription
|
||||||
|
|
||||||
|
import SubsExchangeClient
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
@@ -33,11 +34,11 @@ abstract class SubscriptionFetchAlgorithm(
|
|||||||
companion object {
|
companion object {
|
||||||
public val TAG = "SubscriptionAlgorithm";
|
public val TAG = "SubscriptionAlgorithm";
|
||||||
|
|
||||||
fun getAlgorithm(algo: SubscriptionFetchAlgorithms, scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = false, pool: ForkJoinPool? = null): SubscriptionFetchAlgorithm {
|
fun getAlgorithm(algo: SubscriptionFetchAlgorithms, scope: CoroutineScope, allowFailure: Boolean = false, withCacheFallback: Boolean = false, pool: ForkJoinPool? = null, withExchangeClient: SubsExchangeClient? = null): SubscriptionFetchAlgorithm {
|
||||||
return when(algo) {
|
return when(algo) {
|
||||||
SubscriptionFetchAlgorithms.CACHE -> CachedSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool, 50);
|
SubscriptionFetchAlgorithms.CACHE -> CachedSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool, 50);
|
||||||
SubscriptionFetchAlgorithms.SIMPLE -> SimpleSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
|
SubscriptionFetchAlgorithms.SIMPLE -> SimpleSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
|
||||||
SubscriptionFetchAlgorithms.SMART -> SmartSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
|
SubscriptionFetchAlgorithms.SMART -> SmartSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool, withExchangeClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-3
@@ -1,19 +1,23 @@
|
|||||||
package com.futo.platformplayer.subscription
|
package com.futo.platformplayer.subscription
|
||||||
|
|
||||||
|
import SubsExchangeClient
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.structures.DedupContentPager
|
import com.futo.platformplayer.api.media.structures.DedupContentPager
|
||||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||||
|
import com.futo.platformplayer.api.media.structures.PlatformContentPager
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptException
|
|
||||||
import com.futo.platformplayer.exceptions.ChannelException
|
import com.futo.platformplayer.exceptions.ChannelException
|
||||||
import com.futo.platformplayer.findNonRuntimeException
|
import com.futo.platformplayer.findNonRuntimeException
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
|
||||||
@@ -24,6 +28,9 @@ import com.futo.platformplayer.states.StateCache
|
|||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
|
import com.futo.platformplayer.subsexchange.ChannelRequest
|
||||||
|
import com.futo.platformplayer.subsexchange.ChannelResolve
|
||||||
|
import com.futo.platformplayer.subsexchange.ExchangeContract
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
@@ -35,7 +42,8 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
allowFailure: Boolean = false,
|
allowFailure: Boolean = false,
|
||||||
withCacheFallback: Boolean = true,
|
withCacheFallback: Boolean = true,
|
||||||
_threadPool: ForkJoinPool? = null
|
_threadPool: ForkJoinPool? = null,
|
||||||
|
private val subsExchangeClient: SubsExchangeClient? = null
|
||||||
) : SubscriptionFetchAlgorithm(scope, allowFailure, withCacheFallback, _threadPool) {
|
) : SubscriptionFetchAlgorithm(scope, allowFailure, withCacheFallback, _threadPool) {
|
||||||
|
|
||||||
|
|
||||||
@@ -45,7 +53,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
|
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
|
||||||
val tasks = getSubscriptionTasks(subs);
|
var tasks = getSubscriptionTasks(subs).toMutableList()
|
||||||
|
|
||||||
val tasksGrouped = tasks.groupBy { it.client }
|
val tasksGrouped = tasks.groupBy { it.client }
|
||||||
|
|
||||||
@@ -70,6 +78,32 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
|
|
||||||
val exs: ArrayList<Throwable> = arrayListOf();
|
val exs: ArrayList<Throwable> = arrayListOf();
|
||||||
|
|
||||||
|
var contract: ExchangeContract? = null;
|
||||||
|
var providedTasks: MutableList<SubscriptionTask>? = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
val contractableTasks =
|
||||||
|
tasks.filter { !it.fromPeek && !it.fromCache && (it.type == ResultCapabilities.TYPE_VIDEOS || it.type == ResultCapabilities.TYPE_MIXED) };
|
||||||
|
contract =
|
||||||
|
if (contractableTasks.size > 10) subsExchangeClient?.requestContract(*contractableTasks.map {
|
||||||
|
ChannelRequest(it.url)
|
||||||
|
}.toTypedArray()) else null;
|
||||||
|
if (contract?.provided?.isNotEmpty() == true)
|
||||||
|
Logger.i(TAG, "Received subscription exchange contract (Requires ${contract?.required?.size}, Provides ${contract?.provided?.size}), ID: ${contract?.id}");
|
||||||
|
if (contract != null && contract.required.isNotEmpty()) {
|
||||||
|
providedTasks = mutableListOf()
|
||||||
|
for (task in tasks.toList()) {
|
||||||
|
if (!task.fromCache && !task.fromPeek && contract.provided.contains(task.url)) {
|
||||||
|
providedTasks.add(task);
|
||||||
|
tasks.remove(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable){
|
||||||
|
Logger.e("SubscriptionsTaskFetchAlgorithm", "Failed to retrieve SubsExchange contract due to: " + ex.message, ex);
|
||||||
|
}
|
||||||
|
|
||||||
val failedPlugins = mutableListOf<String>();
|
val failedPlugins = mutableListOf<String>();
|
||||||
val cachedChannels = mutableListOf<String>()
|
val cachedChannels = mutableListOf<String>()
|
||||||
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
|
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
|
||||||
@@ -104,6 +138,42 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Resolve Subscription Exchange
|
||||||
|
if(contract != null) {
|
||||||
|
try {
|
||||||
|
val resolves = taskResults.filter { it.pager != null && (it.task.type == ResultCapabilities.TYPE_MIXED || it.task.type == ResultCapabilities.TYPE_VIDEOS) && contract.required.contains(it.task.url) }.map {
|
||||||
|
ChannelResolve(
|
||||||
|
it.task.url,
|
||||||
|
it.pager!!.getResults().filter { it is IPlatformVideo }.map { SerializedPlatformVideo.fromVideo(it as IPlatformVideo) }
|
||||||
|
)
|
||||||
|
}.toTypedArray()
|
||||||
|
val resolve = subsExchangeClient?.resolveContract(
|
||||||
|
contract,
|
||||||
|
*resolves
|
||||||
|
);
|
||||||
|
if (resolve != null) {
|
||||||
|
UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size})")
|
||||||
|
for(result in resolve){
|
||||||
|
val task = providedTasks?.find { it.url == result.channelUrl };
|
||||||
|
if(task != null) {
|
||||||
|
taskResults.add(SubscriptionTaskResult(task, PlatformContentPager(result.content, result.content.size), null));
|
||||||
|
providedTasks?.remove(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (providedTasks != null) {
|
||||||
|
for(task in providedTasks) {
|
||||||
|
taskResults.add(SubscriptionTaskResult(task, null, IllegalStateException("No data received from exchange")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
//TODO: fetch remainder after all?
|
||||||
|
Logger.e(TAG, "Failed to resolve Subscription Exchange contract due to: " + ex.message, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms")
|
Logger.i("StateSubscriptions", "Subscriptions results in ${timeTotal}ms")
|
||||||
|
|
||||||
//Cache pagers grouped by channel
|
//Cache pagers grouped by channel
|
||||||
@@ -173,6 +243,8 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
Logger.e(StateSubscriptions.TAG, "Subscription peek [${task.sub.channel.name}] failed", ex);
|
Logger.e(StateSubscriptions.TAG, "Subscription peek [${task.sub.channel.name}] failed", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Intercepts task.fromCache & task.fromPeek
|
||||||
synchronized(cachedChannels) {
|
synchronized(cachedChannels) {
|
||||||
if(task.fromCache || task.fromPeek) {
|
if(task.fromCache || task.fromPeek) {
|
||||||
finished++;
|
finished++;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user