mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d64003d1c | |||
| 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 |
+7
-1
@@ -83,8 +83,14 @@
|
||||
path = app/src/stable/assets/sources/dailymotion
|
||||
url = ../plugins/dailymotion.git
|
||||
[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
|
||||
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
|
||||
path = app/src/unstable/assets/sources/apple-podcasts
|
||||
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 'com.google.android.flexbox:flexbox:3.0.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 'com.github.dhaval2404:imagepicker:2.1'
|
||||
implementation 'com.google.zxing:core:3.4.1'
|
||||
|
||||
@@ -156,7 +156,6 @@
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.DeveloperActivity"
|
||||
|
||||
@@ -263,6 +263,10 @@ class PlatformVideoDetails extends PlatformVideo {
|
||||
this.rating = obj.rating ?? null; //IRating
|
||||
this.subtitles = obj.subtitles ?? [];
|
||||
this.isShort = !!obj.isShort ?? false;
|
||||
|
||||
if (obj.getContentRecommendations) {
|
||||
this.getContentRecommendations = obj.getContentRecommendations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.futo.platformplayer
|
||||
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.AnnouncementType
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.polycentric.core.ProcessHandle
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.SystemState
|
||||
import com.futo.polycentric.core.base64UrlToByteArray
|
||||
import userpackage.Protocol
|
||||
import kotlin.math.abs
|
||||
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) }
|
||||
}
|
||||
|
||||
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? {
|
||||
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||
}
|
||||
|
||||
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -205,7 +205,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
var home = HomeSettings();
|
||||
@Serializable
|
||||
class HomeSettings {
|
||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5)
|
||||
@FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 3)
|
||||
@DropdownFieldOptionsId(R.array.feed_style)
|
||||
var homeFeedStyle: Int = 1;
|
||||
|
||||
@@ -216,6 +216,9 @@ class Settings : FragmentedStorageFileJson() {
|
||||
return FeedStyle.THUMBNAIL;
|
||||
}
|
||||
|
||||
@FormField(R.string.show_home_filters, FieldForm.TOGGLE, R.string.show_home_filters_description, 4)
|
||||
var showHomeFilters: Boolean = true;
|
||||
|
||||
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
|
||||
var previewFeedItems: Boolean = true;
|
||||
|
||||
@@ -294,6 +297,9 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
|
||||
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)
|
||||
var previewFeedItems: Boolean = true;
|
||||
|
||||
@@ -356,7 +362,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||
var playback = PlaybackSettings();
|
||||
@Serializable
|
||||
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)
|
||||
var primaryLanguage: Int = 0;
|
||||
|
||||
@@ -380,6 +386,8 @@ class Settings : FragmentedStorageFileJson() {
|
||||
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];
|
||||
|
||||
@@ -644,6 +652,9 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@Serializable
|
||||
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)
|
||||
var clearCookiesOnLogout: Boolean = true;
|
||||
|
||||
|
||||
@@ -79,6 +79,36 @@ class UISlideOverlays {
|
||||
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 {
|
||||
val items = arrayListOf<View>();
|
||||
|
||||
@@ -372,7 +402,7 @@ class UISlideOverlays {
|
||||
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
||||
slideUpMenuOverlay.hide()
|
||||
} 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")
|
||||
slideUpMenuOverlay.hide()
|
||||
} else {
|
||||
@@ -1045,8 +1075,9 @@ class UISlideOverlays {
|
||||
StatePlayer.TYPE_WATCHLATER,
|
||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||
tag = "watch later",
|
||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true);
|
||||
UIDialogs.appToast("Added to watch later", false);
|
||||
call = {
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
|
||||
UIDialogs.appToast("Added to watch later", false);
|
||||
}),
|
||||
)
|
||||
);
|
||||
@@ -1117,7 +1148,7 @@ class UISlideOverlays {
|
||||
container.context.getString(R.string.decide_which_buttons_should_be_pinned),
|
||||
tag = "",
|
||||
call = {
|
||||
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }) {
|
||||
showOrderOverlay(container, container.context.getString(R.string.select_your_pins_in_order), (visible + hidden).map { Pair(it.text.text.toString(), it.tagRef!!) }, {
|
||||
val selected = it
|
||||
.map { x -> visible.find { it.tagRef == x } ?: hidden.find { it.tagRef == x } }
|
||||
.filter { it != null }
|
||||
@@ -1125,7 +1156,7 @@ class UISlideOverlays {
|
||||
.toList();
|
||||
|
||||
onPinnedbuttons?.invoke(selected + (visible + hidden).filter { !selected.contains(it) });
|
||||
}
|
||||
});
|
||||
},
|
||||
invokeParent = false
|
||||
))
|
||||
@@ -1133,29 +1164,40 @@ class UISlideOverlays {
|
||||
|
||||
return SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.more_options), null, true, *views).apply { show() };
|
||||
}
|
||||
|
||||
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit) {
|
||||
fun showOrderOverlay(container: ViewGroup, title: String, options: List<Pair<String, Any>>, onOrdered: (List<Any>)->Unit, description: String? = null) {
|
||||
val selection: MutableList<Any> = mutableListOf();
|
||||
|
||||
var overlay: SlideUpMenuOverlay? = null;
|
||||
|
||||
overlay = SlideUpMenuOverlay(container.context, container, title, container.context.getString(R.string.save), true,
|
||||
options.map { SlideUpMenuItem(
|
||||
listOf(
|
||||
if(!description.isNullOrEmpty()) SlideUpMenuGroup(container.context, "", description, "", listOf()) else null,
|
||||
).filterNotNull() +
|
||||
(options.map { SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_move_up,
|
||||
it.first,
|
||||
"",
|
||||
tag = it.second,
|
||||
call = {
|
||||
val overlayItem = overlay?.getSlideUpItemByTag(it.second);
|
||||
if(overlay!!.selectOption(null, it.second, true, true)) {
|
||||
if(!selection.contains(it.second))
|
||||
if(!selection.contains(it.second)) {
|
||||
selection.add(it.second);
|
||||
} else
|
||||
if(overlayItem != null) {
|
||||
overlayItem.setSubText(selection.indexOf(it.second).toString());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selection.remove(it.second);
|
||||
if(overlayItem != null) {
|
||||
overlayItem.setSubText("");
|
||||
}
|
||||
}
|
||||
},
|
||||
invokeParent = false
|
||||
)
|
||||
});
|
||||
}));
|
||||
overlay.onOK.subscribe {
|
||||
onOrdered.invoke(selection);
|
||||
overlay.hide();
|
||||
|
||||
@@ -10,11 +10,13 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
|
||||
class AddSourceOptionsActivity : AppCompatActivity() {
|
||||
lateinit var _buttonBack: ImageButton;
|
||||
|
||||
lateinit var _overlayContainer: FrameLayout;
|
||||
lateinit var _buttonQR: BigButton;
|
||||
lateinit var _buttonBrowse: BigButton;
|
||||
lateinit var _buttonURL: BigButton;
|
||||
@@ -54,6 +56,7 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
||||
setContentView(R.layout.activity_add_source_options);
|
||||
setNavigationBarColorAndIcons();
|
||||
|
||||
_overlayContainer = findViewById(R.id.overlay_container);
|
||||
_buttonBack = findViewById(R.id.button_back);
|
||||
|
||||
_buttonQR = findViewById(R.id.option_qr);
|
||||
@@ -81,7 +84,25 @@ class AddSourceOptionsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
_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 {
|
||||
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;
|
||||
|
||||
|
||||
+3
-3
@@ -11,16 +11,16 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.ProcessHandle
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -87,7 +87,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
||||
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
|
||||
}
|
||||
|
||||
processHandle.addServer(PolycentricCache.SERVER);
|
||||
processHandle.addServer(ApiMethods.SERVER);
|
||||
processHandle.setUsername(username);
|
||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||
} catch (e: Throwable) {
|
||||
|
||||
+2
-2
@@ -12,12 +12,12 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.KeyPair
|
||||
import com.futo.polycentric.core.Process
|
||||
import com.futo.polycentric.core.ProcessSecret
|
||||
@@ -145,7 +145,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
StatePolycentric.instance.setProcessHandle(processHandle);
|
||||
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
|
||||
processHandle.fullyBackfillClient(ApiMethods.SERVER);
|
||||
withContext(Dispatchers.Main) {
|
||||
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
|
||||
finish();
|
||||
|
||||
@@ -21,10 +21,8 @@ import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
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.views.buttons.BigButton
|
||||
import com.futo.platformplayer.views.overlays.LoaderOverlay
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.SystemState
|
||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||
import com.futo.polycentric.core.toBase64Url
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
@@ -145,7 +145,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
|
||||
processHandle.fullyBackfillClient(ApiMethods.SERVER)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
updateUI();
|
||||
|
||||
@@ -101,7 +101,8 @@ class SyncHomeActivity : AppCompatActivity() {
|
||||
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
|
||||
val connected = session?.connected ?: false
|
||||
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")
|
||||
return syncDeviceView
|
||||
}
|
||||
|
||||
+2
@@ -2,6 +2,8 @@ package com.futo.platformplayer.api.media.models.contents
|
||||
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
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.downloads.VideoLocal
|
||||
|
||||
class LocalVideoMuxedSourceDescriptor(
|
||||
class DownloadedVideoMuxedSourceDescriptor(
|
||||
private val video: VideoLocal
|
||||
) : VideoMuxedSourceDescriptor() {
|
||||
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
|
||||
+5
-2
@@ -13,7 +13,8 @@ class AudioUrlSource(
|
||||
override val codec: String = "",
|
||||
override val language: String = Language.UNKNOWN,
|
||||
override val duration: Long? = null,
|
||||
override var priority: Boolean = false
|
||||
override var priority: Boolean = false,
|
||||
override var original: Boolean = false
|
||||
) : IAudioUrlSource, IStreamMetaDataSource{
|
||||
override var streamMetaData: StreamMetaData? = null;
|
||||
|
||||
@@ -36,7 +37,9 @@ class AudioUrlSource(
|
||||
source.container,
|
||||
source.codec,
|
||||
source.language,
|
||||
source.duration
|
||||
source.duration,
|
||||
source.priority,
|
||||
source.original
|
||||
);
|
||||
ret.streamMetaData = streamData;
|
||||
|
||||
|
||||
+1
@@ -27,6 +27,7 @@ class HLSVariantAudioUrlSource(
|
||||
override val language: String,
|
||||
override val duration: Long?,
|
||||
override val priority: Boolean,
|
||||
override val original: Boolean,
|
||||
val url: String
|
||||
) : IAudioUrlSource {
|
||||
override fun getAudioUrl(): String {
|
||||
|
||||
+1
@@ -8,4 +8,5 @@ interface IAudioSource {
|
||||
val language : String;
|
||||
val duration : Long?;
|
||||
val priority: Boolean;
|
||||
val original: Boolean;
|
||||
}
|
||||
+1
@@ -15,6 +15,7 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
||||
override val duration: Long? = null;
|
||||
|
||||
override var priority: Boolean = false;
|
||||
override val original: Boolean = false;
|
||||
|
||||
val filePath : String;
|
||||
val fileSize: Long;
|
||||
|
||||
+4
-1
@@ -10,15 +10,18 @@ import com.futo.polycentric.core.combineHashCodes
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
open class SerializedPlatformVideo(
|
||||
override val contentType: ContentType = ContentType.MEDIA,
|
||||
override val id: PlatformID,
|
||||
override val name: String,
|
||||
override val thumbnails: Thumbnails,
|
||||
override val author: PlatformAuthorLink,
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
@JsonNames("datetime", "dateTime")
|
||||
override val datetime: OffsetDateTime? = null,
|
||||
override val url: String,
|
||||
override val shareUrl: String = "",
|
||||
@@ -27,7 +30,6 @@ open class SerializedPlatformVideo(
|
||||
override val viewCount: Long,
|
||||
override val isShort: Boolean = false
|
||||
) : IPlatformVideo, SerializedPlatformContent {
|
||||
override val contentType: ContentType = ContentType.MEDIA;
|
||||
|
||||
override val isLive: Boolean = false;
|
||||
|
||||
@@ -44,6 +46,7 @@ open class SerializedPlatformVideo(
|
||||
companion object {
|
||||
fun fromVideo(video: IPlatformVideo) : SerializedPlatformVideo {
|
||||
return SerializedPlatformVideo(
|
||||
ContentType.MEDIA,
|
||||
video.id,
|
||||
video.name,
|
||||
video.thumbnails,
|
||||
|
||||
+3
@@ -21,6 +21,8 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
|
||||
|
||||
override var priority: Boolean = false;
|
||||
|
||||
override var original: Boolean = false;
|
||||
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
|
||||
val contextName = "AudioUrlSource";
|
||||
val config = plugin.config;
|
||||
@@ -35,6 +37,7 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
|
||||
name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}";
|
||||
|
||||
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 {
|
||||
|
||||
+2
@@ -23,6 +23,7 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||
override val bitrate: Int;
|
||||
override val duration: Long;
|
||||
override val priority: Boolean;
|
||||
override var original: Boolean = false;
|
||||
|
||||
override val language: String;
|
||||
|
||||
@@ -45,6 +46,7 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
||||
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -21,6 +21,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
||||
override val language: String;
|
||||
|
||||
override var priority: Boolean = false;
|
||||
override var original: Boolean = false;
|
||||
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
|
||||
val contextName = "HLSAudioSource";
|
||||
@@ -32,6 +33,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
||||
language = _obj.getOrThrow(config, "language", contextName);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import com.futo.platformplayer.constructs.Event1
|
||||
* A RefreshPager represents a pager that can be modified overtime (eg. By getting more results later, by recreating the pager)
|
||||
* When the onPagerChanged event is emitted, a new pager instance is passed, or requested via getCurrentPager
|
||||
*/
|
||||
interface IRefreshPager<T> {
|
||||
interface IRefreshPager<T>: IPager<T> {
|
||||
val onPagerChanged: Event1<IPager<T>>;
|
||||
val onPagerError: Event1<Throwable>;
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.futo.platformplayer.api.media.structures
|
||||
|
||||
import com.futo.platformplayer.api.media.structures.ReusablePager.Window
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
|
||||
/**
|
||||
@@ -9,8 +11,8 @@ import com.futo.platformplayer.logging.Logger
|
||||
* A "Window" is effectively a pager that just reads previous results from the shared results, but when the end is reached, it will call nextPage on the parent if possible for new results.
|
||||
* This allows multiple Windows to exist of the same pager, without messing with position, or duplicate requests
|
||||
*/
|
||||
class ReusablePager<T>: INestedPager<T>, IPager<T> {
|
||||
private val _pager: IPager<T>;
|
||||
open class ReusablePager<T>: INestedPager<T>, IReusablePager<T> {
|
||||
protected var _pager: IPager<T>;
|
||||
val previousResults = arrayListOf<T>();
|
||||
|
||||
constructor(subPager: IPager<T>) {
|
||||
@@ -44,7 +46,7 @@ class ReusablePager<T>: INestedPager<T>, IPager<T> {
|
||||
return previousResults;
|
||||
}
|
||||
|
||||
fun getWindow(): Window<T> {
|
||||
override fun getWindow(): Window<T> {
|
||||
return Window(this);
|
||||
}
|
||||
|
||||
@@ -95,4 +97,118 @@ class ReusablePager<T>: INestedPager<T>, IPager<T> {
|
||||
return ReusablePager(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class ReusableRefreshPager<T>: INestedPager<T>, IReusablePager<T> {
|
||||
protected var _pager: IRefreshPager<T>;
|
||||
val previousResults = arrayListOf<T>();
|
||||
|
||||
private var _currentPage: IPager<T>;
|
||||
|
||||
|
||||
val onPagerChanged = Event1<IPager<T>>()
|
||||
val onPagerError = Event1<Throwable>()
|
||||
|
||||
constructor(subPager: IRefreshPager<T>) {
|
||||
this._pager = subPager;
|
||||
_currentPage = this;
|
||||
synchronized(previousResults) {
|
||||
previousResults.addAll(subPager.getResults());
|
||||
}
|
||||
_pager.onPagerError.subscribe(onPagerError::emit);
|
||||
_pager.onPagerChanged.subscribe {
|
||||
_currentPage = it;
|
||||
synchronized(previousResults) {
|
||||
previousResults.clear();
|
||||
previousResults.addAll(it.getResults());
|
||||
}
|
||||
|
||||
onPagerChanged.emit(_currentPage);
|
||||
};
|
||||
}
|
||||
|
||||
override fun findPager(query: (IPager<T>) -> Boolean): IPager<T>? {
|
||||
if(query(_pager))
|
||||
return _pager;
|
||||
else if(_pager is INestedPager<*>)
|
||||
return (_pager as INestedPager<T>).findPager(query);
|
||||
return null;
|
||||
}
|
||||
|
||||
override fun hasMorePages(): Boolean {
|
||||
return _pager.hasMorePages();
|
||||
}
|
||||
|
||||
override fun nextPage() {
|
||||
_pager.nextPage();
|
||||
}
|
||||
|
||||
override fun getResults(): List<T> {
|
||||
val results = _pager.getResults();
|
||||
synchronized(previousResults) {
|
||||
previousResults.addAll(results);
|
||||
}
|
||||
return previousResults;
|
||||
}
|
||||
|
||||
override fun getWindow(): RefreshWindow<T> {
|
||||
return RefreshWindow(this);
|
||||
}
|
||||
|
||||
|
||||
class RefreshWindow<T>: IPager<T>, INestedPager<T>, IRefreshPager<T> {
|
||||
private val _parent: ReusableRefreshPager<T>;
|
||||
private var _position: Int = 0;
|
||||
private var _read: Int = 0;
|
||||
|
||||
private var _currentResults: List<T>;
|
||||
|
||||
override val onPagerChanged = Event1<IPager<T>>();
|
||||
override val onPagerError = Event1<Throwable>();
|
||||
|
||||
|
||||
override fun getCurrentPager(): IPager<T> {
|
||||
return _parent.getWindow();
|
||||
}
|
||||
|
||||
constructor(parent: ReusableRefreshPager<T>) {
|
||||
_parent = parent;
|
||||
|
||||
synchronized(_parent.previousResults) {
|
||||
_currentResults = _parent.previousResults.toList();
|
||||
_read += _currentResults.size;
|
||||
}
|
||||
parent.onPagerChanged.subscribe(onPagerChanged::emit);
|
||||
parent.onPagerError.subscribe(onPagerError::emit);
|
||||
}
|
||||
|
||||
|
||||
override fun hasMorePages(): Boolean {
|
||||
return _parent.previousResults.size > _read || _parent.hasMorePages();
|
||||
}
|
||||
|
||||
override fun nextPage() {
|
||||
synchronized(_parent.previousResults) {
|
||||
if (_parent.previousResults.size <= _read) {
|
||||
_parent.nextPage();
|
||||
_parent.getResults();
|
||||
}
|
||||
_currentResults = _parent.previousResults.drop(_read).toList();
|
||||
_read += _currentResults.size;
|
||||
}
|
||||
}
|
||||
|
||||
override fun getResults(): List<T> {
|
||||
return _currentResults;
|
||||
}
|
||||
|
||||
override fun findPager(query: (IPager<T>) -> Boolean): IPager<T>? {
|
||||
return _parent.findPager(query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IReusablePager<T>: IPager<T> {
|
||||
fun getWindow(): IPager<T>;
|
||||
}
|
||||
@@ -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.constructs.Event1
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
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.Store
|
||||
import com.futo.polycentric.core.SystemState
|
||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
import com.google.android.material.button.MaterialButton
|
||||
|
||||
@@ -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.ratings.IRating
|
||||
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.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
@@ -57,7 +57,7 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
|
||||
override val video: IVideoSourceDescriptor get() = if(audioSource.isNotEmpty())
|
||||
LocalVideoUnMuxedSourceDescriptor(this)
|
||||
else
|
||||
LocalVideoMuxedSourceDescriptor(this);
|
||||
DownloadedVideoMuxedSourceDescriptor(this);
|
||||
override val preview: IVideoSourceDescriptor? get() = videoSerialized.preview;
|
||||
|
||||
override val live: IVideoSource? get() = videoSerialized.live;
|
||||
|
||||
+2
-4
@@ -13,7 +13,6 @@ import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.fixHtmlLinks
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.resolveChannelUrl
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
@@ -21,6 +20,7 @@ import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.toHumanNumber
|
||||
import com.futo.platformplayer.views.platform.PlatformLinkView
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
import com.futo.polycentric.core.toName
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
|
||||
@@ -134,9 +134,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
|
||||
}
|
||||
}
|
||||
if(!map.containsKey("Harbor"))
|
||||
this.context?.let {
|
||||
map.set("Harbor", polycentricProfile.getHarborUrl(it));
|
||||
}
|
||||
map.set("Harbor", polycentricProfile.getHarborUrl());
|
||||
|
||||
if (map.isNotEmpty())
|
||||
setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "")
|
||||
|
||||
+1
-1
@@ -29,7 +29,6 @@ import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.exceptions.ChannelException
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateCache
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
@@ -39,6 +38,7 @@ import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.max
|
||||
|
||||
+1
-1
@@ -16,12 +16,12 @@ import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
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.resolveChannelUrl
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
import com.futo.platformplayer.views.adapters.viewholders.CreatorViewHolder
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
|
||||
class ChannelListFragment : Fragment, IChannelTabFragment {
|
||||
private var _channels: ArrayList<IPlatformChannel> = arrayListOf();
|
||||
|
||||
+1
-1
@@ -8,8 +8,8 @@ import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.futo.platformplayer.R
|
||||
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.polycentric.core.PolycentricProfile
|
||||
|
||||
|
||||
class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
package com.futo.platformplayer.fragment.channel.tab
|
||||
|
||||
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 {
|
||||
fun setChannel(channel: IPlatformChannel)
|
||||
|
||||
+13
-45
@@ -42,7 +42,6 @@ import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.SearchType
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.selectHighestResolutionImage
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
@@ -55,29 +54,14 @@ import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||
import com.futo.platformplayer.views.subscriptions.SubscribeButton
|
||||
import com.futo.polycentric.core.OwnedClaim
|
||||
import com.futo.polycentric.core.PublicKey
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.SystemState
|
||||
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
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() {
|
||||
override val isMainView: Boolean = true
|
||||
@@ -144,15 +128,14 @@ class ChannelFragment : MainFragment() {
|
||||
|
||||
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>
|
||||
|
||||
init {
|
||||
inflater.inflate(R.layout.fragment_channel, this)
|
||||
_taskLoadPolycentricProfile =
|
||||
TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>({ fragment.lifecycleScope },
|
||||
_taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>({ fragment.lifecycleScope },
|
||||
{ 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> {
|
||||
Logger.w(TAG, "Failed to load polycentric profile.", it)
|
||||
}
|
||||
@@ -238,8 +221,8 @@ class ChannelFragment : MainFragment() {
|
||||
}
|
||||
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
||||
if (content is IPlatformVideo) {
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)
|
||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
|
||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||
}
|
||||
}
|
||||
adapter.onUrlClicked.subscribe { url ->
|
||||
@@ -328,7 +311,7 @@ class ChannelFragment : MainFragment() {
|
||||
_creatorThumbnail.setThumbnail(parameter.thumbnail, true)
|
||||
Glide.with(_imageBanner).clear(_imageBanner)
|
||||
|
||||
loadPolycentricProfile(parameter.id, parameter.url)
|
||||
loadPolycentricProfile(parameter.id)
|
||||
}
|
||||
|
||||
_url = parameter.url
|
||||
@@ -342,7 +325,7 @@ class ChannelFragment : MainFragment() {
|
||||
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true)
|
||||
Glide.with(_imageBanner).clear(_imageBanner)
|
||||
|
||||
loadPolycentricProfile(parameter.channel.id, parameter.channel.url)
|
||||
loadPolycentricProfile(parameter.channel.id)
|
||||
}
|
||||
|
||||
_url = parameter.channel.url
|
||||
@@ -359,16 +342,8 @@ class ChannelFragment : MainFragment() {
|
||||
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
|
||||
}
|
||||
|
||||
private fun loadPolycentricProfile(id: PlatformID, url: String) {
|
||||
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true)
|
||||
if (cachedPolycentricProfile != null) {
|
||||
setPolycentricProfile(cachedPolycentricProfile, animate = true)
|
||||
if (cachedPolycentricProfile.expired) {
|
||||
_taskLoadPolycentricProfile.run(id)
|
||||
}
|
||||
} else {
|
||||
_taskLoadPolycentricProfile.run(id)
|
||||
}
|
||||
private fun loadPolycentricProfile(id: PlatformID) {
|
||||
_taskLoadPolycentricProfile.run(id)
|
||||
}
|
||||
|
||||
private fun setLoading(isLoading: Boolean) {
|
||||
@@ -533,20 +508,13 @@ class ChannelFragment : MainFragment() {
|
||||
|
||||
private fun setPolycentricProfileOr(url: String, or: () -> Unit) {
|
||||
setPolycentricProfile(null, animate = false)
|
||||
|
||||
val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(url) }
|
||||
if (cachedProfile != null) {
|
||||
setPolycentricProfile(cachedProfile, animate = false)
|
||||
} else {
|
||||
or()
|
||||
}
|
||||
or()
|
||||
}
|
||||
|
||||
private fun setPolycentricProfile(
|
||||
cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean
|
||||
profile: PolycentricProfile?, animate: Boolean
|
||||
) {
|
||||
val dp35 = 35.dp(resources)
|
||||
val profile = cachedPolycentricProfile?.profile
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)?.let {
|
||||
it.toURLInfoSystemLinkUrl(
|
||||
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.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
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.overlays.RepliesOverlay
|
||||
import com.futo.polycentric.core.PublicKey
|
||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.UnknownHostException
|
||||
|
||||
+2
-2
@@ -82,8 +82,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
};
|
||||
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
}
|
||||
};
|
||||
adapter.onLongPress.subscribe(this) {
|
||||
|
||||
+9
-2
@@ -9,6 +9,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
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.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
@@ -160,8 +161,14 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||
navigate<RemotePlaylistFragment>(it);
|
||||
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
||||
navigate<ChannelFragment>(it);
|
||||
else
|
||||
navigate<VideoDetailFragment>(it);
|
||||
else {
|
||||
val url = it;
|
||||
activity?.let {
|
||||
close()
|
||||
if(it is MainActivity)
|
||||
it.navigate(it.getFragment<VideoDetailFragment>(), url);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
setQuery(it, true);
|
||||
|
||||
+8
-1
@@ -10,6 +10,7 @@ import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -26,6 +27,7 @@ class CreatorsFragment : MainFragment() {
|
||||
private var _overlayContainer: FrameLayout? = null;
|
||||
private var _containerSearch: FrameLayout? = null;
|
||||
private var _editSearch: EditText? = null;
|
||||
private var _textMeta: TextView? = null;
|
||||
private var _buttonClearSearch: ImageButton? = null
|
||||
|
||||
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 buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search)
|
||||
_editSearch = editSearch
|
||||
_textMeta = view.findViewById(R.id.text_meta);
|
||||
_buttonClearSearch = buttonClearSearch
|
||||
buttonClearSearch.setOnClickListener {
|
||||
editSearch.text.clear()
|
||||
@@ -41,7 +44,11 @@ class CreatorsFragment : MainFragment() {
|
||||
_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.onSettings.subscribe { sub -> _overlayContainer?.let { UISlideOverlays.showSubscriptionOptionsOverlay(sub, it) } }
|
||||
|
||||
|
||||
+3
-2
@@ -22,6 +22,7 @@ import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.toHumanBytesSize
|
||||
import com.futo.platformplayer.toHumanDuration
|
||||
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
||||
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
||||
import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder
|
||||
@@ -215,7 +216,7 @@ class DownloadsFragment : MainFragment() {
|
||||
_listDownloadedHeader.visibility = GONE;
|
||||
} else {
|
||||
_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 ""})";
|
||||
}
|
||||
|
||||
lastDownloads = downloaded;
|
||||
@@ -228,7 +229,7 @@ class DownloadsFragment : MainFragment() {
|
||||
fun filterDownloads(vids: List<VideoLocal>): List<VideoLocal>{
|
||||
var vidsToReturn = vids;
|
||||
if(!_listDownloadSearch.text.isNullOrEmpty())
|
||||
vidsToReturn = vids.filter { it.name.contains(_listDownloadSearch.text, true) };
|
||||
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 };
|
||||
|
||||
@@ -3,12 +3,15 @@ package com.futo.platformplayer.fragment.mainactivity.main
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Display
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.LayoutManager
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
@@ -20,6 +23,7 @@ import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.others.ProgressBar
|
||||
import com.futo.platformplayer.views.others.TagsView
|
||||
@@ -28,7 +32,9 @@ import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||
import com.futo.platformplayer.views.announcements.AnnouncementView
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.math.max
|
||||
|
||||
@@ -68,6 +74,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
|
||||
private val _scrollListener: RecyclerView.OnScrollListener;
|
||||
private var _automaticNextPageCounter = 0;
|
||||
private val _automaticBackoff = arrayOf(0, 500, 1000, 1000, 2000, 5000, 5000, 5000);
|
||||
|
||||
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, GridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>? = null) : super(inflater.context) {
|
||||
this.fragment = fragment;
|
||||
@@ -129,6 +136,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
_toolbarContentView = findViewById(R.id.container_toolbar_content);
|
||||
|
||||
_nextPageHandler = TaskHandler<TPager, List<TResult>>({fragment.lifecycleScope}, {
|
||||
|
||||
if (it is IAsyncPager<*>)
|
||||
it.nextPageAsync();
|
||||
else
|
||||
@@ -182,26 +190,53 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
||||
|
||||
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
|
||||
val canScroll = if (recyclerData.results.isEmpty()) false else {
|
||||
val height = resources.displayMetrics.heightPixels;
|
||||
|
||||
val layoutManager = recyclerData.layoutManager
|
||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||
|
||||
if (firstVisibleItemPosition != RecyclerView.NO_POSITION) {
|
||||
val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition)
|
||||
val itemHeight = firstVisibleView?.height ?: 0
|
||||
val occupiedSpace = recyclerData.results.size / recyclerData.layoutManager.spanCount * itemHeight
|
||||
val recyclerViewHeight = _recyclerResults.height
|
||||
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage occupiedSpace=$occupiedSpace recyclerViewHeight=$recyclerViewHeight")
|
||||
occupiedSpace >= recyclerViewHeight
|
||||
val firstVisibleItemView = if(firstVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(firstVisibleItemPosition) else null;
|
||||
val lastVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition();
|
||||
val lastVisibleItemView = if(lastVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(lastVisibleItemPosition) else null;
|
||||
if(lastVisibleItemView != null && lastVisibleItemPosition == (recyclerData.results.size - 1)) {
|
||||
false;
|
||||
}
|
||||
else if (firstVisibleItemView != null && height != null && firstVisibleItemView.height * recyclerData.results.size < height) {
|
||||
false;
|
||||
} else {
|
||||
false
|
||||
true;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter")
|
||||
if (!canScroll || filteredResults.isEmpty()) {
|
||||
_automaticNextPageCounter++
|
||||
if(_automaticNextPageCounter <= 4)
|
||||
loadNextPage()
|
||||
if(_automaticNextPageCounter < _automaticBackoff.size) {
|
||||
if(_automaticNextPageCounter > 0) {
|
||||
val automaticNextPageCounterSaved = _automaticNextPageCounter;
|
||||
fragment.lifecycleScope.launch(Dispatchers.Default) {
|
||||
val backoff = _automaticBackoff[Math.min(_automaticBackoff.size - 1, _automaticNextPageCounter)];
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
setLoading(true);
|
||||
}
|
||||
delay(backoff.toLong());
|
||||
if(automaticNextPageCounterSaved == _automaticNextPageCounter) {
|
||||
withContext(Dispatchers.Main) {
|
||||
loadNextPage();
|
||||
}
|
||||
}
|
||||
else {
|
||||
withContext(Dispatchers.Main) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
loadNextPage();
|
||||
}
|
||||
} else {
|
||||
Logger.i(TAG, "ensureEnoughContentVisible automaticNextPageCounter reset");
|
||||
_automaticNextPageCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
+124
-5
@@ -5,24 +5,35 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.allViews
|
||||
import androidx.core.view.contains
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.UISlideOverlays.Companion.showOrderOverlay
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.api.media.structures.IRefreshPager
|
||||
import com.futo.platformplayer.api.media.structures.IReusablePager
|
||||
import com.futo.platformplayer.api.media.structures.ReusablePager
|
||||
import com.futo.platformplayer.api.media.structures.ReusableRefreshPager
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StateMeta
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringArrayStorage
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
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.InsertedViewAdapterWithLoader
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||
@@ -90,18 +101,30 @@ class HomeFragment : MainFragment() {
|
||||
_view?.setPreviewsEnabled(previewsEnabled && Settings.instance.home.previewFeedItems);
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
class HomeView : ContentFeedView<HomeFragment> {
|
||||
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
|
||||
|
||||
private var _toggleBar: ToggleBar? = null;
|
||||
|
||||
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
|
||||
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
|
||||
|
||||
private var _lastPager: IReusablePager<IPlatformContent>? = null;
|
||||
|
||||
constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
||||
_taskGetPager = TaskHandler<Boolean, IPager<IPlatformContent>>({ fragment.lifecycleScope }, {
|
||||
StatePlatform.instance.getHomeRefresh(fragment.lifecycleScope)
|
||||
})
|
||||
.success { loadedResult(it); }
|
||||
.success {
|
||||
val wrappedPager = if(it is IRefreshPager)
|
||||
ReusableRefreshPager(it);
|
||||
else
|
||||
ReusablePager(it);
|
||||
_lastPager = wrappedPager;
|
||||
loadedResult(wrappedPager.getWindow());
|
||||
}
|
||||
.exception<ScriptCaptchaRequiredException> { }
|
||||
.exception<ScriptExecutionException> {
|
||||
Logger.w(ChannelFragment.TAG, "Plugin failure.", it);
|
||||
@@ -127,6 +150,8 @@ class HomeFragment : MainFragment() {
|
||||
}, fragment);
|
||||
};
|
||||
|
||||
initializeToolbarContent();
|
||||
|
||||
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||
showAnnouncementView()
|
||||
}
|
||||
@@ -201,13 +226,107 @@ class HomeFragment : MainFragment() {
|
||||
loadResults();
|
||||
}
|
||||
|
||||
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||
return results.filter { !StateMeta.instance.isVideoHidden(it.url) && !StateMeta.instance.isCreatorHidden(it.author.url) };
|
||||
private val _filterLock = Object();
|
||||
private var _toggleRecent = false;
|
||||
private var _toggleWatched = false;
|
||||
private var _togglePluginsDisabled = mutableListOf<String>();
|
||||
private var _togglesConfig = FragmentedStorage.get<StringArrayStorage>("home_toggles");
|
||||
fun initializeToolbarContent() {
|
||||
if(_toolbarContentView.allViews.any { it is ToggleBar })
|
||||
_toolbarContentView.removeView(_toolbarContentView.allViews.find { it is ToggleBar });
|
||||
|
||||
if(Settings.instance.home.showHomeFilters) {
|
||||
|
||||
if (!_togglesConfig.any()) {
|
||||
_togglesConfig.set("today", "watched", "plugins");
|
||||
_togglesConfig.save();
|
||||
}
|
||||
_toggleBar = ToggleBar(context).apply {
|
||||
layoutParams =
|
||||
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
_togglePluginsDisabled.clear();
|
||||
synchronized(_filterLock) {
|
||||
val buttonsPlugins = (if (_togglesConfig.contains("plugins"))
|
||||
(StatePlatform.instance.getEnabledClients()
|
||||
.map { plugin ->
|
||||
ToggleBar.Toggle(plugin.name, plugin.icon, true, {
|
||||
if (it) {
|
||||
if (_togglePluginsDisabled.contains(plugin.id))
|
||||
_togglePluginsDisabled.remove(plugin.id);
|
||||
} else {
|
||||
if (!_togglePluginsDisabled.contains(plugin.id))
|
||||
_togglePluginsDisabled.add(plugin.id);
|
||||
}
|
||||
reloadForFilters();
|
||||
}).withTag("plugins")
|
||||
})
|
||||
else listOf())
|
||||
val buttons = (listOf<ToggleBar.Toggle?>(
|
||||
(if (_togglesConfig.contains("today"))
|
||||
ToggleBar.Toggle("Today", _toggleRecent) {
|
||||
_toggleRecent = it; reloadForFilters()
|
||||
}
|
||||
.withTag("today") else null),
|
||||
(if (_togglesConfig.contains("watched"))
|
||||
ToggleBar.Toggle("Unwatched", _toggleWatched) {
|
||||
_toggleWatched = it; reloadForFilters()
|
||||
}
|
||||
.withTag("watched") else null),
|
||||
).filterNotNull() + buttonsPlugins)
|
||||
.sortedBy { _togglesConfig.indexOf(it.tag ?: "") } ?: listOf()
|
||||
|
||||
val buttonSettings = ToggleBar.Toggle("", R.drawable.ic_settings, true, {
|
||||
showOrderOverlay(_overlayContainer,
|
||||
"Visible home filters",
|
||||
listOf(
|
||||
Pair("Plugins", "plugins"),
|
||||
Pair("Today", "today"),
|
||||
Pair("Watched", "watched")
|
||||
),
|
||||
{
|
||||
val newArray = it.map { it.toString() }.toTypedArray();
|
||||
_togglesConfig.set(*(if (newArray.any()) newArray else arrayOf("none")));
|
||||
_togglesConfig.save();
|
||||
initializeToolbarContent();
|
||||
},
|
||||
"Select which toggles you want to see in order. You can also choose to hide filters in the Grayjay Settings"
|
||||
);
|
||||
}).asButton();
|
||||
|
||||
val buttonsOrder = (buttons + listOf(buttonSettings)).toTypedArray();
|
||||
_toggleBar?.setToggles(*buttonsOrder);
|
||||
}
|
||||
|
||||
_toolbarContentView.addView(_toggleBar, 0);
|
||||
}
|
||||
}
|
||||
fun reloadForFilters() {
|
||||
_lastPager?.let { loadedResult(it.getWindow()) };
|
||||
}
|
||||
|
||||
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) > 25)
|
||||
return@filter false;
|
||||
if(_toggleWatched && StateHistory.instance.isHistoryWatched(it.url, 0))
|
||||
return@filter false;
|
||||
if(_togglePluginsDisabled.any() && it.id.pluginId != null && _togglePluginsDisabled.contains(it.id.pluginId)) {
|
||||
return@filter false;
|
||||
}
|
||||
|
||||
return@filter true;
|
||||
};
|
||||
}
|
||||
|
||||
private fun loadResults(withRefetch: Boolean = true) {
|
||||
setLoading(true);
|
||||
_taskGetPager.run(true);
|
||||
_taskGetPager.run(withRefetch);
|
||||
}
|
||||
private fun loadedResult(pager : IPager<IPlatformContent>) {
|
||||
if (pager is EmptyPager<IPlatformContent>) {
|
||||
|
||||
+76
-3
@@ -6,12 +6,17 @@ import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.views.SearchView
|
||||
import com.futo.platformplayer.views.adapters.*
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
|
||||
class PlaylistsFragment : MainFragment() {
|
||||
@@ -65,6 +72,7 @@ class PlaylistsFragment : MainFragment() {
|
||||
private val _fragment: PlaylistsFragment;
|
||||
|
||||
var watchLater: ArrayList<IPlatformVideo> = arrayListOf();
|
||||
var allPlaylists: ArrayList<Playlist> = arrayListOf();
|
||||
var playlists: ArrayList<Playlist> = arrayListOf();
|
||||
private var _appBar: AppBarLayout;
|
||||
private var _adapterWatchLater: VideoListHorizontalAdapter;
|
||||
@@ -72,12 +80,20 @@ class PlaylistsFragment : MainFragment() {
|
||||
private var _layoutWatchlist: ConstraintLayout;
|
||||
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
|
||||
|
||||
private var _listPlaylistsSearch: EditText;
|
||||
|
||||
private var _ordering: String? = null;
|
||||
|
||||
|
||||
constructor(fragment: PlaylistsFragment, inflater: LayoutInflater) : super(inflater.context) {
|
||||
_fragment = fragment;
|
||||
inflater.inflate(R.layout.fragment_playlists, this);
|
||||
|
||||
_listPlaylistsSearch = findViewById(R.id.playlists_search);
|
||||
|
||||
watchLater = ArrayList();
|
||||
playlists = ArrayList();
|
||||
allPlaylists = ArrayList();
|
||||
|
||||
val recyclerWatchLater = findViewById<RecyclerView>(R.id.recycler_watch_later);
|
||||
|
||||
@@ -105,6 +121,7 @@ class PlaylistsFragment : MainFragment() {
|
||||
buttonCreatePlaylist.setOnClickListener {
|
||||
_slideUpOverlay = UISlideOverlays.showCreatePlaylistOverlay(findViewById<FrameLayout>(R.id.overlay_create_playlist)) {
|
||||
val playlist = Playlist(it, arrayListOf());
|
||||
allPlaylists.add(0, playlist);
|
||||
playlists.add(0, playlist);
|
||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||
|
||||
@@ -120,6 +137,34 @@ class PlaylistsFragment : MainFragment() {
|
||||
_appBar = findViewById(R.id.app_bar);
|
||||
_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)); };
|
||||
StatePlaylists.instance.onWatchLaterChanged.subscribe(this) {
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
@@ -134,10 +179,12 @@ class PlaylistsFragment : MainFragment() {
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun onShown() {
|
||||
allPlaylists.clear();
|
||||
playlists.clear()
|
||||
playlists.addAll(
|
||||
allPlaylists.addAll(
|
||||
StatePlaylists.instance.getPlaylists().sortedByDescending { maxOf(it.datePlayed, it.dateUpdate, it.dateCreation) }
|
||||
);
|
||||
playlists.addAll(filterPlaylists(allPlaylists));
|
||||
_adapterPlaylist.notifyDataSetChanged();
|
||||
|
||||
updateWatchLater();
|
||||
@@ -157,6 +204,32 @@ class PlaylistsFragment : MainFragment() {
|
||||
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() {
|
||||
val watchList = StatePlaylists.instance.getWatchLater();
|
||||
if (watchList.isNotEmpty()) {
|
||||
@@ -164,7 +237,7 @@ class PlaylistsFragment : MainFragment() {
|
||||
|
||||
_appBar.let { appBar ->
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
@@ -172,7 +245,7 @@ class PlaylistsFragment : MainFragment() {
|
||||
|
||||
_appBar.let { appBar ->
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
+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.dp
|
||||
import com.futo.platformplayer.fixHtmlWhitespace
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
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.comments.AddCommentView
|
||||
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.pills.PillRatingLikesDislikes
|
||||
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.Models
|
||||
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.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.shape.CornerFamily
|
||||
@@ -112,7 +111,7 @@ class PostDetailFragment : MainFragment {
|
||||
private var _isLoading = false;
|
||||
private var _post: IPlatformPostDetails? = null;
|
||||
private var _postOverview: IPlatformPost? = null;
|
||||
private var _polycentricProfile: PolycentricCache.CachedPolycentricProfile? = null;
|
||||
private var _polycentricProfile: PolycentricProfile? = null;
|
||||
private var _version = 0;
|
||||
private var _isRepliesVisible: Boolean = false;
|
||||
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);
|
||||
} 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) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load claims.", it);
|
||||
@@ -274,7 +273,7 @@ class PostDetailFragment : MainFragment {
|
||||
};
|
||||
|
||||
_buttonStore.setOnClickListener {
|
||||
_polycentricProfile?.profile?.systemState?.store?.let {
|
||||
_polycentricProfile?.systemState?.store?.let {
|
||||
try {
|
||||
val uri = Uri.parse(it);
|
||||
val intent = Intent(Intent.ACTION_VIEW);
|
||||
@@ -334,7 +333,7 @@ class PostDetailFragment : MainFragment {
|
||||
}
|
||||
|
||||
try {
|
||||
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
|
||||
val queryReferencesResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, ref, null,null,
|
||||
arrayListOf(
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
|
||||
ContentType.OPINION.value).setValue(
|
||||
@@ -604,16 +603,8 @@ class PostDetailFragment : MainFragment {
|
||||
|
||||
private fun fetchPolycentricProfile() {
|
||||
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);
|
||||
_taskLoadPolycentricProfile.run(author.id);
|
||||
}
|
||||
}
|
||||
|
||||
private fun setChannelMeta(value: IPlatformPost?) {
|
||||
@@ -639,17 +630,18 @@ class PostDetailFragment : MainFragment {
|
||||
_repliesOverlay.cleanup();
|
||||
}
|
||||
|
||||
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
_polycentricProfile = cachedPolycentricProfile;
|
||||
private fun setPolycentricProfile(polycentricProfile: PolycentricProfile?, animate: Boolean) {
|
||||
_polycentricProfile = polycentricProfile;
|
||||
|
||||
if (cachedPolycentricProfile?.profile == null) {
|
||||
val pp = _polycentricProfile;
|
||||
if (pp == null) {
|
||||
_layoutMonetization.visibility = View.GONE;
|
||||
_creatorThumbnail.setHarborAvailable(false, animate, null);
|
||||
return;
|
||||
}
|
||||
|
||||
_layoutMonetization.visibility = View.VISIBLE;
|
||||
_creatorThumbnail.setHarborAvailable(true, animate, cachedPolycentricProfile.profile.system.toProto());
|
||||
_creatorThumbnail.setHarborAvailable(true, animate, pp.system.toProto());
|
||||
}
|
||||
|
||||
private fun fetchPost() {
|
||||
|
||||
+1
-1
@@ -556,7 +556,7 @@ class SourceDetailFragment : MainFragment() {
|
||||
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
||||
|
||||
val config = SourcePluginConfig.fromJson(configJson);
|
||||
if (config.version <= c.version && config.name != "Youtube") {
|
||||
if (config.version <= c.version) {
|
||||
Logger.i(TAG, "Plugin is up to date.");
|
||||
withContext(Dispatchers.Main) { UIDialogs.toast(context.getString(R.string.plugin_is_fully_up_to_date)); };
|
||||
return@launch;
|
||||
|
||||
+2
@@ -256,6 +256,8 @@ class SubscriptionGroupFragment : MainFragment() {
|
||||
val sub = StateSubscriptions.instance.getSubscription(sub) ?: StateSubscriptions.instance.getSubscriptionOther(sub);
|
||||
if(sub != null && sub.channel.thumbnail != null) {
|
||||
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
|
||||
if(g.image != null)
|
||||
g.image!!.subscriptionUrl = sub.channel.url;
|
||||
g.image?.setImageView(_imageGroup);
|
||||
g.image?.setImageView(_imageGroupBackground);
|
||||
break;
|
||||
|
||||
+9
-2
@@ -9,6 +9,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -122,8 +123,14 @@ class SuggestionsFragment : MainFragment {
|
||||
navigate<RemotePlaylistFragment>(it);
|
||||
else if(StatePlatform.instance.hasEnabledChannelClient(it))
|
||||
navigate<ChannelFragment>(it);
|
||||
else
|
||||
navigate<VideoDetailFragment>(it);
|
||||
else {
|
||||
val url = it;
|
||||
activity?.let {
|
||||
close()
|
||||
if(it is MainActivity)
|
||||
it.navigate(it.getFragment<VideoDetailFragment>(), url);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
navigate<ContentSearchResultsFragment>(SuggestionsFragmentData(it, SearchType.VIDEO, _channelUrl));
|
||||
|
||||
+129
-66
@@ -94,12 +94,10 @@ import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||
import com.futo.platformplayer.fixHtmlLinks
|
||||
import com.futo.platformplayer.fixHtmlWhitespace
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.getNowDiffSeconds
|
||||
import com.futo.platformplayer.helpers.VideoHelper
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
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.comments.AddCommentView
|
||||
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.LiveChatOverlay
|
||||
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.RoundButtonGroup
|
||||
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.subscriptions.SubscribeButton
|
||||
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.Models
|
||||
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.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -195,6 +197,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
private var _liveChat: LiveChatManager? = null;
|
||||
private var _videoResumePositionMilliseconds : Long = 0L;
|
||||
|
||||
private var _chapters: List<IChapter>? = null;
|
||||
|
||||
private val _player: FutoVideoPlayer;
|
||||
private val _cast: CastView;
|
||||
private val _playerProgress: PlayerControlView;
|
||||
@@ -263,6 +267,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
private val _container_content_liveChat: LiveChatOverlay;
|
||||
private val _container_content_browser: WebviewOverlay;
|
||||
private val _container_content_support: SupportOverlay;
|
||||
private val _container_content_chapters: ChaptersOverlay;
|
||||
|
||||
private var _container_content_current: View;
|
||||
|
||||
@@ -294,7 +299,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
private set;
|
||||
private var _historicalPosition: Long = 0;
|
||||
private var _commentsCount = 0;
|
||||
private var _polycentricProfile: PolycentricCache.CachedPolycentricProfile? = null;
|
||||
private var _polycentricProfile: PolycentricProfile? = null;
|
||||
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
|
||||
private var _autoplayVideo: IPlatformVideo? = null
|
||||
|
||||
@@ -374,6 +379,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_container_content_liveChat = findViewById(R.id.videodetail_container_livechat);
|
||||
_container_content_support = findViewById(R.id.videodetail_container_support);
|
||||
_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);
|
||||
_commentsList = findViewById(R.id.comments_list);
|
||||
@@ -398,6 +404,10 @@ class VideoDetailView : ConstraintLayout {
|
||||
_monetization = findViewById(R.id.monetization);
|
||||
_player.attachPlayer();
|
||||
|
||||
_player.onChapterClicked.subscribe {
|
||||
showChaptersUI();
|
||||
};
|
||||
|
||||
|
||||
_buttonSubscribe.onSubscribed.subscribe {
|
||||
_slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
|
||||
@@ -409,12 +419,12 @@ class VideoDetailView : ConstraintLayout {
|
||||
};
|
||||
|
||||
_monetization.onSupportTap.subscribe {
|
||||
_container_content_support.setPolycentricProfile(_polycentricProfile?.profile);
|
||||
_container_content_support.setPolycentricProfile(_polycentricProfile);
|
||||
switchContentView(_container_content_support);
|
||||
};
|
||||
|
||||
_monetization.onStoreTap.subscribe {
|
||||
_polycentricProfile?.profile?.systemState?.store?.let {
|
||||
_polycentricProfile?.systemState?.store?.let {
|
||||
try {
|
||||
val uri = Uri.parse(it);
|
||||
val intent = Intent(Intent.ACTION_VIEW);
|
||||
@@ -579,6 +589,14 @@ class VideoDetailView : ConstraintLayout {
|
||||
_minimize_title.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 {
|
||||
if (StateCasting.instance.activeDevice == null) {
|
||||
handlePlayChanged(it);
|
||||
@@ -678,6 +696,11 @@ class VideoDetailView : ConstraintLayout {
|
||||
_container_content_replies.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_chapters.onClose.subscribe { switchContentView(_container_content_main); };
|
||||
|
||||
_container_content_chapters.onClick.subscribe {
|
||||
handleSeek(it.timeStart.toLong() * 1000);
|
||||
}
|
||||
|
||||
_description_viewMore.setOnClickListener {
|
||||
switchContentView(_container_content_description);
|
||||
@@ -844,6 +867,22 @@ class VideoDetailView : ConstraintLayout {
|
||||
_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() {
|
||||
val isLimitedVersion = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
|
||||
if (it is JSClient)
|
||||
@@ -857,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)
|
||||
RoundButton(context, R.drawable.ic_chat, context.getString(R.string.live_chat), TAG_LIVECHAT) {
|
||||
video?.let {
|
||||
@@ -922,7 +968,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
} else if(devices.size == 1){
|
||||
val device = devices.first();
|
||||
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)
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
@@ -963,6 +1009,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
throw IllegalStateException("Expected media content, found ${video.contentType}");
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
_videoResumePositionMilliseconds = _player.position
|
||||
setVideoDetails(video);
|
||||
}
|
||||
}
|
||||
@@ -1227,16 +1274,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
||||
_channelName.text = video.author.name;
|
||||
|
||||
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url, true);
|
||||
if (cachedPolycentricProfile != null) {
|
||||
setPolycentricProfile(cachedPolycentricProfile, animate = false);
|
||||
if (cachedPolycentricProfile.expired) {
|
||||
_taskLoadPolycentricProfile.run(video.author.id);
|
||||
}
|
||||
} else {
|
||||
setPolycentricProfile(null, animate = false);
|
||||
_taskLoadPolycentricProfile.run(video.author.id);
|
||||
}
|
||||
setPolycentricProfile(null, animate = false);
|
||||
_taskLoadPolycentricProfile.run(video.author.id);
|
||||
|
||||
_player.clear();
|
||||
|
||||
@@ -1265,8 +1304,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
||||
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
||||
_didTriggerDatasourceErrroCount = 0;
|
||||
_didTriggerDatasourceError = false;
|
||||
_autoplayVideo = null
|
||||
Logger.i(TAG, "Autoplay video cleared (setVideoDetails)")
|
||||
|
||||
@@ -1277,6 +1314,10 @@ class VideoDetailView : ConstraintLayout {
|
||||
_lastVideoSource = null;
|
||||
_lastAudioSource = 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())
|
||||
@@ -1337,10 +1378,12 @@ class VideoDetailView : ConstraintLayout {
|
||||
val chapters = null ?: StatePlatform.instance.getContentChapters(video.url);
|
||||
_player.setChapters(chapters);
|
||||
_cast.setChapters(chapters);
|
||||
_chapters = _player.getChapters();
|
||||
} catch (ex: Throwable) {
|
||||
Logger.e(TAG, "Failed to get chapters", ex);
|
||||
_player.setChapters(null);
|
||||
_cast.setChapters(null);
|
||||
_chapters = null;
|
||||
|
||||
/*withContext(Dispatchers.Main) {
|
||||
UIDialogs.toast(context, "Failed to get chapters\n" + ex.message);
|
||||
@@ -1379,6 +1422,10 @@ class VideoDetailView : ConstraintLayout {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
updateMoreButtons();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1394,11 +1441,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
setTabIndex(2, true)
|
||||
} else {
|
||||
when (Settings.instance.comments.defaultCommentSection) {
|
||||
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(
|
||||
0,
|
||||
true
|
||||
) else setTabIndex(1, true);
|
||||
1 -> setTabIndex(1, true);
|
||||
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(0, true) else setTabIndex(1, true)
|
||||
1 -> setTabIndex(1, true)
|
||||
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
|
||||
}
|
||||
}
|
||||
@@ -1436,16 +1480,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
_buttonSubscribe.setSubscribeChannel(video.author.url);
|
||||
setDescription(video.description.fixHtmlLinks());
|
||||
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
setPolycentricProfile(null, animate = false);
|
||||
_taskLoadPolycentricProfile.run(video.author.id);
|
||||
|
||||
_platform.setPlatformFromClientID(video.id.pluginId);
|
||||
val subTitleSegments: ArrayList<String> = ArrayList();
|
||||
@@ -1474,7 +1510,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val queryReferencesResponse = ApiMethods.getQueryReferences(
|
||||
PolycentricCache.SERVER, ref, null, null,
|
||||
ApiMethods.SERVER, ref, null, null,
|
||||
arrayListOf(
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
||||
.setFromType(ContentType.OPINION.value).setValue(
|
||||
@@ -1490,10 +1526,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
val likes = queryReferencesResponse.countsList[0];
|
||||
val dislikes = queryReferencesResponse.countsList[1];
|
||||
val hasLiked =
|
||||
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 hasLiked = 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*/;
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
_rating.visibility = View.VISIBLE;
|
||||
@@ -1831,7 +1865,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private var _didTriggerDatasourceErrroCount = 0;
|
||||
private var _didTriggerDatasourceErrorCount = 0;
|
||||
private var _didTriggerDatasourceError = false;
|
||||
private fun onDataSourceError(exception: Throwable) {
|
||||
Logger.e(TAG, "onDataSourceError", exception);
|
||||
@@ -1841,32 +1875,53 @@ class VideoDetailView : ConstraintLayout {
|
||||
return;
|
||||
val config = currentVideo.sourceConfig;
|
||||
|
||||
if(_didTriggerDatasourceErrroCount <= 3) {
|
||||
if(_didTriggerDatasourceErrorCount <= 3) {
|
||||
_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;
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await();
|
||||
val previousVideoSource = _lastVideoSource;
|
||||
val previousAudioSource = _lastAudioSource;
|
||||
try {
|
||||
val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await();
|
||||
val previousVideoSource = _lastVideoSource;
|
||||
val previousAudioSource = _lastAudioSource;
|
||||
|
||||
if(newDetails is IPlatformVideoDetails) {
|
||||
val newVideoSource = if(previousVideoSource != null)
|
||||
VideoHelper.selectBestVideoSource(newDetails.video, previousVideoSource.height * previousVideoSource.width, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS);
|
||||
else null;
|
||||
val newAudioSource = if(previousAudioSource != null)
|
||||
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);
|
||||
if (newDetails is IPlatformVideoDetails) {
|
||||
val newVideoSource = if (previousVideoSource != null)
|
||||
VideoHelper.selectBestVideoSource(
|
||||
newDetails.video,
|
||||
previousVideoSource.height * previousVideoSource.width,
|
||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
||||
);
|
||||
else null;
|
||||
val newAudioSource = if (previousAudioSource != null)
|
||||
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,
|
||||
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),
|
||||
@@ -2590,13 +2645,21 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
|
||||
onChannelClicked.subscribe {
|
||||
fragment.navigate<ChannelFragment>(it)
|
||||
if(it.url.isNotBlank())
|
||||
fragment.navigate<ChannelFragment>(it)
|
||||
else
|
||||
UIDialogs.appToast("No author url present");
|
||||
}
|
||||
|
||||
onAddToWatchLaterClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
}
|
||||
}
|
||||
onAddToQueueClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
StatePlayer.instance.addToQueue(it);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -2768,13 +2831,12 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
_polycentricProfile = cachedPolycentricProfile;
|
||||
private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
|
||||
_polycentricProfile = profile
|
||||
|
||||
val dp_35 = 35.dp(context.resources)
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
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) {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
@@ -2783,12 +2845,12 @@ class VideoDetailView : ConstraintLayout {
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
val username = cachedPolycentricProfile?.profile?.systemState?.username
|
||||
val username = profile?.systemState?.username
|
||||
if (username != null) {
|
||||
_channelName.text = username
|
||||
}
|
||||
|
||||
_monetization.setPolycentricProfile(cachedPolycentricProfile);
|
||||
_monetization.setPolycentricProfile(profile);
|
||||
}
|
||||
|
||||
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
|
||||
@@ -2976,7 +3038,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
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) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load claims.", it);
|
||||
@@ -3062,6 +3124,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
const val TAG_SHARE = "share";
|
||||
const val TAG_OVERLAY = "overlay";
|
||||
const val TAG_LIVECHAT = "livechat";
|
||||
const val TAG_CHAPTERS = "chapters";
|
||||
const val TAG_OPEN = "open";
|
||||
const val TAG_SEND_TO_DEVICE = "send_to_device";
|
||||
const val TAG_MORE = "MORE";
|
||||
|
||||
+42
-1
@@ -9,6 +9,7 @@ import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.setPadding
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
@@ -22,6 +23,7 @@ import com.futo.platformplayer.states.StateDownloads
|
||||
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
|
||||
|
||||
abstract class VideoListEditorView : LinearLayout {
|
||||
@@ -37,9 +39,15 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
protected var _buttonExport: ImageButton;
|
||||
private var _buttonShare: ImageButton;
|
||||
private var _buttonEdit: ImageButton;
|
||||
private var _buttonSearch: ImageButton;
|
||||
|
||||
private var _search: SearchView;
|
||||
|
||||
private var _onShare: (()->Unit)? = null;
|
||||
|
||||
private var _loadedVideos: List<IPlatformVideo>? = null;
|
||||
private var _loadedVideosCanEdit: Boolean = false;
|
||||
|
||||
constructor(inflater: LayoutInflater) : super(inflater.context) {
|
||||
inflater.inflate(R.layout.fragment_video_list_editor, this);
|
||||
|
||||
@@ -57,6 +65,26 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
_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);
|
||||
val onShare = _onShare;
|
||||
@@ -171,9 +199,22 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
.load(R.drawable.placeholder_video_thumbnail)
|
||||
.into(_imagePlaylistThumbnail)
|
||||
}
|
||||
|
||||
_loadedVideos = videos;
|
||||
_loadedVideosCanEdit = 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) {
|
||||
_buttonDownload.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.models.PlatformAuthorLink
|
||||
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.views.casting.CastButton
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
|
||||
class NavigationTopBarFragment : TopFragment() {
|
||||
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.manifest.DashManifestParser
|
||||
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.VideoUnMuxedSourceDescriptor
|
||||
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);
|
||||
}
|
||||
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 }) {
|
||||
preferredLanguage
|
||||
} else {
|
||||
if(altSources.any { it.language == Language.ENGLISH })
|
||||
Language.ENGLISH
|
||||
Language.ENGLISH;
|
||||
else
|
||||
Language.UNKNOWN;
|
||||
}
|
||||
@@ -208,5 +214,38 @@ class VideoHelper {
|
||||
}
|
||||
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;
|
||||
|
||||
import static com.futo.platformplayer.Extensions_PolycentricKt.getDataLinkFromUrl;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
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.MultiModelLoaderFactory;
|
||||
import com.bumptech.glide.signature.ObjectKey;
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache;
|
||||
import com.futo.polycentric.core.ApiMethods;
|
||||
|
||||
import kotlin.Unit;
|
||||
import kotlinx.coroutines.CoroutineScopeKt;
|
||||
import kotlinx.coroutines.Deferred;
|
||||
import kotlinx.coroutines.Dispatchers;
|
||||
import userpackage.Protocol;
|
||||
|
||||
import java.lang.Exception;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.CancellationException;
|
||||
@@ -60,7 +66,14 @@ public class PolycentricModelLoader implements ModelLoader<String, ByteBuffer> {
|
||||
@Override
|
||||
public void loadData(@NonNull Priority priority, @NonNull DataFetcher.DataCallback<? super ByteBuffer> callback) {
|
||||
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 -> {
|
||||
if (throwable != null) {
|
||||
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.models.PlatformAuthorLink
|
||||
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.serializers.OffsetDateTimeSerializer
|
||||
import java.time.LocalDateTime
|
||||
@@ -46,6 +47,7 @@ class HistoryVideo {
|
||||
val name = str.substring(indexNext + 3);
|
||||
|
||||
val video = resolve?.invoke(url) ?: SerializedPlatformVideo(
|
||||
ContentType.MEDIA,
|
||||
id = PlatformID.asUrlID(url),
|
||||
name = name,
|
||||
thumbnails = Thumbnails(),
|
||||
|
||||
@@ -7,6 +7,8 @@ import android.widget.ImageView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.PresetImages
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Transient
|
||||
import java.io.File
|
||||
@@ -18,7 +20,8 @@ data class ImageVariable(
|
||||
@Transient
|
||||
@Contextual
|
||||
private val bitmap: Bitmap? = null,
|
||||
val presetName: String? = null) {
|
||||
val presetName: String? = null,
|
||||
var subscriptionUrl: String? = null) {
|
||||
|
||||
@SuppressLint("DiscouragedApi")
|
||||
fun setImageView(imageView: ImageView, fallbackResId: Int = -1) {
|
||||
@@ -33,6 +36,12 @@ data class ImageVariable(
|
||||
} else if(!url.isNullOrEmpty()) {
|
||||
Glide.with(imageView)
|
||||
.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)
|
||||
.into(imageView);
|
||||
} else if(!presetName.isNullOrEmpty()) {
|
||||
@@ -63,7 +72,13 @@ data class ImageVariable(
|
||||
return ImageVariable(null, null, null, str);
|
||||
}
|
||||
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) {
|
||||
listOf()
|
||||
} 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 {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
@@ -340,7 +340,7 @@ class HLS {
|
||||
|
||||
val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.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);
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,10 @@ import kotlinx.serialization.json.jsonPrimitive
|
||||
class PlatformContentSerializer : JsonContentPolymorphicSerializer<SerializedPlatformContent>(SerializedPlatformContent::class) {
|
||||
|
||||
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<SerializedPlatformContent> {
|
||||
val obj = element.jsonObject["contentType"];
|
||||
val obj = element.jsonObject["contentType"] ?: element.jsonObject["ContentType"];
|
||||
|
||||
//TODO: Remove this temporary fallback..at some point
|
||||
if(obj == null && element.jsonObject["isLive"]?.jsonPrimitive?.booleanOrNull != null)
|
||||
if(obj == null && (element.jsonObject["isLive"]?.jsonPrimitive?.booleanOrNull ?: element.jsonObject["IsLive"]?.jsonPrimitive?.booleanOrNull) != null)
|
||||
return SerializedPlatformVideo.serializer();
|
||||
|
||||
if(obj?.jsonPrimitive?.isString != false) {
|
||||
|
||||
@@ -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.MultiChronoContentPager
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.resolveChannelUrl
|
||||
import com.futo.platformplayer.serializers.PlatformContentSerializer
|
||||
import com.futo.platformplayer.stores.db.ManagedDBStore
|
||||
@@ -50,14 +49,7 @@ class StateCache {
|
||||
val subs = StateSubscriptions.instance.getSubscriptions();
|
||||
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
|
||||
val allUrls = subs
|
||||
.map {
|
||||
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()
|
||||
.map { it.channel.url }
|
||||
.distinct()
|
||||
.filter { StatePlatform.instance.hasEnabledChannelClient(it) };
|
||||
|
||||
|
||||
@@ -48,6 +48,17 @@ class StateDownloads {
|
||||
private val _downloadsStat = StatFs(_downloadsDirectory.absolutePath);
|
||||
|
||||
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()
|
||||
.apply { afterLoadingDownloaded(this) };
|
||||
private val _downloading = FragmentedStorage.storeJson<VideoDownload>("downloading")
|
||||
@@ -87,9 +98,6 @@ class StateDownloads {
|
||||
Logger.i("StateDownloads", "Deleting local video ${id.value}");
|
||||
val downloaded = getCachedVideo(id);
|
||||
if(downloaded != null) {
|
||||
synchronized(_downloadedSet) {
|
||||
_downloadedSet.remove(id);
|
||||
}
|
||||
_downloaded.delete(downloaded);
|
||||
}
|
||||
onDownloadedChanged.emit();
|
||||
@@ -263,9 +271,6 @@ class StateDownloads {
|
||||
if(existing.groupID == null) {
|
||||
existing.groupID = VideoDownload.GROUP_WATCHLATER;
|
||||
existing.groupType = VideoDownload.GROUP_WATCHLATER;
|
||||
synchronized(_downloadedSet) {
|
||||
_downloadedSet.add(existing.id);
|
||||
}
|
||||
_downloaded.save(existing);
|
||||
}
|
||||
}
|
||||
@@ -308,9 +313,6 @@ class StateDownloads {
|
||||
if(existing.groupID == null) {
|
||||
existing.groupID = playlist.id;
|
||||
existing.groupType = VideoDownload.GROUP_PLAYLIST;
|
||||
synchronized(_downloadedSet) {
|
||||
_downloadedSet.add(existing.id);
|
||||
}
|
||||
_downloaded.save(existing);
|
||||
}
|
||||
}
|
||||
@@ -476,7 +478,16 @@ class StateDownloads {
|
||||
|
||||
val root = DocumentFile.fromTreeUri(context, it!!);
|
||||
|
||||
val localVideos = StateDownloads.instance.getDownloadedVideosPlaylist(playlistId)
|
||||
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;
|
||||
|
||||
@@ -484,6 +495,7 @@ class StateDownloads {
|
||||
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})");
|
||||
@@ -501,6 +513,7 @@ class StateDownloads {
|
||||
lastNotifyTime = now;
|
||||
}
|
||||
}, root);
|
||||
success++;
|
||||
} catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed export [${video.name}]: ${ex.message}", ex);
|
||||
}
|
||||
@@ -509,7 +522,7 @@ class StateDownloads {
|
||||
withContext(Dispatchers.Main) {
|
||||
it.setProgress(1f);
|
||||
it.dismiss();
|
||||
UIDialogs.appToast("Finished exporting playlist");
|
||||
UIDialogs.appToast("Finished exporting playlist (${success} videos${if(i < success) ", ${i} errors" else ""})");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class StateMeta {
|
||||
return when(lastCommentSection.value){
|
||||
"Polycentric" -> 0;
|
||||
"Platform" -> 1;
|
||||
else -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
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.video.IPlatformVideo
|
||||
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.Event1
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -130,6 +131,12 @@ class StatePlayer {
|
||||
closeMediaSession();
|
||||
}
|
||||
|
||||
fun saveQueueAsPlaylist(name: String){
|
||||
val videos = _queue.toList();
|
||||
val playlist = Playlist(name, videos.map { SerializedPlatformVideo.fromVideo(it) });
|
||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||
}
|
||||
|
||||
//Notifications
|
||||
fun hasMediaSession() : Boolean {
|
||||
return MediaPlaybackService.getService() != null;
|
||||
|
||||
@@ -177,11 +177,14 @@ class StatePlaylists {
|
||||
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) {
|
||||
if(!_watchlistStore.hasItem { it.url == video.url })
|
||||
wasNew = true;
|
||||
_watchlistStore.saveAsync(video);
|
||||
if(orderPosition == -1)
|
||||
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values) .toTypedArray());
|
||||
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray());
|
||||
else {
|
||||
val existing = _watchlistOrderStore.getAllValues().toMutableList();
|
||||
existing.add(orderPosition, video.url);
|
||||
@@ -198,6 +201,7 @@ class StatePlaylists {
|
||||
}
|
||||
|
||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||
return wasNew;
|
||||
}
|
||||
|
||||
fun getLastPlayedPlaylist() : Playlist? {
|
||||
@@ -226,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) {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
StateSync.instance.broadcastJsonData(
|
||||
GJSyncOpcodes.syncWatchLater, SyncWatchLaterPackage(
|
||||
if (orderOnly) listOf() else getWatchLater(),
|
||||
if (orderOnly) mapOf() else _watchLaterAdds.all(),
|
||||
if (orderOnly) mapOf() else _watchLaterRemovals.all(),
|
||||
getWatchLaterLastReorderTime().toEpochSecond(),
|
||||
_watchlistOrderStore.values.toList()
|
||||
)
|
||||
GJSyncOpcodes.syncWatchLater, getWatchLaterSyncPacket(orderOnly)
|
||||
);
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to broadcast watch later", e)
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.futo.platformplayer.states
|
||||
|
||||
import android.content.Context
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.LoginActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
@@ -101,6 +102,8 @@ class StatePlugins {
|
||||
if (availableClient !is JSClient) {
|
||||
continue
|
||||
}
|
||||
if(!Settings.instance.plugins.checkDisabledPluginsForUpdates && !StatePlatform.instance.isClientEnabled(availableClient.id))
|
||||
continue;
|
||||
|
||||
val newConfig = checkForUpdates(availableClient.config);
|
||||
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.awaitFirstDeferred
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||
import com.futo.platformplayer.resolveChannelUrl
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
@@ -33,6 +31,7 @@ import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.ClaimType
|
||||
import com.futo.polycentric.core.ContentType
|
||||
import com.futo.polycentric.core.Opinion
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
import com.futo.polycentric.core.ProcessHandle
|
||||
import com.futo.polycentric.core.PublicKey
|
||||
import com.futo.polycentric.core.SignedEvent
|
||||
@@ -234,34 +233,7 @@ class StatePolycentric {
|
||||
if (!enabled) {
|
||||
return Pair(false, listOf(url));
|
||||
}
|
||||
var polycentricProfile: PolycentricProfile? = null;
|
||||
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));
|
||||
return Pair(didUpdate, listOf(url));
|
||||
}
|
||||
|
||||
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()),
|
||||
name = systemState.username,
|
||||
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
|
||||
),
|
||||
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 {
|
||||
ensureEnabled()
|
||||
|
||||
val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null,
|
||||
val response = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, null,
|
||||
null,
|
||||
listOf(
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
||||
@@ -382,7 +354,7 @@ class StatePolycentric {
|
||||
}
|
||||
|
||||
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()
|
||||
.setProcess(pointer.process)
|
||||
.addRanges(Protocol.Range.newBuilder()
|
||||
@@ -400,11 +372,11 @@ class StatePolycentric {
|
||||
}
|
||||
|
||||
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 profileEvents = ApiMethods.getQueryLatest(
|
||||
PolycentricCache.SERVER,
|
||||
ApiMethods.SERVER,
|
||||
ev.system.toProto(),
|
||||
listOf(
|
||||
ContentType.AVATAR.value,
|
||||
@@ -433,7 +405,7 @@ class StatePolycentric {
|
||||
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
||||
url = systemLinkUrl,
|
||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
|
||||
subscribers = null
|
||||
),
|
||||
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) {
|
||||
return EmptyPager()
|
||||
}
|
||||
|
||||
val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null,
|
||||
val response = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, null,
|
||||
Protocol.QueryReferencesRequestEvents.newBuilder()
|
||||
.setFromType(ContentType.POST.value)
|
||||
.addAllCountLwwElementReferences(arrayListOf(
|
||||
@@ -486,7 +458,7 @@ class StatePolycentric {
|
||||
}
|
||||
|
||||
override suspend fun nextPageAsync() {
|
||||
val nextPageResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, _cursor,
|
||||
val nextPageResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, _cursor,
|
||||
Protocol.QueryReferencesRequestEvents.newBuilder()
|
||||
.setFromType(ContentType.POST.value)
|
||||
.addAllCountLwwElementReferences(arrayListOf(
|
||||
@@ -534,7 +506,7 @@ class StatePolycentric {
|
||||
return@mapNotNull LazyComment(scope.async(_commentPoolDispatcher){
|
||||
Logger.i(TAG, "Fetching comment data for [" + ev.system.key.toBase64() + "]");
|
||||
val profileEvents = ApiMethods.getQueryLatest(
|
||||
PolycentricCache.SERVER,
|
||||
ApiMethods.SERVER,
|
||||
ev.system.toProto(),
|
||||
listOf(
|
||||
ContentType.AVATAR.value,
|
||||
@@ -558,7 +530,7 @@ class StatePolycentric {
|
||||
|
||||
val unixMilliseconds = ev.unixMilliseconds
|
||||
//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)
|
||||
return@async PolycentricPlatformComment(
|
||||
contextUrl = contextUrl,
|
||||
@@ -566,7 +538,7 @@ class StatePolycentric {
|
||||
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
||||
url = systemLinkUrl,
|
||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
|
||||
subscribers = null
|
||||
),
|
||||
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
|
||||
|
||||
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.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.models.Subscription
|
||||
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.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.models.SyncSubscriptionGroupsPackage
|
||||
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.futo.platformplayer.states
|
||||
|
||||
import SubsExchangeClient
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
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.Subscription
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.resolveChannelUrl
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringDateMapStorage
|
||||
import com.futo.platformplayer.stores.StringStorage
|
||||
import com.futo.platformplayer.stores.StringStringMapStorage
|
||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||
import com.futo.platformplayer.stores.v2.ReconstructStore
|
||||
@@ -68,10 +69,24 @@ class StateSubscriptions {
|
||||
|
||||
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
||||
|
||||
private val _subsExchangeServer = "http://10.10.15.159"//"https://exchange.grayjay.app/";
|
||||
private val _subscriptionKey = FragmentedStorage.get<StringStorage>("sub_exchange_key");
|
||||
|
||||
init {
|
||||
global.onUpdateProgress.subscribe { 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 {
|
||||
@@ -335,12 +350,6 @@ class StateSubscriptions {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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>> {
|
||||
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)
|
||||
algo.onNewCacheHit.subscribe(onNewCacheHit)
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ import kotlin.system.measureTimeMillis
|
||||
|
||||
class StateSync {
|
||||
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 _lastAddressStorage = FragmentedStorage.get<StringStringMapStorage>("sync_last_address_storage")
|
||||
private val _syncSessionData = FragmentedStorage.get<StringTMapStorage<SyncSessionData>>("syncSessionData")
|
||||
@@ -305,12 +306,22 @@ class StateSync {
|
||||
synchronized(_sessions) {
|
||||
session = _sessions[s.remotePublicKey]
|
||||
if (session == null) {
|
||||
val remoteDeviceName = synchronized(_nameStorage) {
|
||||
_nameStorage.get(remotePublicKey)
|
||||
}
|
||||
|
||||
session = SyncSession(remotePublicKey, onAuthorized = { it, isNewlyAuthorized, isNewSession ->
|
||||
if (!isNewSession) {
|
||||
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) {
|
||||
_lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress)
|
||||
}
|
||||
@@ -341,7 +352,7 @@ class StateSync {
|
||||
|
||||
deviceRemoved.emit(it.remotePublicKey)
|
||||
|
||||
})
|
||||
}, remoteDeviceName)
|
||||
_sessions[remotePublicKey] = session!!
|
||||
}
|
||||
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) {
|
||||
withContext(Dispatchers.IO) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -41,4 +41,19 @@ class StringArrayStorage : FragmentedStorageFileJson() {
|
||||
return values.toList();
|
||||
}
|
||||
}
|
||||
fun any(): Boolean {
|
||||
synchronized(values) {
|
||||
return values.any();
|
||||
}
|
||||
}
|
||||
fun contains(v: String): Boolean {
|
||||
synchronized(values) {
|
||||
return values.contains(v);
|
||||
}
|
||||
}
|
||||
fun indexOf(v: String): Int {
|
||||
synchronized(values){
|
||||
return values.indexOf(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,9 @@ class ManagedStore<T>{
|
||||
|
||||
val className: String? get() = _class.classifier?.assume<KClass<*>>()?.simpleName;
|
||||
|
||||
private var _onModificationCreate: ((T) -> Unit)? = null;
|
||||
private var _onModificationDelete: ((T) -> Unit)? = null;
|
||||
|
||||
val name: String;
|
||||
|
||||
constructor(name: String, dir: File, clazz: KType, serializer: StoreSerializer<T>, niceName: String? = null) {
|
||||
@@ -62,6 +65,12 @@ class ManagedStore<T>{
|
||||
return this;
|
||||
}
|
||||
|
||||
fun withOnModified(created: (T)->Unit, deleted: (T)->Unit): ManagedStore<T> {
|
||||
_onModificationCreate = created;
|
||||
_onModificationDelete = deleted;
|
||||
return this;
|
||||
}
|
||||
|
||||
fun load(): ManagedStore<T> {
|
||||
synchronized(_files) {
|
||||
_files.clear();
|
||||
@@ -265,6 +274,7 @@ class ManagedStore<T>{
|
||||
file = saveNew(obj);
|
||||
if(_reconstructStore != null && (_reconstructStore!!.backupOnCreate || withReconstruction))
|
||||
saveReconstruction(file, obj);
|
||||
_onModificationCreate?.invoke(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,6 +310,7 @@ class ManagedStore<T>{
|
||||
_files.remove(item);
|
||||
Logger.v(TAG, "Deleting file ${logName(file.id)}");
|
||||
file.delete();
|
||||
_onModificationDelete?.invoke(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -1,5 +1,6 @@
|
||||
package com.futo.platformplayer.subscription
|
||||
|
||||
import SubsExchangeClient
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
@@ -15,8 +16,9 @@ class SmartSubscriptionAlgorithm(
|
||||
scope: CoroutineScope,
|
||||
allowFailure: Boolean = false,
|
||||
withCacheFallback: Boolean = true,
|
||||
threadPool: ForkJoinPool? = null
|
||||
): SubscriptionsTaskFetchAlgorithm(scope, allowFailure, withCacheFallback, threadPool) {
|
||||
threadPool: ForkJoinPool? = null,
|
||||
subsExchangeClient: SubsExchangeClient? = null
|
||||
): SubscriptionsTaskFetchAlgorithm(scope, allowFailure, withCacheFallback, threadPool, subsExchangeClient) {
|
||||
override fun getSubscriptionTasks(subs: Map<Subscription, List<String>>): List<SubscriptionTask> {
|
||||
val allTasks: List<SubscriptionTask> = subs.flatMap { entry ->
|
||||
val sub = entry.key;
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
package com.futo.platformplayer.subscription
|
||||
|
||||
import SubsExchangeClient
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
@@ -33,11 +34,11 @@ abstract class SubscriptionFetchAlgorithm(
|
||||
companion object {
|
||||
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) {
|
||||
SubscriptionFetchAlgorithms.CACHE -> CachedSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool, 50);
|
||||
SubscriptionFetchAlgorithms.SIMPLE -> SimpleSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
|
||||
SubscriptionFetchAlgorithms.SMART -> SmartSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool);
|
||||
SubscriptionFetchAlgorithms.SMART -> SmartSubscriptionAlgorithm(scope, allowFailure, withCacheFallback, pool, withExchangeClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+76
-3
@@ -1,19 +1,23 @@
|
||||
package com.futo.platformplayer.subscription
|
||||
|
||||
import SubsExchangeClient
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||
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.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.structures.DedupContentPager
|
||||
import com.futo.platformplayer.api.media.structures.EmptyPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
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.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||
import com.futo.platformplayer.exceptions.ChannelException
|
||||
import com.futo.platformplayer.findNonRuntimeException
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
|
||||
@@ -24,6 +28,9 @@ import com.futo.platformplayer.states.StateCache
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
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 java.time.OffsetDateTime
|
||||
import java.util.concurrent.ExecutionException
|
||||
@@ -35,7 +42,8 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
scope: CoroutineScope,
|
||||
allowFailure: Boolean = false,
|
||||
withCacheFallback: Boolean = true,
|
||||
_threadPool: ForkJoinPool? = null
|
||||
_threadPool: ForkJoinPool? = null,
|
||||
private val subsExchangeClient: SubsExchangeClient? = null
|
||||
) : SubscriptionFetchAlgorithm(scope, allowFailure, withCacheFallback, _threadPool) {
|
||||
|
||||
|
||||
@@ -45,7 +53,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
}
|
||||
|
||||
override fun getSubscriptions(subs: Map<Subscription, List<String>>): Result {
|
||||
val tasks = getSubscriptionTasks(subs);
|
||||
var tasks = getSubscriptionTasks(subs).toMutableList()
|
||||
|
||||
val tasksGrouped = tasks.groupBy { it.client }
|
||||
|
||||
@@ -70,6 +78,32 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
|
||||
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 cachedChannels = mutableListOf<String>()
|
||||
val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
|
||||
@@ -104,6 +138,43 @@ 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) {
|
||||
val invalids = resolve.filter { it.content.any { it.datetime == 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")
|
||||
|
||||
//Cache pagers grouped by channel
|
||||
@@ -173,6 +244,8 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
||||
Logger.e(StateSubscriptions.TAG, "Subscription peek [${task.sub.channel.name}] failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
//Intercepts task.fromCache & task.fromPeek
|
||||
synchronized(cachedChannels) {
|
||||
if(task.fromCache || task.fromPeek) {
|
||||
finished++;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.futo.platformplayer.subsexchange
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class ChannelRequest(
|
||||
@SerialName("ChannelUrl")
|
||||
var channelUrl: String
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.futo.platformplayer.subsexchange
|
||||
|
||||
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.video.SerializedPlatformContent
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Serializable
|
||||
class ChannelResolve(
|
||||
@SerialName("ChannelUrl")
|
||||
var channelUrl: String,
|
||||
@SerialName("Content")
|
||||
var content: List<SerializedPlatformContent>,
|
||||
@SerialName("Channel")
|
||||
var channel: IPlatformChannel? = null
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.futo.platformplayer.subsexchange
|
||||
|
||||
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.video.SerializedPlatformContent
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeStringSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Serializable
|
||||
class ChannelResult(
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeSerializer::class)
|
||||
@SerialName("dateTime")
|
||||
var dateTime: OffsetDateTime,
|
||||
@SerialName("channelUrl")
|
||||
var channelUrl: String,
|
||||
@SerialName("content")
|
||||
var content: List<SerializedPlatformContent>,
|
||||
@SerialName("channel")
|
||||
var channel: IPlatformChannel? = null
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.futo.platformplayer.subsexchange
|
||||
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeStringSerializer
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Serializer
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Serializable
|
||||
class ExchangeContract(
|
||||
@SerialName("ID")
|
||||
var id: String,
|
||||
@SerialName("Requests")
|
||||
var requests: List<ChannelRequest>,
|
||||
@SerialName("Provided")
|
||||
var provided: List<String> = listOf(),
|
||||
@SerialName("Required")
|
||||
var required: List<String> = listOf(),
|
||||
@SerialName("Expire")
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeStringSerializer::class)
|
||||
var expired: OffsetDateTime = OffsetDateTime.MIN,
|
||||
@SerialName("ContractVersion")
|
||||
var contractVersion: Int = 1
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.futo.platformplayer.subsexchange
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ExchangeContractResolve(
|
||||
@SerialName("PublicKey")
|
||||
val publicKey: String,
|
||||
@SerialName("Signature")
|
||||
val signature: String,
|
||||
@SerialName("Data")
|
||||
val data: String
|
||||
)
|
||||
@@ -0,0 +1,150 @@
|
||||
import com.futo.platformplayer.api.media.Serializer
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.subsexchange.ChannelRequest
|
||||
import com.futo.platformplayer.subsexchange.ChannelResolve
|
||||
import com.futo.platformplayer.subsexchange.ChannelResult
|
||||
import com.futo.platformplayer.subsexchange.ExchangeContract
|
||||
import com.futo.platformplayer.subsexchange.ExchangeContractResolve
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.security.KeyFactory
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.security.Signature
|
||||
import java.security.interfaces.RSAPrivateKey
|
||||
import java.security.interfaces.RSAPublicKey
|
||||
import java.util.Base64
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStream
|
||||
import java.io.OutputStreamWriter
|
||||
import java.math.BigInteger
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.security.spec.RSAPublicKeySpec
|
||||
|
||||
|
||||
class SubsExchangeClient(private val server: String, private val privateKey: String) {
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
private val publicKey: String = extractPublicKey(privateKey)
|
||||
|
||||
// Endpoints
|
||||
|
||||
// Endpoint: Contract
|
||||
fun requestContract(vararg channels: ChannelRequest): ExchangeContract {
|
||||
val data = post("/api/Channel/Contract", Json.encodeToString(channels), "application/json")
|
||||
return Json.decodeFromString(data)
|
||||
}
|
||||
suspend fun requestContractAsync(vararg channels: ChannelRequest): ExchangeContract {
|
||||
val data = postAsync("/api/Channel/Contract", Json.encodeToString(channels), "application/json")
|
||||
return Json.decodeFromString(data)
|
||||
}
|
||||
|
||||
// Endpoint: Resolve
|
||||
fun resolveContract(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> {
|
||||
val contractResolve = convertResolves(*resolves)
|
||||
val result = post("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve), "application/json")
|
||||
Logger.v("SubsExchangeClient", "Resolve:" + result);
|
||||
return Serializer.json.decodeFromString(result)
|
||||
}
|
||||
suspend fun resolveContractAsync(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> {
|
||||
val contractResolve = convertResolves(*resolves)
|
||||
val result = postAsync("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve), "application/json")
|
||||
return Serializer.json.decodeFromString(result)
|
||||
}
|
||||
|
||||
|
||||
private fun convertResolves(vararg resolves: ChannelResolve): ExchangeContractResolve {
|
||||
val data = Serializer.json.encodeToString(resolves)
|
||||
val signature = createSignature(data, privateKey)
|
||||
|
||||
return ExchangeContractResolve(
|
||||
publicKey = publicKey,
|
||||
signature = signature,
|
||||
data = data
|
||||
)
|
||||
}
|
||||
|
||||
// IO methods
|
||||
private fun post(query: String, body: String, contentType: String): String {
|
||||
val url = URL("${server.trim('/')}$query")
|
||||
with(url.openConnection() as HttpURLConnection) {
|
||||
requestMethod = "POST"
|
||||
setRequestProperty("Content-Type", contentType)
|
||||
doOutput = true
|
||||
OutputStreamWriter(outputStream, StandardCharsets.UTF_8).use { it.write(body); it.flush() }
|
||||
|
||||
val status = responseCode;
|
||||
Logger.i("SubsExchangeClient", "POST [${url}]: ${status}");
|
||||
|
||||
if(status == 200)
|
||||
InputStreamReader(inputStream, StandardCharsets.UTF_8).use {
|
||||
return it.readText()
|
||||
}
|
||||
else {
|
||||
var errorStr = "";
|
||||
try {
|
||||
errorStr = InputStreamReader(errorStream, StandardCharsets.UTF_8).use {
|
||||
return@use it.readText()
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable){}
|
||||
|
||||
throw Exception("Exchange server resulted in code ${status}:\n" + errorStr);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
private suspend fun postAsync(query: String, body: String, contentType: String): String {
|
||||
return withContext(Dispatchers.IO) {
|
||||
post(query, body, contentType)
|
||||
}
|
||||
}
|
||||
|
||||
// Crypto methods
|
||||
companion object {
|
||||
fun createPrivateKey(): String {
|
||||
val rsa = KeyFactory.getInstance("RSA")
|
||||
val keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||
keyPairGenerator.initialize(2048);
|
||||
val keyPair = keyPairGenerator.generateKeyPair();
|
||||
return Base64.getEncoder().encodeToString(keyPair.private.encoded);
|
||||
}
|
||||
|
||||
fun extractPublicKey(privateKey: String): String {
|
||||
val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))
|
||||
val keyFactory = KeyFactory.getInstance("RSA")
|
||||
val privateKeyObj = keyFactory.generatePrivate(keySpec) as RSAPrivateKey
|
||||
val publicKeyObj: PublicKey? = keyFactory.generatePublic(RSAPublicKeySpec(privateKeyObj.modulus, BigInteger.valueOf(65537)));
|
||||
var publicKeyBase64 = Base64.getEncoder().encodeToString(publicKeyObj?.encoded);
|
||||
var pem = "-----BEGIN PUBLIC KEY-----"
|
||||
while(publicKeyBase64.length > 0) {
|
||||
val length = Math.min(publicKeyBase64.length, 64);
|
||||
pem += "\n" + publicKeyBase64.substring(0, length);
|
||||
publicKeyBase64 = publicKeyBase64.substring(length);
|
||||
}
|
||||
return pem + "\n-----END PUBLIC KEY-----";
|
||||
}
|
||||
|
||||
fun createSignature(data: String, privateKey: String): String {
|
||||
val keySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))
|
||||
val keyFactory = KeyFactory.getInstance("RSA")
|
||||
val rsaPrivateKey = keyFactory.generatePrivate(keySpec) as RSAPrivateKey
|
||||
|
||||
val signature = Signature.getInstance("SHA256withRSA")
|
||||
signature.initSign(rsaPrivateKey)
|
||||
signature.update(data.toByteArray(Charsets.UTF_8))
|
||||
|
||||
val signatureBytes = signature.sign()
|
||||
return Base64.getEncoder().encodeToString(signatureBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,10 @@ import com.futo.platformplayer.api.media.Serializer
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.HistoryVideo
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.smartMerge
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
@@ -30,6 +28,7 @@ import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
@@ -53,6 +52,9 @@ class SyncSession : IAuthorizable {
|
||||
private val _id = UUID.randomUUID()
|
||||
private var _remoteId: UUID? = null
|
||||
private var _lastAuthorizedRemoteId: UUID? = null
|
||||
var remoteDeviceName: String? = null
|
||||
private set
|
||||
val displayName: String get() = remoteDeviceName ?: remotePublicKey
|
||||
|
||||
var connected: Boolean = false
|
||||
private set(v) {
|
||||
@@ -62,7 +64,7 @@ class SyncSession : IAuthorizable {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(remotePublicKey: String, onAuthorized: (session: SyncSession, isNewlyAuthorized: Boolean, isNewSession: Boolean) -> Unit, onUnauthorized: (session: SyncSession) -> Unit, onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit, onClose: (session: SyncSession) -> Unit) {
|
||||
constructor(remotePublicKey: String, onAuthorized: (session: SyncSession, isNewlyAuthorized: Boolean, isNewSession: Boolean) -> Unit, onUnauthorized: (session: SyncSession) -> Unit, onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit, onClose: (session: SyncSession) -> Unit, remoteDeviceName: String?) {
|
||||
this.remotePublicKey = remotePublicKey
|
||||
_onAuthorized = onAuthorized
|
||||
_onUnauthorized = onUnauthorized
|
||||
@@ -85,7 +87,20 @@ class SyncSession : IAuthorizable {
|
||||
|
||||
fun authorize(socketSession: SyncSocketSession) {
|
||||
Logger.i(TAG, "Sent AUTHORIZED with session id $_id")
|
||||
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(_id.toString().toByteArray()))
|
||||
|
||||
if (socketSession.remoteVersion >= 3) {
|
||||
val idStringBytes = _id.toString().toByteArray()
|
||||
val nameBytes = "${android.os.Build.MANUFACTURER}-${android.os.Build.MODEL}".toByteArray()
|
||||
val buffer = ByteArray(1 + idStringBytes.size + 1 + nameBytes.size)
|
||||
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).apply {
|
||||
put(idStringBytes.size.toByte())
|
||||
put(idStringBytes)
|
||||
put(nameBytes.size.toByte())
|
||||
put(nameBytes)
|
||||
}.apply { flip() })
|
||||
} else {
|
||||
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(_id.toString().toByteArray()))
|
||||
}
|
||||
_authorized = true
|
||||
checkAuthorized()
|
||||
}
|
||||
@@ -138,15 +153,37 @@ class SyncSession : IAuthorizable {
|
||||
|
||||
when (opcode) {
|
||||
Opcode.NOTIFY_AUTHORIZED.value -> {
|
||||
val str = data.toUtf8String()
|
||||
_remoteId = if (data.remaining() >= 0) UUID.fromString(str) else UUID.fromString("00000000-0000-0000-0000-000000000000")
|
||||
if (socketSession.remoteVersion >= 3) {
|
||||
val idByteCount = data.get().toInt()
|
||||
if (idByteCount > 64)
|
||||
throw Exception("Id should always be smaller than 64 bytes")
|
||||
|
||||
val idBytes = ByteArray(idByteCount)
|
||||
data.get(idBytes)
|
||||
|
||||
val nameByteCount = data.get().toInt()
|
||||
if (nameByteCount > 64)
|
||||
throw Exception("Name should always be smaller than 64 bytes")
|
||||
|
||||
val nameBytes = ByteArray(nameByteCount)
|
||||
data.get(nameBytes)
|
||||
|
||||
_remoteId = UUID.fromString(idBytes.toString(Charsets.UTF_8))
|
||||
remoteDeviceName = nameBytes.toString(Charsets.UTF_8)
|
||||
} else {
|
||||
val str = data.toUtf8String()
|
||||
_remoteId = if (data.remaining() >= 0) UUID.fromString(str) else UUID.fromString("00000000-0000-0000-0000-000000000000")
|
||||
remoteDeviceName = null
|
||||
}
|
||||
|
||||
_remoteAuthorized = true
|
||||
Logger.i(TAG, "Received AUTHORIZED with session id $_remoteId")
|
||||
Logger.i(TAG, "Received AUTHORIZED with session id $_remoteId (device name: '${remoteDeviceName ?: "not set"}')")
|
||||
checkAuthorized()
|
||||
return
|
||||
}
|
||||
Opcode.NOTIFY_UNAUTHORIZED.value -> {
|
||||
_remoteId = null
|
||||
remoteDeviceName = null
|
||||
_lastAuthorizedRemoteId = null
|
||||
_remoteAuthorized = false
|
||||
_onUnauthorized(this)
|
||||
@@ -195,6 +232,8 @@ class SyncSession : IAuthorizable {
|
||||
sendData(GJSyncOpcodes.syncSubscriptionGroups, StateSubscriptionGroups.instance.getSyncSubscriptionGroupsPackageString());
|
||||
sendData(GJSyncOpcodes.syncPlaylists, StatePlaylists.instance.getSyncPlaylistsPackageString())
|
||||
|
||||
sendData(GJSyncOpcodes.syncWatchLater, Json.encodeToString(StatePlaylists.instance.getWatchLaterSyncPacket(false)));
|
||||
|
||||
val recentHistory = StateHistory.instance.getRecentHistory(syncSessionData.lastHistory);
|
||||
if(recentHistory.size > 0)
|
||||
sendJsonData(GJSyncOpcodes.syncHistory, recentHistory);
|
||||
|
||||
@@ -46,6 +46,8 @@ class SyncSocketSession {
|
||||
val localPublicKey: String get() = _localPublicKey
|
||||
private val _onData: (session: SyncSocketSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) -> Unit
|
||||
var authorizable: IAuthorizable? = null
|
||||
var remoteVersion: Int = -1
|
||||
private set
|
||||
|
||||
val remoteAddress: String
|
||||
|
||||
@@ -162,11 +164,12 @@ class SyncSocketSession {
|
||||
}
|
||||
|
||||
private fun performVersionCheck() {
|
||||
val CURRENT_VERSION = 2
|
||||
val CURRENT_VERSION = 3
|
||||
val MINIMUM_VERSION = 2
|
||||
_outputStream.writeInt(CURRENT_VERSION)
|
||||
val version = _inputStream.readInt()
|
||||
Logger.i(TAG, "performVersionCheck (version = $version)")
|
||||
if (version != CURRENT_VERSION)
|
||||
remoteVersion = _inputStream.readInt()
|
||||
Logger.i(TAG, "performVersionCheck (version = $remoteVersion)")
|
||||
if (remoteVersion < MINIMUM_VERSION)
|
||||
throw Exception("Invalid version")
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||
import com.futo.platformplayer.views.adapters.viewholders.StoreItemViewHolder
|
||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@@ -125,8 +125,7 @@ class MonetizationView : LinearLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?) {
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
fun setPolycentricProfile(profile: PolycentricProfile?) {
|
||||
if (profile != null) {
|
||||
if (profile.systemState.store.isNotEmpty()) {
|
||||
_buttonStore.visibility = View.VISIBLE;
|
||||
|
||||
@@ -14,10 +14,10 @@ import androidx.core.view.isVisible
|
||||
import androidx.core.view.size
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.futo.platformplayer.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.models.ImageVariable
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||
import com.futo.platformplayer.views.others.ToggleTagView
|
||||
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionBarViewHolder
|
||||
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
|
||||
import com.futo.platformplayer.views.subscriptions.SubscriptionExploreButton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ToggleBar : LinearLayout {
|
||||
private val _tagsContainer: LinearLayout;
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
StateSubscriptionGroups.instance.onGroupsChanged.remove(this);
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
inflate(context, R.layout.view_toggle_bar, this);
|
||||
|
||||
_tagsContainer = findViewById(R.id.container_tags);
|
||||
}
|
||||
|
||||
fun setToggles(vararg buttons: Toggle) {
|
||||
_tagsContainer.removeAllViews();
|
||||
for(button in buttons) {
|
||||
_tagsContainer.addView(ToggleTagView(context).apply {
|
||||
if(button.icon > 0)
|
||||
this.setInfo(button.icon, button.name, button.isActive, button.isButton);
|
||||
else if(button.iconVariable != null)
|
||||
this.setInfo(button.iconVariable, button.name, button.isActive, button.isButton);
|
||||
else
|
||||
this.setInfo(button.name, button.isActive, button.isButton);
|
||||
this.onClick.subscribe { button.action(it); };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Toggle {
|
||||
val name: String;
|
||||
val icon: Int;
|
||||
val iconVariable: ImageVariable?;
|
||||
val action: (Boolean)->Unit;
|
||||
val isActive: Boolean;
|
||||
var isButton: Boolean = false
|
||||
private set;
|
||||
var tag: String? = null;
|
||||
|
||||
constructor(name: String, icon: ImageVariable?, isActive: Boolean = false, action: (Boolean)->Unit) {
|
||||
this.name = name;
|
||||
this.icon = 0;
|
||||
this.iconVariable = icon;
|
||||
this.action = action;
|
||||
this.isActive = isActive;
|
||||
}
|
||||
constructor(name: String, icon: Int, isActive: Boolean = false, action: (Boolean)->Unit) {
|
||||
this.name = name;
|
||||
this.icon = icon;
|
||||
this.iconVariable = null;
|
||||
this.action = action;
|
||||
this.isActive = isActive;
|
||||
}
|
||||
constructor(name: String, isActive: Boolean = false, action: (Boolean)->Unit) {
|
||||
this.name = name;
|
||||
this.icon = 0;
|
||||
this.iconVariable = null;
|
||||
this.action = action;
|
||||
this.isActive = isActive;
|
||||
}
|
||||
|
||||
fun asButton(): Toggle{
|
||||
isButton = true;
|
||||
return this;
|
||||
}
|
||||
fun withTag(str: String): Toggle {
|
||||
tag = str;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import com.futo.platformplayer.fragment.channel.tab.ChannelListFragment
|
||||
import com.futo.platformplayer.fragment.channel.tab.ChannelMonetizationFragment
|
||||
import com.futo.platformplayer.fragment.channel.tab.ChannelPlaylistsFragment
|
||||
import com.futo.platformplayer.fragment.channel.tab.IChannelTabFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.futo.platformplayer.views.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.chapters.ChapterType
|
||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.comments.LazyComment
|
||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.fixHtmlLinks
|
||||
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.toHumanDuration
|
||||
import com.futo.platformplayer.toHumanNowDiffString
|
||||
import com.futo.platformplayer.toHumanNumber
|
||||
import com.futo.platformplayer.toHumanTime
|
||||
import com.futo.platformplayer.views.LoaderView
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.pills.PillButton
|
||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.Opinion
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ChapterViewHolder : ViewHolder {
|
||||
|
||||
private val _layoutChapter: ConstraintLayout;
|
||||
|
||||
private val _containerChapter: ConstraintLayout;
|
||||
|
||||
private val _textTitle: TextView;
|
||||
private val _textTimestamp: TextView;
|
||||
private val _textMeta: TextView;
|
||||
|
||||
var onClick = Event1<IChapter>();
|
||||
var chapter: IChapter? = null
|
||||
private set;
|
||||
|
||||
constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_chapter, viewGroup, false)) {
|
||||
_layoutChapter = itemView.findViewById(R.id.layout_chapter);
|
||||
_containerChapter = itemView.findViewById(R.id.chapter_container);
|
||||
|
||||
_containerChapter.setOnClickListener {
|
||||
chapter?.let {
|
||||
onClick.emit(it);
|
||||
}
|
||||
}
|
||||
|
||||
_textTitle = itemView.findViewById(R.id.text_title);
|
||||
_textTimestamp = itemView.findViewById(R.id.text_timestamp);
|
||||
_textMeta = itemView.findViewById(R.id.text_meta);
|
||||
}
|
||||
|
||||
fun bind(chapter: IChapter) {
|
||||
_textTitle.text = chapter.name;
|
||||
_textTimestamp.text = chapter.timeStart.toLong().toHumanTime(false);
|
||||
|
||||
if(chapter.type == ChapterType.NORMAL) {
|
||||
_textMeta.isVisible = false;
|
||||
}
|
||||
else {
|
||||
_textMeta.isVisible = true;
|
||||
when(chapter.type) {
|
||||
ChapterType.SKIP -> _textMeta.text = "(Skip)";
|
||||
ChapterType.SKIPPABLE -> _textMeta.text = "(Manual Skip)"
|
||||
ChapterType.SKIPONCE -> _textMeta.text = "(Skip Once)"
|
||||
else -> _textMeta.isVisible = false;
|
||||
};
|
||||
}
|
||||
this.chapter = chapter;
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CommentViewHolder";
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,6 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.fixHtmlLinks
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
@@ -29,6 +27,7 @@ import com.futo.platformplayer.views.LoaderView
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.pills.PillButton
|
||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.Opinion
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -81,24 +80,18 @@ class CommentViewHolder : ViewHolder {
|
||||
throw Exception("Not implemented for non polycentric comments")
|
||||
}
|
||||
|
||||
if (args.hasLiked) {
|
||||
args.processHandle.opinion(c.reference, Opinion.like);
|
||||
val newOpinion: Opinion = if (args.hasLiked) {
|
||||
Opinion.like
|
||||
} else if (args.hasDisliked) {
|
||||
args.processHandle.opinion(c.reference, Opinion.dislike);
|
||||
Opinion.dislike
|
||||
} else {
|
||||
args.processHandle.opinion(c.reference, Opinion.neutral);
|
||||
Opinion.neutral
|
||||
}
|
||||
|
||||
_layoutComment.alpha = if (args.dislikes > 2 && args.dislikes.toFloat() / (args.likes + args.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f;
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
Logger.i(TAG, "Started backfill");
|
||||
args.processHandle.fullyBackfillServersAnnounceExceptions();
|
||||
Logger.i(TAG, "Finished backfill");
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to backfill servers.", e)
|
||||
}
|
||||
ApiMethods.setOpinion(args.processHandle, c.reference, newOpinion)
|
||||
}
|
||||
|
||||
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
|
||||
|
||||
+1
-1
@@ -16,7 +16,6 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.fixHtmlLinks
|
||||
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
@@ -26,6 +25,7 @@ import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
import com.futo.platformplayer.views.pills.PillButton
|
||||
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
|
||||
import com.futo.polycentric.core.Opinion
|
||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.IdentityHashMap
|
||||
|
||||
@@ -16,7 +16,6 @@ import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||
@@ -29,21 +28,12 @@ open class PlaylistView : LinearLayout {
|
||||
protected val _imageThumbnail: ImageView
|
||||
protected val _imageChannel: ImageView?
|
||||
protected val _creatorThumbnail: CreatorThumbnail?
|
||||
protected val _imageNeopassChannel: ImageView?;
|
||||
protected val _platformIndicator: PlatformIndicator;
|
||||
protected val _textPlaylistName: TextView
|
||||
protected val _textVideoCount: TextView
|
||||
protected val _textVideoCountLabel: TextView;
|
||||
protected val _textPlaylistItems: TextView
|
||||
protected val _textChannelName: TextView
|
||||
protected var _neopassAnimator: ObjectAnimator? = null;
|
||||
|
||||
private val _taskLoadValidClaims = TaskHandler<PlatformID, PolycentricCache.CachedOwnedClaims>(StateApp.instance.scopeGetter,
|
||||
{ PolycentricCache.instance.getValidClaimsAsync(it).await() })
|
||||
.success { it -> updateClaimsLayout(it, animate = true) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load claims.", it);
|
||||
};
|
||||
|
||||
val onPlaylistClicked = Event1<IPlatformPlaylist>();
|
||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||
@@ -66,7 +56,6 @@ open class PlaylistView : LinearLayout {
|
||||
_textVideoCountLabel = findViewById(R.id.text_video_count_label);
|
||||
_textChannelName = findViewById(R.id.text_channel_name);
|
||||
_textPlaylistItems = findViewById(R.id.text_playlist_items);
|
||||
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
|
||||
|
||||
setOnClickListener { onOpenClicked() };
|
||||
_imageChannel?.setOnClickListener { currentPlaylist?.let { onChannelClicked.emit(it.author) } };
|
||||
@@ -88,20 +77,6 @@ open class PlaylistView : LinearLayout {
|
||||
|
||||
|
||||
open fun bind(content: IPlatformContent) {
|
||||
_taskLoadValidClaims.cancel();
|
||||
|
||||
if (content.author.id.claimType > 0) {
|
||||
val cachedClaims = PolycentricCache.instance.getCachedValidClaims(content.author.id);
|
||||
if (cachedClaims != null) {
|
||||
updateClaimsLayout(cachedClaims, animate = false);
|
||||
} else {
|
||||
updateClaimsLayout(null, animate = false);
|
||||
_taskLoadValidClaims.run(content.author.id);
|
||||
}
|
||||
} else {
|
||||
updateClaimsLayout(null, animate = false);
|
||||
}
|
||||
|
||||
isClickable = true;
|
||||
|
||||
_imageChannel?.let {
|
||||
@@ -155,25 +130,6 @@ open class PlaylistView : LinearLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateClaimsLayout(claims: PolycentricCache.CachedOwnedClaims?, animate: Boolean) {
|
||||
_neopassAnimator?.cancel();
|
||||
_neopassAnimator = null;
|
||||
|
||||
val firstClaim = claims?.ownedClaims?.firstOrNull();
|
||||
val harborAvailable = firstClaim != null
|
||||
if (harborAvailable) {
|
||||
_imageNeopassChannel?.visibility = View.VISIBLE
|
||||
if (animate) {
|
||||
_neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500)
|
||||
_neopassAnimator?.start()
|
||||
}
|
||||
} else {
|
||||
_imageNeopassChannel?.visibility = View.GONE
|
||||
}
|
||||
|
||||
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate, firstClaim?.system?.toProto())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "VideoPreviewViewHolder"
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
||||
private lateinit var _sortedDataset: List<Subscription>;
|
||||
private val _inflater: LayoutInflater;
|
||||
private val _confirmationMessage: String;
|
||||
private val _onDatasetChanged: ((List<Subscription>)->Unit)?;
|
||||
|
||||
var onClick = Event1<Subscription>();
|
||||
var onSettings = Event1<Subscription>();
|
||||
@@ -30,9 +31,10 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
||||
updateDataset();
|
||||
}
|
||||
|
||||
constructor(inflater: LayoutInflater, confirmationMessage: String) : super() {
|
||||
constructor(inflater: LayoutInflater, confirmationMessage: String, onDatasetChanged: ((List<Subscription>)->Unit)? = null) : super() {
|
||||
_inflater = inflater;
|
||||
_confirmationMessage = confirmationMessage;
|
||||
_onDatasetChanged = onDatasetChanged;
|
||||
|
||||
StateSubscriptions.instance.onSubscriptionsChanged.subscribe { _, _ -> if(Looper.myLooper() != Looper.getMainLooper())
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { updateDataset() }
|
||||
@@ -78,6 +80,8 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
||||
.filter { (queryLower.isNullOrBlank() || it.channel.name.lowercase().contains(queryLower)) }
|
||||
.toList();
|
||||
|
||||
_onDatasetChanged?.invoke(_sortedDataset);
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.toHumanTimeIndicator
|
||||
@@ -32,14 +31,6 @@ class SubscriptionViewHolder : ViewHolder {
|
||||
private val _platformIndicator : PlatformIndicator;
|
||||
private val _textMeta: TextView;
|
||||
|
||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
||||
StateApp.instance.scopeGetter,
|
||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
||||
.success { it -> onProfileLoaded(null, it, true) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load profile.", it);
|
||||
};
|
||||
|
||||
var subscription: Subscription? = null
|
||||
private set;
|
||||
|
||||
@@ -74,45 +65,12 @@ class SubscriptionViewHolder : ViewHolder {
|
||||
}
|
||||
|
||||
fun bind(sub: Subscription) {
|
||||
_taskLoadProfile.cancel();
|
||||
|
||||
this.subscription = sub;
|
||||
|
||||
_creatorThumbnail.setThumbnail(sub.channel.thumbnail, false);
|
||||
_textName.text = sub.channel.name;
|
||||
bindViewMetrics(sub);
|
||||
_platformIndicator.setPlatformFromClientID(sub.channel.id.pluginId);
|
||||
|
||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(sub.channel.url, true);
|
||||
if (cachedProfile != null) {
|
||||
onProfileLoaded(sub, cachedProfile, false);
|
||||
if (cachedProfile.expired) {
|
||||
_taskLoadProfile.run(sub.channel.id);
|
||||
}
|
||||
} else {
|
||||
_taskLoadProfile.run(sub.channel.id);
|
||||
}
|
||||
}
|
||||
|
||||
private fun onProfileLoaded(sub: Subscription?, cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
val dp_46 = 46.dp(itemView.context.resources);
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_46 * dp_46)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(this.subscription?.channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
_textName.text = profile.systemState.username;
|
||||
}
|
||||
|
||||
if(sub != null)
|
||||
bindViewMetrics(sub)
|
||||
}
|
||||
|
||||
fun bindViewMetrics(sub: Subscription?) {
|
||||
|
||||
@@ -30,7 +30,6 @@ import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.fixHtmlWhitespace
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.toHumanNowDiffString
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
@@ -44,7 +43,6 @@ class PreviewPostView : LinearLayout {
|
||||
|
||||
private val _imageAuthorThumbnail: ImageView;
|
||||
private val _textAuthorName: TextView;
|
||||
private val _imageNeopassChannel: ImageView;
|
||||
private val _textMetadata: TextView;
|
||||
private val _textTitle: TextView;
|
||||
private val _textDescription: TextView;
|
||||
@@ -64,15 +62,6 @@ class PreviewPostView : LinearLayout {
|
||||
private val _layoutComments: LinearLayout?;
|
||||
private val _textComments: TextView?;
|
||||
|
||||
private var _neopassAnimator: ObjectAnimator? = null;
|
||||
|
||||
private val _taskLoadValidClaims = TaskHandler<PlatformID, PolycentricCache.CachedOwnedClaims>(StateApp.instance.scopeGetter,
|
||||
{ PolycentricCache.instance.getValidClaimsAsync(it).await() })
|
||||
.success { it -> updateClaimsLayout(it, animate = true) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load claims.", it);
|
||||
};
|
||||
|
||||
val content: IPlatformContent? get() = _content;
|
||||
|
||||
val onContentClicked = Event1<IPlatformContent>();
|
||||
@@ -83,7 +72,6 @@ class PreviewPostView : LinearLayout {
|
||||
|
||||
_imageAuthorThumbnail = findViewById(R.id.image_author_thumbnail);
|
||||
_textAuthorName = findViewById(R.id.text_author_name);
|
||||
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
|
||||
_textMetadata = findViewById(R.id.text_metadata);
|
||||
_textTitle = findViewById(R.id.text_title);
|
||||
_textDescription = findViewById(R.id.text_description);
|
||||
@@ -130,21 +118,8 @@ class PreviewPostView : LinearLayout {
|
||||
}
|
||||
|
||||
fun bind(content: IPlatformContent) {
|
||||
_taskLoadValidClaims.cancel();
|
||||
_content = content;
|
||||
|
||||
if (content.author.id.claimType > 0) {
|
||||
val cachedClaims = PolycentricCache.instance.getCachedValidClaims(content.author.id);
|
||||
if (cachedClaims != null) {
|
||||
updateClaimsLayout(cachedClaims, animate = false);
|
||||
} else {
|
||||
updateClaimsLayout(null, animate = false);
|
||||
_taskLoadValidClaims.run(content.author.id);
|
||||
}
|
||||
} else {
|
||||
updateClaimsLayout(null, animate = false);
|
||||
}
|
||||
|
||||
_textAuthorName.text = content.author.name;
|
||||
_textMetadata.text = content.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: "";
|
||||
|
||||
@@ -292,25 +267,6 @@ class PreviewPostView : LinearLayout {
|
||||
};
|
||||
}
|
||||
|
||||
private fun updateClaimsLayout(claims: PolycentricCache.CachedOwnedClaims?, animate: Boolean) {
|
||||
_neopassAnimator?.cancel();
|
||||
_neopassAnimator = null;
|
||||
|
||||
val harborAvailable = claims != null && !claims.ownedClaims.isNullOrEmpty();
|
||||
if (harborAvailable) {
|
||||
_imageNeopassChannel.visibility = View.VISIBLE
|
||||
if (animate) {
|
||||
_neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500)
|
||||
_neopassAnimator?.start()
|
||||
}
|
||||
} else {
|
||||
_imageNeopassChannel.visibility = View.GONE
|
||||
}
|
||||
|
||||
//TODO: Necessary if we decide to use creator thumbnail with neopass indicator instead
|
||||
//_creatorThumbnail?.setHarborAvailable(harborAvailable, animate)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = "PreviewPostView";
|
||||
}
|
||||
|
||||
+1
-73
@@ -24,7 +24,6 @@ import com.futo.platformplayer.getNowDiffSeconds
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.loadThumbnails
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
@@ -47,7 +46,6 @@ open class PreviewVideoView : LinearLayout {
|
||||
protected val _imageVideo: ImageView
|
||||
protected val _imageChannel: ImageView?
|
||||
protected val _creatorThumbnail: CreatorThumbnail?
|
||||
protected val _imageNeopassChannel: ImageView?;
|
||||
protected val _platformIndicator: PlatformIndicator;
|
||||
protected val _textVideoName: TextView
|
||||
protected val _textChannelName: TextView
|
||||
@@ -57,7 +55,6 @@ open class PreviewVideoView : LinearLayout {
|
||||
protected var _playerVideoThumbnail: FutoThumbnailPlayer? = null;
|
||||
protected val _containerLive: LinearLayout;
|
||||
protected val _playerContainer: FrameLayout;
|
||||
protected var _neopassAnimator: ObjectAnimator? = null;
|
||||
protected val _layoutDownloaded: FrameLayout;
|
||||
|
||||
protected val _button_add_to_queue : View;
|
||||
@@ -65,15 +62,6 @@ open class PreviewVideoView : LinearLayout {
|
||||
protected val _button_add_to : View;
|
||||
|
||||
protected val _exoPlayer: PlayerManager?;
|
||||
|
||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
||||
StateApp.instance.scopeGetter,
|
||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
||||
.success { it -> onProfileLoaded(it, true) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load profile.", it);
|
||||
};
|
||||
|
||||
private val _timeBar: ProgressBar?;
|
||||
|
||||
val onVideoClicked = Event2<IPlatformVideo, Long>();
|
||||
@@ -108,7 +96,6 @@ open class PreviewVideoView : LinearLayout {
|
||||
_button_add_to_queue = findViewById(R.id.button_add_to_queue);
|
||||
_button_add_to_watch_later = findViewById(R.id.button_add_to_watch_later);
|
||||
_button_add_to = findViewById(R.id.button_add_to);
|
||||
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
|
||||
_layoutDownloaded = findViewById(R.id.layout_downloaded);
|
||||
_timeBar = findViewById(R.id.time_bar)
|
||||
|
||||
@@ -132,7 +119,7 @@ open class PreviewVideoView : LinearLayout {
|
||||
|
||||
fun hideAddTo() {
|
||||
_button_add_to.visibility = View.GONE
|
||||
_button_add_to_queue.visibility = View.GONE
|
||||
//_button_add_to_queue.visibility = View.GONE
|
||||
}
|
||||
|
||||
protected open fun inflate(feedStyle: FeedStyle) {
|
||||
@@ -160,15 +147,12 @@ open class PreviewVideoView : LinearLayout {
|
||||
|
||||
|
||||
open fun bind(content: IPlatformContent) {
|
||||
_taskLoadProfile.cancel();
|
||||
|
||||
isClickable = true;
|
||||
|
||||
val isPlanned = (content.datetime?.getNowDiffSeconds() ?: 0) < 0;
|
||||
|
||||
stopPreview();
|
||||
|
||||
_imageNeopassChannel?.visibility = View.GONE;
|
||||
_creatorThumbnail?.setThumbnail(content.author.thumbnail, false);
|
||||
|
||||
val thumbnail = content.author.thumbnail
|
||||
@@ -186,16 +170,6 @@ open class PreviewVideoView : LinearLayout {
|
||||
|
||||
_textChannelName.text = content.author.name
|
||||
|
||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(content.author.url, true);
|
||||
if (cachedProfile != null) {
|
||||
onProfileLoaded(cachedProfile, false);
|
||||
if (cachedProfile.expired) {
|
||||
_taskLoadProfile.run(content.author.id);
|
||||
}
|
||||
} else {
|
||||
_taskLoadProfile.run(content.author.id);
|
||||
}
|
||||
|
||||
_imageChannel?.clipToOutline = true;
|
||||
|
||||
_textVideoName.text = content.name;
|
||||
@@ -335,52 +309,6 @@ open class PreviewVideoView : LinearLayout {
|
||||
_playerVideoThumbnail?.setMuteChangedListener(callback);
|
||||
}
|
||||
|
||||
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
_neopassAnimator?.cancel();
|
||||
_neopassAnimator = null;
|
||||
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
if (_creatorThumbnail != null) {
|
||||
val dp_32 = 32.dp(context.resources);
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_32 * dp_32)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(content?.author?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
} else if (_imageChannel != null) {
|
||||
val dp_28 = 28.dp(context.resources);
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_28 * dp_28)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_imageChannel.let {
|
||||
Glide.with(_imageChannel)
|
||||
.load(avatar)
|
||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||
.into(_imageChannel);
|
||||
}
|
||||
|
||||
_imageNeopassChannel?.visibility = View.VISIBLE
|
||||
if (animate) {
|
||||
_neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500)
|
||||
_neopassAnimator?.start()
|
||||
} else {
|
||||
_imageNeopassChannel?.alpha = 1.0f;
|
||||
}
|
||||
} else {
|
||||
_imageNeopassChannel?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
_textChannelName.text = profile.systemState.username
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "VideoPreviewViewHolder"
|
||||
}
|
||||
|
||||
-77
@@ -11,7 +11,6 @@ import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||
@@ -27,14 +26,6 @@ class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyVi
|
||||
|
||||
val onClick = Event1<IPlatformChannel>();
|
||||
|
||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
||||
StateApp.instance.scopeGetter,
|
||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
||||
.success { onProfileLoaded(it, true) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load profile.", it);
|
||||
};
|
||||
|
||||
init {
|
||||
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
|
||||
_name = _view.findViewById(R.id.text_channel_name);
|
||||
@@ -45,40 +36,10 @@ class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyVi
|
||||
}
|
||||
|
||||
override fun bind(value: IPlatformChannel) {
|
||||
_taskLoadProfile.cancel();
|
||||
|
||||
_channel = value;
|
||||
|
||||
_creatorThumbnail.setThumbnail(value.thumbnail, false);
|
||||
_name.text = value.name;
|
||||
|
||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(value.url, true);
|
||||
if (cachedProfile != null) {
|
||||
onProfileLoaded(cachedProfile, false);
|
||||
if (cachedProfile.expired) {
|
||||
_taskLoadProfile.run(value.id);
|
||||
}
|
||||
} else {
|
||||
_taskLoadProfile.run(value.id);
|
||||
}
|
||||
}
|
||||
|
||||
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
val dp_55 = 55.dp(itemView.context.resources)
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
_name.text = profile.systemState.username;
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -94,14 +55,6 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
|
||||
|
||||
val onClick = Event1<Selectable>();
|
||||
|
||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
||||
StateApp.instance.scopeGetter,
|
||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
||||
.success { onProfileLoaded(it, true) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load profile.", it);
|
||||
};
|
||||
|
||||
init {
|
||||
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
|
||||
_name = _view.findViewById(R.id.text_channel_name);
|
||||
@@ -112,8 +65,6 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
|
||||
}
|
||||
|
||||
override fun bind(value: Selectable) {
|
||||
_taskLoadProfile.cancel();
|
||||
|
||||
_channel = value;
|
||||
|
||||
if(value.active)
|
||||
@@ -123,34 +74,6 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
|
||||
|
||||
_creatorThumbnail.setThumbnail(value.channel.thumbnail, false);
|
||||
_name.text = value.channel.name;
|
||||
|
||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(value.channel.url, true);
|
||||
if (cachedProfile != null) {
|
||||
onProfileLoaded(cachedProfile, false);
|
||||
if (cachedProfile.expired) {
|
||||
_taskLoadProfile.run(value.channel.id);
|
||||
}
|
||||
} else {
|
||||
_taskLoadProfile.run(value.channel.id);
|
||||
}
|
||||
}
|
||||
|
||||
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
val dp_55 = 55.dp(itemView.context.resources)
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(_channel?.channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
_name.text = profile.systemState.username;
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
-40
@@ -12,7 +12,6 @@ import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.toHumanNumber
|
||||
@@ -34,14 +33,6 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
|
||||
|
||||
val onClick = Event1<PlatformAuthorLink>();
|
||||
|
||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
||||
StateApp.instance.scopeGetter,
|
||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
||||
.success { it -> onProfileLoaded(it, true) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load profile.", it);
|
||||
};
|
||||
|
||||
init {
|
||||
_textName = _view.findViewById(R.id.text_channel_name);
|
||||
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
|
||||
@@ -61,21 +52,9 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
|
||||
}
|
||||
|
||||
override fun bind(value: PlatformAuthorLink) {
|
||||
_taskLoadProfile.cancel();
|
||||
|
||||
_creatorThumbnail.setThumbnail(value.thumbnail, false);
|
||||
_textName.text = value.name;
|
||||
|
||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(value.url, true);
|
||||
if (cachedProfile != null) {
|
||||
onProfileLoaded(cachedProfile, false);
|
||||
if (cachedProfile.expired) {
|
||||
_taskLoadProfile.run(value.id);
|
||||
}
|
||||
} else {
|
||||
_taskLoadProfile.run(value.id);
|
||||
}
|
||||
|
||||
if(value.subscribers == null || (value.subscribers ?: 0) <= 0L)
|
||||
_textMetadata.visibility = View.GONE;
|
||||
else {
|
||||
@@ -87,25 +66,6 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
|
||||
_authorLink = value;
|
||||
}
|
||||
|
||||
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
val dp_61 = 61.dp(itemView.context.resources);
|
||||
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_61 * dp_61)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(_authorLink?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
_textName.text = profile.systemState.username;
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CreatorViewHolder";
|
||||
}
|
||||
|
||||
-39
@@ -12,7 +12,6 @@ import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.adapters.AnyAdapter
|
||||
@@ -27,14 +26,6 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
|
||||
private var _subscription: Subscription? = null;
|
||||
private var _channel: SerializedChannel? = null;
|
||||
|
||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
||||
StateApp.instance.scopeGetter,
|
||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
||||
.success { onProfileLoaded(it, true) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load profile.", it);
|
||||
};
|
||||
|
||||
val onClick = Event1<Subscription>();
|
||||
|
||||
init {
|
||||
@@ -47,44 +38,14 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
|
||||
}
|
||||
|
||||
override fun bind(value: Subscription) {
|
||||
_taskLoadProfile.cancel();
|
||||
|
||||
_channel = value.channel;
|
||||
|
||||
_creatorThumbnail.setThumbnail(value.channel.thumbnail, false);
|
||||
_name.text = value.channel.name;
|
||||
|
||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(value.channel.url, true);
|
||||
if (cachedProfile != null) {
|
||||
onProfileLoaded(cachedProfile, false);
|
||||
if (cachedProfile.expired) {
|
||||
_taskLoadProfile.run(value.channel.id);
|
||||
}
|
||||
} else {
|
||||
_taskLoadProfile.run(value.channel.id);
|
||||
}
|
||||
|
||||
_subscription = value;
|
||||
}
|
||||
|
||||
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
val dp_55 = 55.dp(itemView.context.resources)
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
_name.text = profile.systemState.username;
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SubscriptionBarViewHolder";
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user