Merge branch 'master' into 'small-hls-fixes'

# Conflicts:
#   app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
This commit is contained in:
Kai DeLorenzo 2025-03-24 02:12:27 +00:00
commit bdae35b1a8
128 changed files with 826 additions and 1266 deletions

2
.gitmodules vendored
View file

@ -83,7 +83,7 @@
path = app/src/stable/assets/sources/dailymotion path = app/src/stable/assets/sources/dailymotion
url = ../plugins/dailymotion.git url = ../plugins/dailymotion.git
[submodule "app/src/stable/assets/sources/apple-podcast"] [submodule "app/src/stable/assets/sources/apple-podcast"]
path = app/src/stable/assets/sources/apple-podcast path = app/src/stable/assets/sources/apple-podcasts
url = ../plugins/apple-podcasts.git url = ../plugins/apple-podcasts.git
[submodule "app/src/unstable/assets/sources/apple-podcasts"] [submodule "app/src/unstable/assets/sources/apple-podcasts"]
path = app/src/unstable/assets/sources/apple-podcasts path = app/src/unstable/assets/sources/apple-podcasts

View file

@ -197,7 +197,7 @@ dependencies {
implementation 'org.jsoup:jsoup:1.15.3' implementation 'org.jsoup:jsoup:1.15.3'
implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.arthenica:ffmpeg-kit-full:5.1' implementation 'com.arthenica:ffmpeg-kit-full:6.0-2.LTS'
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0' implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
implementation 'com.github.dhaval2404:imagepicker:2.1' implementation 'com.github.dhaval2404:imagepicker:2.1'
implementation 'com.google.zxing:core:3.4.1' implementation 'com.google.zxing:core:3.4.1'

View file

@ -156,7 +156,6 @@
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.SettingsActivity" android:name=".activities.SettingsActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" /> android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity <activity
android:name=".activities.DeveloperActivity" android:name=".activities.DeveloperActivity"

View file

@ -263,6 +263,10 @@ class PlatformVideoDetails extends PlatformVideo {
this.rating = obj.rating ?? null; //IRating this.rating = obj.rating ?? null; //IRating
this.subtitles = obj.subtitles ?? []; this.subtitles = obj.subtitles ?? [];
this.isShort = !!obj.isShort ?? false; this.isShort = !!obj.isShort ?? false;
if (obj.getContentRecommendations) {
this.getContentRecommendations = obj.getContentRecommendations
}
} }
} }

View file

@ -1,13 +1,13 @@
package com.futo.platformplayer package com.futo.platformplayer
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.AnnouncementType import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.polycentric.core.ProcessHandle import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.base64UrlToByteArray
import userpackage.Protocol import userpackage.Protocol
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min import kotlin.math.min
@ -40,33 +40,25 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) } return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
} }
fun String.getDataLinkFromUrl(): Protocol.URLInfoDataLink? {
val urlData = if (this.startsWith("polycentric://")) {
this.substring("polycentric://".length)
} else this;
val urlBytes = urlData.base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
if (urlInfo.urlType != 4L) {
return null
}
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
return dataLink
}
fun Protocol.Claim.resolveChannelUrl(): String? { fun Protocol.Claim.resolveChannelUrl(): String? {
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) }) return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
} }
fun Protocol.Claim.resolveChannelUrls(): List<String> { fun Protocol.Claim.resolveChannelUrls(): List<String> {
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) }) return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
}
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
Logger.w("Backfill", "Polycentric prod server not added, adding it.")
addServer(PolycentricCache.SERVER)
}
val exceptions = fullyBackfillServers()
for (pair in exceptions) {
val server = pair.key
val exception = pair.value
StateAnnouncement.instance.registerAnnouncement(
"backfill-failed",
"Backfill failed",
"Failed to backfill server $server. $exception",
AnnouncementType.SESSION_RECURRING
);
Logger.e("Backfill", "Failed to backfill server $server.", exception)
}
} }

View file

@ -356,7 +356,7 @@ class Settings : FragmentedStorageFileJson() {
var playback = PlaybackSettings(); var playback = PlaybackSettings();
@Serializable @Serializable
class PlaybackSettings { class PlaybackSettings {
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1) @FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -2)
@DropdownFieldOptionsId(R.array.audio_languages) @DropdownFieldOptionsId(R.array.audio_languages)
var primaryLanguage: Int = 0; var primaryLanguage: Int = 0;
@ -380,6 +380,8 @@ class Settings : FragmentedStorageFileJson() {
else -> null else -> null
} }
} }
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
var preferOriginalAudio: Boolean = true;
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage]; //= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
@ -644,6 +646,9 @@ class Settings : FragmentedStorageFileJson() {
@Serializable @Serializable
class Plugins { class Plugins {
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
var checkDisabledPluginsForUpdates: Boolean = false;
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0) @FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
var clearCookiesOnLogout: Boolean = true; var clearCookiesOnLogout: Boolean = true;

View file

@ -79,6 +79,36 @@ class UISlideOverlays {
return menu; return menu;
} }
fun showQueueOptionsOverlay(context: Context, container: ViewGroup) {
UISlideOverlays.showOverlay(container, "Queue options", null, {
}, SlideUpMenuItem(context, R.drawable.ic_playlist, "Save as playlist", "", "Creates a new playlist with queue as videos", null, {
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
addPlaylistOverlay.onOK.subscribe {
val text = nameInput.text.trim()
if (text.isBlank()) {
return@subscribe;
}
addPlaylistOverlay.hide();
nameInput.deactivate();
nameInput.clear();
StatePlayer.instance.saveQueueAsPlaylist(text);
UIDialogs.appToast("Playlist [${text}] created");
};
addPlaylistOverlay.onCancel.subscribe {
nameInput.deactivate();
nameInput.clear();
};
addPlaylistOverlay.show();
nameInput.activate();
}, false));
}
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay { fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
val items = arrayListOf<View>(); val items = arrayListOf<View>();
@ -337,7 +367,9 @@ class UISlideOverlays {
call = { call = {
selectedVideoVariant = it selectedVideoVariant = it
slideUpMenuOverlay.selectOption(videoButtons, it) slideUpMenuOverlay.selectOption(videoButtons, it)
slideUpMenuOverlay.setOk(container.context.getString(R.string.download)) if (audioButtons.isEmpty()){
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
}
}, },
invokeParent = false invokeParent = false
)) ))
@ -372,7 +404,7 @@ class UISlideOverlays {
UIDialogs.toast(container.context, "Variant video HLS playlist download started") UIDialogs.toast(container.context, "Variant video HLS playlist download started")
slideUpMenuOverlay.hide() slideUpMenuOverlay.hide()
} else if (source is IHLSManifestAudioSource) { } else if (source is IHLSManifestAudioSource) {
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, resolvedPlaylistUrl), null) StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, resolvedPlaylistUrl), null)
UIDialogs.toast(container.context, "Variant audio HLS playlist download started") UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
slideUpMenuOverlay.hide() slideUpMenuOverlay.hide()
} else { } else {
@ -419,7 +451,7 @@ class UISlideOverlays {
} }
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources, items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
listOf(listOf(SlideUpMenuItem( listOf((if (audioSources != null) listOf(SlideUpMenuItem(
container.context, container.context,
R.drawable.ic_movie, R.drawable.ic_movie,
container.context.getString(R.string.none), container.context.getString(R.string.none),
@ -432,7 +464,7 @@ class UISlideOverlays {
menu?.setOk(container.context.getString(R.string.download)); menu?.setOk(container.context.getString(R.string.download));
}, },
invokeParent = false invokeParent = false
)) + )) else listOf()) +
videoSources videoSources
.filter { it.isDownloadable() } .filter { it.isDownloadable() }
.map { .map {
@ -909,7 +941,7 @@ class UISlideOverlays {
val watchLater = StatePlaylists.instance.getWatchLater(); val watchLater = StatePlaylists.instance.getWatchLater();
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions", items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
(listOf( (listOf(
if(!isLimited) if(!isLimited && !video.isLive)
SlideUpMenuItem( SlideUpMenuItem(
container.context, container.context,
R.drawable.ic_download, R.drawable.ic_download,
@ -1045,8 +1077,9 @@ class UISlideOverlays {
StatePlayer.TYPE_WATCHLATER, StatePlayer.TYPE_WATCHLATER,
"${watchLater.size} " + container.context.getString(R.string.videos), "${watchLater.size} " + container.context.getString(R.string.videos),
tag = "watch later", tag = "watch later",
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); call = {
UIDialogs.appToast("Added to watch later", false); if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
UIDialogs.appToast("Added to watch later", false);
}), }),
) )
); );

View file

@ -113,7 +113,7 @@ class LoginActivity : AppCompatActivity() {
companion object { companion object {
private val TAG = "LoginActivity"; private val TAG = "LoginActivity";
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#_ ]*"); private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#:_ ]*");
private var _callback: ((SourceAuth?) -> Unit)? = null; private var _callback: ((SourceAuth?) -> Unit)? = null;

View file

@ -11,16 +11,16 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.LoaderView import com.futo.platformplayer.views.LoaderView
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ProcessHandle import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store import com.futo.polycentric.core.Store
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -87,7 +87,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
Logger.e(TAG, "Failed to save process secret to secret storage.", e) Logger.e(TAG, "Failed to save process secret to secret storage.", e)
} }
processHandle.addServer(PolycentricCache.SERVER); processHandle.addServer(ApiMethods.SERVER);
processHandle.setUsername(username); processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle); StatePolycentric.instance.setProcessHandle(processHandle);
} catch (e: Throwable) { } catch (e: Throwable) {

View file

@ -12,12 +12,12 @@ import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.overlays.LoaderOverlay import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.KeyPair import com.futo.polycentric.core.KeyPair
import com.futo.polycentric.core.Process import com.futo.polycentric.core.Process
import com.futo.polycentric.core.ProcessSecret import com.futo.polycentric.core.ProcessSecret
@ -145,7 +145,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
} }
StatePolycentric.instance.setProcessHandle(processHandle); StatePolycentric.instance.setProcessHandle(processHandle);
processHandle.fullyBackfillClient(PolycentricCache.SERVER); processHandle.fullyBackfillClient(ApiMethods.SERVER);
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java)); startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
finish(); finish();

View file

@ -21,10 +21,8 @@ import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.dp import com.futo.platformplayer.dp
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.setNavigationBarColorAndIcons
@ -32,8 +30,10 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.LoaderOverlay import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.Store import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
import com.futo.polycentric.core.toBase64Url import com.futo.polycentric.core.toBase64Url
import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.futo.polycentric.core.toURLInfoSystemLinkUrl
@ -145,7 +145,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {
processHandle.fullyBackfillClient(PolycentricCache.SERVER) processHandle.fullyBackfillClient(ApiMethods.SERVER)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
updateUI(); updateUI();

View file

@ -101,7 +101,8 @@ class SyncHomeActivity : AppCompatActivity() {
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView { private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
val connected = session?.connected ?: false val connected = session?.connected ?: false
syncDeviceView.setLinkType(if (connected) LinkType.Local else LinkType.None) syncDeviceView.setLinkType(if (connected) LinkType.Local else LinkType.None)
.setName(publicKey) .setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
//TODO: also display public key?
.setStatus(if (connected) "Connected" else "Disconnected") .setStatus(if (connected) "Connected" else "Disconnected")
return syncDeviceView return syncDeviceView
} }

View file

@ -73,7 +73,7 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent") Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
current += bytesToSend.toLong() current += bytesToSend.toLong()
if (current >= end) { if (current > end) {
Logger.i(TAG, "Expected amount of bytes sent") Logger.i(TAG, "Expected amount of bytes sent")
break break
} }

View file

@ -13,7 +13,8 @@ class AudioUrlSource(
override val codec: String = "", override val codec: String = "",
override val language: String = Language.UNKNOWN, override val language: String = Language.UNKNOWN,
override val duration: Long? = null, override val duration: Long? = null,
override var priority: Boolean = false override var priority: Boolean = false,
override var original: Boolean = false
) : IAudioUrlSource, IStreamMetaDataSource{ ) : IAudioUrlSource, IStreamMetaDataSource{
override var streamMetaData: StreamMetaData? = null; override var streamMetaData: StreamMetaData? = null;
@ -36,7 +37,9 @@ class AudioUrlSource(
source.container, source.container,
source.codec, source.codec,
source.language, source.language,
source.duration source.duration,
source.priority,
source.original
); );
ret.streamMetaData = streamData; ret.streamMetaData = streamData;

View file

@ -27,6 +27,7 @@ class HLSVariantAudioUrlSource(
override val language: String, override val language: String,
override val duration: Long?, override val duration: Long?,
override val priority: Boolean, override val priority: Boolean,
override val original: Boolean,
val url: String val url: String
) : IAudioUrlSource { ) : IAudioUrlSource {
override fun getAudioUrl(): String { override fun getAudioUrl(): String {

View file

@ -8,4 +8,5 @@ interface IAudioSource {
val language : String; val language : String;
val duration : Long?; val duration : Long?;
val priority: Boolean; val priority: Boolean;
val original: Boolean;
} }

View file

@ -15,6 +15,7 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
override val duration: Long? = null; override val duration: Long? = null;
override var priority: Boolean = false; override var priority: Boolean = false;
override val original: Boolean = false;
val filePath : String; val filePath : String;
val fileSize: Long; val fileSize: Long;
@ -33,13 +34,13 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
} }
companion object { companion object {
fun fromSource(source: IAudioSource, path: String, fileSize: Long): LocalAudioSource { fun fromSource(source: IAudioSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalAudioSource {
return LocalAudioSource( return LocalAudioSource(
source.name, source.name,
path, path,
fileSize, fileSize,
source.bitrate, source.bitrate,
source.container, overrideContainer ?: source.container,
source.codec, source.codec,
source.language source.language
); );

View file

@ -35,7 +35,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
} }
companion object { companion object {
fun fromSource(source: IVideoSource, path: String, fileSize: Long): LocalVideoSource { fun fromSource(source: IVideoSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalVideoSource {
return LocalVideoSource( return LocalVideoSource(
source.name, source.name,
path, path,
@ -43,7 +43,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
source.width, source.width,
source.height, source.height,
source.duration, source.duration,
source.container, overrideContainer ?: source.container,
source.codec, source.codec,
source.bitrate?:0 source.bitrate?:0
); );

View file

@ -21,6 +21,8 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
override var priority: Boolean = false; override var priority: Boolean = false;
override var original: Boolean = false;
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
val contextName = "AudioUrlSource"; val contextName = "AudioUrlSource";
val config = plugin.config; val config = plugin.config;
@ -35,6 +37,7 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}"; name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}";
priority = if(_obj.has("priority")) obj.getOrThrow(config, "priority", contextName) else false; priority = if(_obj.has("priority")) obj.getOrThrow(config, "priority", contextName) else false;
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
} }
override fun getAudioUrl() : String { override fun getAudioUrl() : String {

View file

@ -4,6 +4,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
import com.futo.platformplayer.api.media.platforms.js.DevJSClient import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
@ -14,13 +16,14 @@ import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.others.Language import com.futo.platformplayer.others.Language
import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StateDeveloper
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource { class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
override val container : String = "application/dash+xml"; override val container : String;
override val name : String; override val name : String;
override val codec: String; override val codec: String;
override val bitrate: Int; override val bitrate: Int;
override val duration: Long; override val duration: Long;
override val priority: Boolean; override val priority: Boolean;
override var original: Boolean = false;
override val language: String; override val language: String;
@ -29,17 +32,21 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
override val hasGenerate: Boolean; override val hasGenerate: Boolean;
override var streamMetaData: StreamMetaData? = null;
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
val contextName = "DashRawSource"; val contextName = "DashRawSource";
val config = plugin.config; val config = plugin.config;
name = _obj.getOrThrow(config, "name", contextName); name = _obj.getOrThrow(config, "name", contextName);
url = _obj.getOrThrow(config, "url", contextName); url = _obj.getOrThrow(config, "url", contextName);
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
manifest = _obj.getOrThrow(config, "manifest", contextName); manifest = _obj.getOrThrow(config, "manifest", contextName);
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: ""; codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0; bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0; duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false; priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN; language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
hasGenerate = _obj.has("generate"); hasGenerate = _obj.has("generate");
} }
@ -50,15 +57,28 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
throw IllegalStateException("Source object already closed"); throw IllegalStateException("Source object already closed");
val plugin = _plugin.getUnderlyingPlugin(); val plugin = _plugin.getUnderlyingPlugin();
var result: String? = null;
if(_plugin is DevJSClient) if(_plugin is DevJSClient)
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) { result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_obj.invokeString("generate"); _obj.invokeString("generate");
} }
} }
else else
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") { result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
_obj.invokeString("generate"); _obj.invokeString("generate");
} }
if(result != null){
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
}
}
return result;
} }
} }

View file

@ -6,6 +6,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
import com.futo.platformplayer.api.media.platforms.js.DevJSClient import com.futo.platformplayer.api.media.platforms.js.DevJSClient
import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.engine.IV8PluginConfig import com.futo.platformplayer.engine.IV8PluginConfig
@ -20,8 +22,8 @@ interface IJSDashManifestRawSource {
var manifest: String?; var manifest: String?;
fun generate(): String?; fun generate(): String?;
} }
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource { open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
override val container : String = "application/dash+xml"; override val container : String;
override val name : String; override val name : String;
override val width: Int; override val width: Int;
override val height: Int; override val height: Int;
@ -36,11 +38,14 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
override val hasGenerate: Boolean; override val hasGenerate: Boolean;
val canMerge: Boolean; val canMerge: Boolean;
override var streamMetaData: StreamMetaData? = null;
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
val contextName = "DashRawSource"; val contextName = "DashRawSource";
val config = plugin.config; val config = plugin.config;
name = _obj.getOrThrow(config, "name", contextName); name = _obj.getOrThrow(config, "name", contextName);
url = _obj.getOrThrow(config, "url", contextName); url = _obj.getOrThrow(config, "url", contextName);
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null); manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0; width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0; height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
@ -57,17 +62,30 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
return manifest; return manifest;
if(_obj.isClosed) if(_obj.isClosed)
throw IllegalStateException("Source object already closed"); throw IllegalStateException("Source object already closed");
var result: String? = null;
if(_plugin is DevJSClient) { if(_plugin is DevJSClient) {
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") { result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_obj.invokeString("generate"); _obj.invokeString("generate");
}); });
} }
} }
else else
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", { result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
_obj.invokeString("generate"); _obj.invokeString("generate");
}); });
if(result != null){
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
}
}
return result;
} }
} }
@ -100,12 +118,16 @@ class JSDashManifestMergingRawSource(
if(videoDash == null) return null; if(videoDash == null) return null;
//TODO: Temporary simple solution..make more reliable version //TODO: Temporary simple solution..make more reliable version
var result: String? = null;
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!); val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
if(audioAdaptationSet != null) { if(audioAdaptationSet != null) {
return videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value) result = videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
} }
else else
return videoDash; result = videoDash;
return result;
} }
companion object { companion object {

View file

@ -21,6 +21,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
override val language: String; override val language: String;
override var priority: Boolean = false; override var priority: Boolean = false;
override var original: Boolean = false;
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) { constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
val contextName = "HLSAudioSource"; val contextName = "HLSAudioSource";
@ -32,6 +33,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
language = _obj.getOrThrow(config, "language", contextName); language = _obj.getOrThrow(config, "language", contextName);
priority = obj.getOrNull(config, "priority", contextName) ?: false; priority = obj.getOrNull(config, "priority", contextName) ?: false;
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
} }

View file

@ -64,7 +64,7 @@ class StateCasting {
private val _scopeMain = CoroutineScope(Dispatchers.Main); private val _scopeMain = CoroutineScope(Dispatchers.Main);
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get(); private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
private val _castServer = ManagedHttpServer(9999); private val _castServer = ManagedHttpServer();
private var _started = false; private var _started = false;
var devices: HashMap<String, CastingDevice> = hashMapOf(); var devices: HashMap<String, CastingDevice> = hashMapOf();

View file

@ -22,7 +22,6 @@ import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComm
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.dp import com.futo.platformplayer.dp
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
@ -30,6 +29,7 @@ import com.futo.platformplayer.states.StatePolycentric
import com.futo.polycentric.core.ClaimType import com.futo.polycentric.core.ClaimType
import com.futo.polycentric.core.Store import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton

View file

@ -141,11 +141,17 @@ class VideoDownload {
var error: String? = null; var error: String? = null;
var videoFilePath: String? = null; var videoFilePath: String? = null;
var videoFileName: String? = null; var videoFileNameBase: String? = null;
var videoFileNameExt: String? = null;
val videoFileName: String? get() = if(videoFileNameBase.isNullOrEmpty()) null else videoFileNameBase + (if(!videoFileNameExt.isNullOrEmpty()) "." + videoFileNameExt else "");
var videoOverrideContainer: String? = null;
var videoFileSize: Long? = null; var videoFileSize: Long? = null;
var audioFilePath: String? = null; var audioFilePath: String? = null;
var audioFileName: String? = null; var audioFileNameBase: String? = null;
var audioFileNameExt: String? = null;
val audioFileName: String? get() = if(audioFileNameBase.isNullOrEmpty()) null else audioFileNameBase + (if(!audioFileNameExt.isNullOrEmpty()) "." + audioFileNameExt else "");
var audioOverrideContainer: String? = null;
var audioFileSize: Long? = null; var audioFileSize: Long? = null;
var subtitleFilePath: String? = null; var subtitleFilePath: String? = null;
@ -235,11 +241,13 @@ class VideoDownload {
videoDetails = null; videoDetails = null;
videoSource = null; videoSource = null;
videoSourceLive = null; videoSourceLive = null;
videoOverrideContainer = null;
} }
if(requiresLiveAudioSource && !isLiveAudioSourceValid) { if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
videoDetails = null; videoDetails = null;
audioSource = null; audioSource = null;
videoSourceLive = null; videoSourceLive = null;
audioOverrideContainer = null;
} }
if(video == null && videoDetails == null) if(video == null && videoDetails == null)
throw IllegalStateException("Missing information for download to complete"); throw IllegalStateException("Missing information for download to complete");
@ -367,8 +375,8 @@ class VideoDownload {
else throw DownloadException("Could not find a valid video or audio source for download") else throw DownloadException("Could not find a valid video or audio source for download")
if(asource is JSSource) { if(asource is JSSource) {
this.hasVideoRequestExecutor = this.hasVideoRequestExecutor || asource.hasRequestExecutor; this.hasAudioRequestExecutor = this.hasAudioRequestExecutor || asource.hasRequestExecutor;
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate); this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
} }
if(asource == null) { if(asource == null) {
@ -410,11 +418,13 @@ class VideoDownload {
else audioSource; else audioSource;
if(actualVideoSource != null) { if(actualVideoSource != null) {
videoFileName = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}].${videoContainerToExtension(actualVideoSource!!.container)}".sanitizeFileName(); videoFileNameBase = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}]".sanitizeFileName();
videoFileNameExt = videoContainerToExtension(actualVideoSource!!.container);
videoFilePath = File(downloadDir, videoFileName!!).absolutePath; videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
} }
if(actualAudioSource != null) { if(actualAudioSource != null) {
audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(actualAudioSource!!.container)}".sanitizeFileName(); audioFileNameBase = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}]".sanitizeFileName();
audioFileNameExt = audioContainerToExtension(actualAudioSource!!.container);
audioFilePath = File(downloadDir, audioFileName!!).absolutePath; audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
} }
if(subtitleSource != null) { if(subtitleSource != null) {
@ -1058,8 +1068,8 @@ class VideoDownload {
fun complete() { fun complete() {
Logger.i(TAG, "VideoDownload Complete [${name}]"); Logger.i(TAG, "VideoDownload Complete [${name}]");
val existing = StateDownloads.instance.getCachedVideo(id); val existing = StateDownloads.instance.getCachedVideo(id);
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0) }; val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0, videoOverrideContainer) };
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0) }; val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0, audioOverrideContainer) };
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) }; val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource) if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
@ -1140,7 +1150,7 @@ class VideoDownload {
else if (container.contains("video/x-matroska")) else if (container.contains("video/x-matroska"))
return "mkv"; return "mkv";
else else
return "video"; return "video";//throw IllegalStateException("Unknown container: " + container)
} }
fun audioContainerToExtension(container: String): String { fun audioContainerToExtension(container: String): String {
@ -1151,11 +1161,11 @@ class VideoDownload {
else if (container.contains("audio/mp3")) else if (container.contains("audio/mp3"))
return "mp3"; return "mp3";
else if (container.contains("audio/webm")) else if (container.contains("audio/webm"))
return "webma"; return "webm";
else if (container == "application/vnd.apple.mpegurl") else if (container == "application/vnd.apple.mpegurl")
return "mp4"; return "mp4a";
else else
return "audio"; return "audio";// throw IllegalStateException("Unknown container: " + container)
} }
fun subtitleContainerToExtension(container: String?): String { fun subtitleContainerToExtension(container: String?): String {

View file

@ -39,7 +39,7 @@ class VideoExport {
this.subtitleSource = subtitleSource; this.subtitleSource = subtitleSource;
} }
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope { suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null, documentRoot: DocumentFile? = null): DocumentFile = coroutineScope {
val v = videoSource; val v = videoSource;
val a = audioSource; val a = audioSource;
val s = subtitleSource; val s = subtitleSource;
@ -50,7 +50,7 @@ class VideoExport {
if (s != null) sourceCount++; if (s != null) sourceCount++;
val outputFile: DocumentFile?; val outputFile: DocumentFile?;
val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set"); val downloadRoot = documentRoot ?: StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
if (sourceCount > 1) { if (sourceCount > 1) {
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container); val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
val f = downloadRoot.createFile("video/mp4", outputFileName) val f = downloadRoot.createFile("video/mp4", outputFileName)

View file

@ -13,7 +13,6 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.dp import com.futo.platformplayer.dp
import com.futo.platformplayer.fixHtmlLinks import com.futo.platformplayer.fixHtmlLinks
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.resolveChannelUrl import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
@ -21,6 +20,7 @@ import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.toHumanNumber import com.futo.platformplayer.toHumanNumber
import com.futo.platformplayer.views.platform.PlatformLinkView import com.futo.platformplayer.views.platform.PlatformLinkView
import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.toName import com.futo.polycentric.core.toName
import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.futo.polycentric.core.toURLInfoSystemLinkUrl
@ -134,9 +134,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
} }
} }
if(!map.containsKey("Harbor")) if(!map.containsKey("Harbor"))
this.context?.let { map.set("Harbor", polycentricProfile.getHarborUrl());
map.set("Harbor", polycentricProfile.getHarborUrl(it));
}
if (map.isNotEmpty()) if (map.isNotEmpty())
setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "") setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "")

View file

@ -29,7 +29,6 @@ import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.exceptions.ChannelException import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.fragment.mainactivity.main.FeedView import com.futo.platformplayer.fragment.mainactivity.main.FeedView
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateCache import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
@ -39,6 +38,7 @@ import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
import com.futo.polycentric.core.PolycentricProfile
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.max import kotlin.math.max

View file

@ -16,12 +16,12 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.resolveChannelUrl import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.viewholders.CreatorViewHolder import com.futo.platformplayer.views.adapters.viewholders.CreatorViewHolder
import com.futo.polycentric.core.PolycentricProfile
class ChannelListFragment : Fragment, IChannelTabFragment { class ChannelListFragment : Fragment, IChannelTabFragment {
private var _channels: ArrayList<IPlatformChannel> = arrayListOf(); private var _channels: ArrayList<IPlatformChannel> = arrayListOf();

View file

@ -8,8 +8,8 @@ import android.widget.TextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.views.SupportView import com.futo.platformplayer.views.SupportView
import com.futo.polycentric.core.PolycentricProfile
class ChannelMonetizationFragment : Fragment, IChannelTabFragment { class ChannelMonetizationFragment : Fragment, IChannelTabFragment {

View file

@ -1,7 +1,7 @@
package com.futo.platformplayer.fragment.channel.tab package com.futo.platformplayer.fragment.channel.tab
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile import com.futo.polycentric.core.PolycentricProfile
interface IChannelTabFragment { interface IChannelTabFragment {
fun setChannel(channel: IPlatformChannel) fun setChannel(channel: IPlatformChannel)

View file

@ -42,7 +42,6 @@ import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SearchType import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.selectHighestResolutionImage import com.futo.platformplayer.selectHighestResolutionImage
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
@ -55,29 +54,14 @@ import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter
import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.subscriptions.SubscribeButton import com.futo.platformplayer.views.subscriptions.SubscribeButton
import com.futo.polycentric.core.OwnedClaim import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.PublicKey import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
@Serializable
data class PolycentricProfile(
val system: PublicKey, val systemState: SystemState, val ownedClaims: List<OwnedClaim>
) {
fun getHarborUrl(context: Context): String{
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system));
val url = system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable());
return "https://harbor.social/" + url.substring("polycentric://".length);
}
}
class ChannelFragment : MainFragment() { class ChannelFragment : MainFragment() {
override val isMainView: Boolean = true override val isMainView: Boolean = true
@ -144,15 +128,14 @@ class ChannelFragment : MainFragment() {
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {} private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
private val _taskLoadPolycentricProfile: TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?> private val _taskLoadPolycentricProfile: TaskHandler<PlatformID, PolycentricProfile?>
private val _taskGetChannel: TaskHandler<String, IPlatformChannel> private val _taskGetChannel: TaskHandler<String, IPlatformChannel>
init { init {
inflater.inflate(R.layout.fragment_channel, this) inflater.inflate(R.layout.fragment_channel, this)
_taskLoadPolycentricProfile = _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>({ fragment.lifecycleScope },
TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>({ fragment.lifecycleScope },
{ id -> { id ->
return@TaskHandler PolycentricCache.instance.getProfileAsync(id) return@TaskHandler ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, id.claimFieldType.toLong(), id.claimType.toLong(), id.value!!)
}).success { setPolycentricProfile(it, animate = true) }.exception<Throwable> { }).success { setPolycentricProfile(it, animate = true) }.exception<Throwable> {
Logger.w(TAG, "Failed to load polycentric profile.", it) Logger.w(TAG, "Failed to load polycentric profile.", it)
} }
@ -238,8 +221,8 @@ class ChannelFragment : MainFragment() {
} }
adapter.onAddToWatchLaterClicked.subscribe { content -> adapter.onAddToWatchLaterClicked.subscribe { content ->
if (content is IPlatformVideo) { if (content is IPlatformVideo) {
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true) if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
UIDialogs.toast("Added to watch later\n[${content.name}]") UIDialogs.toast("Added to watch later\n[${content.name}]")
} }
} }
adapter.onUrlClicked.subscribe { url -> adapter.onUrlClicked.subscribe { url ->
@ -328,7 +311,7 @@ class ChannelFragment : MainFragment() {
_creatorThumbnail.setThumbnail(parameter.thumbnail, true) _creatorThumbnail.setThumbnail(parameter.thumbnail, true)
Glide.with(_imageBanner).clear(_imageBanner) Glide.with(_imageBanner).clear(_imageBanner)
loadPolycentricProfile(parameter.id, parameter.url) loadPolycentricProfile(parameter.id)
} }
_url = parameter.url _url = parameter.url
@ -342,7 +325,7 @@ class ChannelFragment : MainFragment() {
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true) _creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true)
Glide.with(_imageBanner).clear(_imageBanner) Glide.with(_imageBanner).clear(_imageBanner)
loadPolycentricProfile(parameter.channel.id, parameter.channel.url) loadPolycentricProfile(parameter.channel.id)
} }
_url = parameter.channel.url _url = parameter.channel.url
@ -359,16 +342,8 @@ class ChannelFragment : MainFragment() {
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex)) _tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
} }
private fun loadPolycentricProfile(id: PlatformID, url: String) { private fun loadPolycentricProfile(id: PlatformID) {
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true) _taskLoadPolycentricProfile.run(id)
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = true)
if (cachedPolycentricProfile.expired) {
_taskLoadPolycentricProfile.run(id)
}
} else {
_taskLoadPolycentricProfile.run(id)
}
} }
private fun setLoading(isLoading: Boolean) { private fun setLoading(isLoading: Boolean) {
@ -533,20 +508,13 @@ class ChannelFragment : MainFragment() {
private fun setPolycentricProfileOr(url: String, or: () -> Unit) { private fun setPolycentricProfileOr(url: String, or: () -> Unit) {
setPolycentricProfile(null, animate = false) setPolycentricProfile(null, animate = false)
or()
val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(url) }
if (cachedProfile != null) {
setPolycentricProfile(cachedProfile, animate = false)
} else {
or()
}
} }
private fun setPolycentricProfile( private fun setPolycentricProfile(
cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean profile: PolycentricProfile?, animate: Boolean
) { ) {
val dp35 = 35.dp(resources) val dp35 = 35.dp(resources)
val profile = cachedPolycentricProfile?.profile
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)?.let { val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)?.let {
it.toURLInfoSystemLinkUrl( it.toURLInfoSystemLinkUrl(
profile.system.toProto(), it.process, profile.systemState.servers.toList() profile.system.toProto(), it.process, profile.systemState.servers.toList()

View file

@ -23,7 +23,6 @@ import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
@ -32,6 +31,7 @@ import com.futo.platformplayer.views.adapters.CommentWithReferenceViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.overlays.RepliesOverlay import com.futo.platformplayer.views.overlays.RepliesOverlay
import com.futo.polycentric.core.PublicKey import com.futo.polycentric.core.PublicKey
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.UnknownHostException import java.net.UnknownHostException

View file

@ -82,8 +82,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
}; };
adapter.onAddToWatchLaterClicked.subscribe(this) { adapter.onAddToWatchLaterClicked.subscribe(this) {
if(it is IPlatformVideo) { if(it is IPlatformVideo) {
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true); if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
UIDialogs.toast("Added to watch later\n[${it.name}]"); UIDialogs.toast("Added to watch later\n[${it.name}]");
} }
}; };
adapter.onLongPress.subscribe(this) { adapter.onLongPress.subscribe(this) {

View file

@ -10,6 +10,7 @@ import android.widget.EditText
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.Spinner import android.widget.Spinner
import android.widget.TextView
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -26,6 +27,7 @@ class CreatorsFragment : MainFragment() {
private var _overlayContainer: FrameLayout? = null; private var _overlayContainer: FrameLayout? = null;
private var _containerSearch: FrameLayout? = null; private var _containerSearch: FrameLayout? = null;
private var _editSearch: EditText? = null; private var _editSearch: EditText? = null;
private var _textMeta: TextView? = null;
private var _buttonClearSearch: ImageButton? = null private var _buttonClearSearch: ImageButton? = null
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@ -34,6 +36,7 @@ class CreatorsFragment : MainFragment() {
val editSearch: EditText = view.findViewById(R.id.edit_search); val editSearch: EditText = view.findViewById(R.id.edit_search);
val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search) val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search)
_editSearch = editSearch _editSearch = editSearch
_textMeta = view.findViewById(R.id.text_meta);
_buttonClearSearch = buttonClearSearch _buttonClearSearch = buttonClearSearch
buttonClearSearch.setOnClickListener { buttonClearSearch.setOnClickListener {
editSearch.text.clear() editSearch.text.clear()
@ -41,7 +44,11 @@ class CreatorsFragment : MainFragment() {
_buttonClearSearch?.visibility = View.INVISIBLE; _buttonClearSearch?.visibility = View.INVISIBLE;
} }
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription)); val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription)) { subs ->
_textMeta?.let {
it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}";
}
};
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) }; adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
adapter.onSettings.subscribe { sub -> _overlayContainer?.let { UISlideOverlays.showSubscriptionOptionsOverlay(sub, it) } } adapter.onSettings.subscribe { sub -> _overlayContainer?.let { UISlideOverlays.showSubscriptionOptionsOverlay(sub, it) } }

View file

@ -22,6 +22,7 @@ import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.toHumanBytesSize import com.futo.platformplayer.toHumanBytesSize
import com.futo.platformplayer.toHumanDuration
import com.futo.platformplayer.views.AnyInsertedAdapterView import com.futo.platformplayer.views.AnyInsertedAdapterView
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder
@ -215,7 +216,7 @@ class DownloadsFragment : MainFragment() {
_listDownloadedHeader.visibility = GONE; _listDownloadedHeader.visibility = GONE;
} else { } else {
_listDownloadedHeader.visibility = VISIBLE; _listDownloadedHeader.visibility = VISIBLE;
_listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()})"; _listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()}${if(downloaded.size > 0) ", ${downloaded.sumOf { it.duration }.toHumanDuration(false)}" else ""})";
} }
lastDownloads = downloaded; lastDownloads = downloaded;

View file

@ -23,6 +23,7 @@ import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.NoResultsView import com.futo.platformplayer.views.NoResultsView
import com.futo.platformplayer.views.ToggleBar
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder import com.futo.platformplayer.views.adapters.InsertedViewHolder
@ -94,6 +95,8 @@ class HomeFragment : MainFragment() {
class HomeView : ContentFeedView<HomeFragment> { class HomeView : ContentFeedView<HomeFragment> {
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle(); override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
private var _toggleBar: ToggleBar? = null;
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>; private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
@ -127,6 +130,8 @@ class HomeFragment : MainFragment() {
}, fragment); }, fragment);
}; };
initializeToolbarContent();
setPreviewsEnabled(Settings.instance.home.previewFeedItems); setPreviewsEnabled(Settings.instance.home.previewFeedItems);
showAnnouncementView() showAnnouncementView()
} }
@ -201,13 +206,43 @@ class HomeFragment : MainFragment() {
loadResults(); loadResults();
} }
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> { private val _filterLock = Object();
return results.filter { !StateMeta.instance.isVideoHidden(it.url) && !StateMeta.instance.isCreatorHidden(it.author.url) }; private var _toggleRecent = false;
fun initializeToolbarContent() {
//Not stable enough with current viewport paging, doesn't work with less results, and reloads content instead of just re-filtering existing
/*
_toggleBar = ToggleBar(context).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
synchronized(_filterLock) {
_toggleBar?.setToggles(
//TODO: loadResults needs to be replaced with an internal reload of the current content
ToggleBar.Toggle("Recent", _toggleRecent) { _toggleRecent = it; loadResults(false) }
)
}
_toolbarContentView.addView(_toggleBar, 0);
*/
} }
private fun loadResults() { override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
return results.filter {
if(StateMeta.instance.isVideoHidden(it.url))
return@filter false;
if(StateMeta.instance.isCreatorHidden(it.author.url))
return@filter false;
if(_toggleRecent && (it.datetime?.getNowDiffHours() ?: 0) > 23) {
return@filter false;
}
return@filter true;
};
}
private fun loadResults(withRefetch: Boolean = true) {
setLoading(true); setLoading(true);
_taskGetPager.run(true); _taskGetPager.run(withRefetch);
} }
private fun loadedResult(pager : IPager<IPlatformContent>) { private fun loadedResult(pager : IPager<IPlatformContent>) {
if (pager is EmptyPager<IPlatformContent>) { if (pager is EmptyPager<IPlatformContent>) {

View file

@ -8,6 +8,7 @@ import android.view.ViewGroup
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
@ -78,6 +79,14 @@ class PlaylistFragment : MainFragment() {
val nameInput = SlideUpMenuTextInput(context, context.getString(R.string.name)); val nameInput = SlideUpMenuTextInput(context, context.getString(R.string.name));
val editPlaylistOverlay = SlideUpMenuOverlay(context, overlayContainer, context.getString(R.string.edit_playlist), context.getString(R.string.ok), false, nameInput); val editPlaylistOverlay = SlideUpMenuOverlay(context, overlayContainer, context.getString(R.string.edit_playlist), context.getString(R.string.ok), false, nameInput);
_buttonExport.setOnClickListener {
_playlist?.let {
val context = StateApp.instance.contextOrNull ?: return@let;
if(context is IWithResultLauncher)
StateDownloads.instance.exportPlaylist(context, it.id);
}
};
_buttonDownload.visibility = View.VISIBLE; _buttonDownload.visibility = View.VISIBLE;
editPlaylistOverlay.onOK.subscribe { editPlaylistOverlay.onOK.subscribe {
val text = nameInput.text; val text = nameInput.text;
@ -176,6 +185,7 @@ class PlaylistFragment : MainFragment() {
setVideos(parameter.videos, true) setVideos(parameter.videos, true)
setMetadata(parameter.videos.size, parameter.videos.sumOf { it.duration }) setMetadata(parameter.videos.size, parameter.videos.sumOf { it.duration })
setButtonDownloadVisible(true) setButtonDownloadVisible(true)
setButtonExportVisible(false)
setButtonEditVisible(true) setButtonEditVisible(true)
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) { if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {

View file

@ -33,10 +33,8 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp import com.futo.platformplayer.dp
import com.futo.platformplayer.fixHtmlWhitespace import com.futo.platformplayer.fixHtmlWhitespace
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
@ -47,7 +45,6 @@ import com.futo.platformplayer.views.adapters.ChannelTab
import com.futo.platformplayer.views.adapters.feedtypes.PreviewPostView import com.futo.platformplayer.views.adapters.feedtypes.PreviewPostView
import com.futo.platformplayer.views.comments.AddCommentView import com.futo.platformplayer.views.comments.AddCommentView
import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.others.Toggle
import com.futo.platformplayer.views.overlays.RepliesOverlay import com.futo.platformplayer.views.overlays.RepliesOverlay
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
import com.futo.platformplayer.views.platform.PlatformIndicator import com.futo.platformplayer.views.platform.PlatformIndicator
@ -57,6 +54,8 @@ import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ContentType import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Models import com.futo.polycentric.core.Models
import com.futo.polycentric.core.Opinion import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import com.google.android.flexbox.FlexboxLayout import com.google.android.flexbox.FlexboxLayout
import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.CornerFamily
@ -112,7 +111,7 @@ class PostDetailFragment : MainFragment {
private var _isLoading = false; private var _isLoading = false;
private var _post: IPlatformPostDetails? = null; private var _post: IPlatformPostDetails? = null;
private var _postOverview: IPlatformPost? = null; private var _postOverview: IPlatformPost? = null;
private var _polycentricProfile: PolycentricCache.CachedPolycentricProfile? = null; private var _polycentricProfile: PolycentricProfile? = null;
private var _version = 0; private var _version = 0;
private var _isRepliesVisible: Boolean = false; private var _isRepliesVisible: Boolean = false;
private var _repliesAnimator: ViewPropertyAnimator? = null; private var _repliesAnimator: ViewPropertyAnimator? = null;
@ -169,7 +168,7 @@ class PostDetailFragment : MainFragment {
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost, null, _fragment); UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost, null, _fragment);
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope }; } else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) }) private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) })
.success { it -> setPolycentricProfile(it, animate = true) } .success { it -> setPolycentricProfile(it, animate = true) }
.exception<Throwable> { .exception<Throwable> {
Logger.w(TAG, "Failed to load claims.", it); Logger.w(TAG, "Failed to load claims.", it);
@ -274,7 +273,7 @@ class PostDetailFragment : MainFragment {
}; };
_buttonStore.setOnClickListener { _buttonStore.setOnClickListener {
_polycentricProfile?.profile?.systemState?.store?.let { _polycentricProfile?.systemState?.store?.let {
try { try {
val uri = Uri.parse(it); val uri = Uri.parse(it);
val intent = Intent(Intent.ACTION_VIEW); val intent = Intent(Intent.ACTION_VIEW);
@ -334,7 +333,7 @@ class PostDetailFragment : MainFragment {
} }
try { try {
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null, val queryReferencesResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, ref, null,null,
arrayListOf( arrayListOf(
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType( Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
ContentType.OPINION.value).setValue( ContentType.OPINION.value).setValue(
@ -604,16 +603,8 @@ class PostDetailFragment : MainFragment {
private fun fetchPolycentricProfile() { private fun fetchPolycentricProfile() {
val author = _post?.author ?: _postOverview?.author ?: return; val author = _post?.author ?: _postOverview?.author ?: return;
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(author.url, true);
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = false);
if (cachedPolycentricProfile.expired) {
_taskLoadPolycentricProfile.run(author.id);
}
} else {
setPolycentricProfile(null, animate = false); setPolycentricProfile(null, animate = false);
_taskLoadPolycentricProfile.run(author.id); _taskLoadPolycentricProfile.run(author.id);
}
} }
private fun setChannelMeta(value: IPlatformPost?) { private fun setChannelMeta(value: IPlatformPost?) {
@ -639,17 +630,18 @@ class PostDetailFragment : MainFragment {
_repliesOverlay.cleanup(); _repliesOverlay.cleanup();
} }
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { private fun setPolycentricProfile(polycentricProfile: PolycentricProfile?, animate: Boolean) {
_polycentricProfile = cachedPolycentricProfile; _polycentricProfile = polycentricProfile;
if (cachedPolycentricProfile?.profile == null) { val pp = _polycentricProfile;
if (pp == null) {
_layoutMonetization.visibility = View.GONE; _layoutMonetization.visibility = View.GONE;
_creatorThumbnail.setHarborAvailable(false, animate, null); _creatorThumbnail.setHarborAvailable(false, animate, null);
return; return;
} }
_layoutMonetization.visibility = View.VISIBLE; _layoutMonetization.visibility = View.VISIBLE;
_creatorThumbnail.setHarborAvailable(true, animate, cachedPolycentricProfile.profile.system.toProto()); _creatorThumbnail.setHarborAvailable(true, animate, pp.system.toProto());
} }
private fun fetchPost() { private fun fetchPost() {

View file

@ -556,7 +556,7 @@ class SourceDetailFragment : MainFragment() {
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}"); Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
val config = SourcePluginConfig.fromJson(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."); Logger.i(TAG, "Plugin is up to date.");
withContext(Dispatchers.Main) { UIDialogs.toast(context.getString(R.string.plugin_is_fully_up_to_date)); }; withContext(Dispatchers.Main) { UIDialogs.toast(context.getString(R.string.plugin_is_fully_up_to_date)); };
return@launch; return@launch;

View file

@ -256,6 +256,8 @@ class SubscriptionGroupFragment : MainFragment() {
val sub = StateSubscriptions.instance.getSubscription(sub) ?: StateSubscriptions.instance.getSubscriptionOther(sub); val sub = StateSubscriptions.instance.getSubscription(sub) ?: StateSubscriptions.instance.getSubscriptionOther(sub);
if(sub != null && sub.channel.thumbnail != null) { if(sub != null && sub.channel.thumbnail != null) {
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!); g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
if(g.image != null)
g.image!!.subscriptionUrl = sub.channel.url;
g.image?.setImageView(_imageGroup); g.image?.setImageView(_imageGroup);
g.image?.setImageView(_imageGroupBackground); g.image?.setImageView(_imageGroupBackground);
break; break;

View file

@ -94,12 +94,10 @@ import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.exceptions.UnsupportedCastException import com.futo.platformplayer.exceptions.UnsupportedCastException
import com.futo.platformplayer.fixHtmlLinks import com.futo.platformplayer.fixHtmlLinks
import com.futo.platformplayer.fixHtmlWhitespace import com.futo.platformplayer.fixHtmlWhitespace
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.getNowDiffSeconds import com.futo.platformplayer.getNowDiffSeconds
import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.receivers.MediaControlReceiver import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.AnnouncementType import com.futo.platformplayer.states.AnnouncementType
@ -158,6 +156,8 @@ import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ContentType import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Models import com.futo.polycentric.core.Models
import com.futo.polycentric.core.Opinion import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -294,7 +294,7 @@ class VideoDetailView : ConstraintLayout {
private set; private set;
private var _historicalPosition: Long = 0; private var _historicalPosition: Long = 0;
private var _commentsCount = 0; private var _commentsCount = 0;
private var _polycentricProfile: PolycentricCache.CachedPolycentricProfile? = null; private var _polycentricProfile: PolycentricProfile? = null;
private var _slideUpOverlay: SlideUpMenuOverlay? = null; private var _slideUpOverlay: SlideUpMenuOverlay? = null;
private var _autoplayVideo: IPlatformVideo? = null private var _autoplayVideo: IPlatformVideo? = null
@ -409,12 +409,12 @@ class VideoDetailView : ConstraintLayout {
}; };
_monetization.onSupportTap.subscribe { _monetization.onSupportTap.subscribe {
_container_content_support.setPolycentricProfile(_polycentricProfile?.profile); _container_content_support.setPolycentricProfile(_polycentricProfile);
switchContentView(_container_content_support); switchContentView(_container_content_support);
}; };
_monetization.onStoreTap.subscribe { _monetization.onStoreTap.subscribe {
_polycentricProfile?.profile?.systemState?.store?.let { _polycentricProfile?.systemState?.store?.let {
try { try {
val uri = Uri.parse(it); val uri = Uri.parse(it);
val intent = Intent(Intent.ACTION_VIEW); val intent = Intent(Intent.ACTION_VIEW);
@ -579,6 +579,14 @@ class VideoDetailView : ConstraintLayout {
_minimize_title.setOnClickListener { onMaximize.emit(false) }; _minimize_title.setOnClickListener { onMaximize.emit(false) };
_minimize_meta.setOnClickListener { onMaximize.emit(false) }; _minimize_meta.setOnClickListener { onMaximize.emit(false) };
_player.onStateChange.subscribe {
if (_player.activelyPlaying) {
Logger.i(TAG, "Play changed, resetting error counter _didTriggerDatasourceErrorCount = 0 (_player.activelyPlaying: ${_player.activelyPlaying})")
_didTriggerDatasourceErrorCount = 0;
_didTriggerDatasourceError = false;
}
}
_player.onPlayChanged.subscribe { _player.onPlayChanged.subscribe {
if (StateCasting.instance.activeDevice == null) { if (StateCasting.instance.activeDevice == null) {
handlePlayChanged(it); handlePlayChanged(it);
@ -882,7 +890,7 @@ class VideoDetailView : ConstraintLayout {
_slideUpOverlay?.hide(); _slideUpOverlay?.hide();
} }
else null, else null,
if(!isLimitedVersion) if(!isLimitedVersion && !(video?.isLive ?: false))
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) { RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
video?.let { video?.let {
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver); _slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
@ -922,7 +930,7 @@ class VideoDetailView : ConstraintLayout {
} else if(devices.size == 1){ } else if(devices.size == 1){
val device = devices.first(); val device = devices.first();
Logger.i(TAG, "Send to device? (public key: ${device.remotePublicKey}): " + videoToSend.url) Logger.i(TAG, "Send to device? (public key: ${device.remotePublicKey}): " + videoToSend.url)
UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non ${device.remotePublicKey}" , { UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non '${device.displayName}'" , {
Logger.i(TAG, "Send to device confirmed (public key: ${device.remotePublicKey}): " + videoToSend.url) Logger.i(TAG, "Send to device confirmed (public key: ${device.remotePublicKey}): " + videoToSend.url)
fragment.lifecycleScope.launch(Dispatchers.IO) { fragment.lifecycleScope.launch(Dispatchers.IO) {
@ -963,6 +971,7 @@ class VideoDetailView : ConstraintLayout {
throw IllegalStateException("Expected media content, found ${video.contentType}"); throw IllegalStateException("Expected media content, found ${video.contentType}");
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
_videoResumePositionMilliseconds = _player.position
setVideoDetails(video); setVideoDetails(video);
} }
} }
@ -1227,16 +1236,8 @@ class VideoDetailView : ConstraintLayout {
_creatorThumbnail.setThumbnail(video.author.thumbnail, false); _creatorThumbnail.setThumbnail(video.author.thumbnail, false);
_channelName.text = video.author.name; _channelName.text = video.author.name;
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url, true); setPolycentricProfile(null, animate = false);
if (cachedPolycentricProfile != null) { _taskLoadPolycentricProfile.run(video.author.id);
setPolycentricProfile(cachedPolycentricProfile, animate = false);
if (cachedPolycentricProfile.expired) {
_taskLoadPolycentricProfile.run(video.author.id);
}
} else {
setPolycentricProfile(null, animate = false);
_taskLoadPolycentricProfile.run(video.author.id);
}
_player.clear(); _player.clear();
@ -1265,8 +1266,6 @@ class VideoDetailView : ConstraintLayout {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) { fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
Logger.i(TAG, "setVideoDetails (${videoDetail.name})") Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
_didTriggerDatasourceErrroCount = 0;
_didTriggerDatasourceError = false;
_autoplayVideo = null _autoplayVideo = null
Logger.i(TAG, "Autoplay video cleared (setVideoDetails)") Logger.i(TAG, "Autoplay video cleared (setVideoDetails)")
@ -1277,6 +1276,10 @@ class VideoDetailView : ConstraintLayout {
_lastVideoSource = null; _lastVideoSource = null;
_lastAudioSource = null; _lastAudioSource = null;
_lastSubtitleSource = null; _lastSubtitleSource = null;
Logger.i(TAG, "_didTriggerDatasourceErrorCount reset to 0 because new video")
_didTriggerDatasourceErrorCount = 0;
_didTriggerDatasourceError = false;
} }
if (videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now()) if (videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now())
@ -1394,11 +1397,8 @@ class VideoDetailView : ConstraintLayout {
setTabIndex(2, true) setTabIndex(2, true)
} else { } else {
when (Settings.instance.comments.defaultCommentSection) { when (Settings.instance.comments.defaultCommentSection) {
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex( 0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(0, true) else setTabIndex(1, true)
0, 1 -> setTabIndex(1, true)
true
) else setTabIndex(1, true);
1 -> setTabIndex(1, true);
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true) 2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
} }
} }
@ -1436,16 +1436,8 @@ class VideoDetailView : ConstraintLayout {
_buttonSubscribe.setSubscribeChannel(video.author.url); _buttonSubscribe.setSubscribeChannel(video.author.url);
setDescription(video.description.fixHtmlLinks()); setDescription(video.description.fixHtmlLinks());
_creatorThumbnail.setThumbnail(video.author.thumbnail, false); _creatorThumbnail.setThumbnail(video.author.thumbnail, false);
setPolycentricProfile(null, animate = false);
_taskLoadPolycentricProfile.run(video.author.id);
val cachedPolycentricProfile =
PolycentricCache.instance.getCachedProfile(video.author.url, true);
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = false);
} else {
setPolycentricProfile(null, animate = false);
_taskLoadPolycentricProfile.run(video.author.id);
}
_platform.setPlatformFromClientID(video.id.pluginId); _platform.setPlatformFromClientID(video.id.pluginId);
val subTitleSegments: ArrayList<String> = ArrayList(); val subTitleSegments: ArrayList<String> = ArrayList();
@ -1474,7 +1466,7 @@ class VideoDetailView : ConstraintLayout {
fragment.lifecycleScope.launch(Dispatchers.IO) { fragment.lifecycleScope.launch(Dispatchers.IO) {
try { try {
val queryReferencesResponse = ApiMethods.getQueryReferences( val queryReferencesResponse = ApiMethods.getQueryReferences(
PolycentricCache.SERVER, ref, null, null, ApiMethods.SERVER, ref, null, null,
arrayListOf( arrayListOf(
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder() Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
.setFromType(ContentType.OPINION.value).setValue( .setFromType(ContentType.OPINION.value).setValue(
@ -1490,10 +1482,8 @@ class VideoDetailView : ConstraintLayout {
val likes = queryReferencesResponse.countsList[0]; val likes = queryReferencesResponse.countsList[0];
val dislikes = queryReferencesResponse.countsList[1]; val dislikes = queryReferencesResponse.countsList[1];
val hasLiked = val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/; val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
val hasDisliked =
StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
_rating.visibility = View.VISIBLE; _rating.visibility = View.VISIBLE;
@ -1831,7 +1821,7 @@ class VideoDetailView : ConstraintLayout {
} }
} }
private var _didTriggerDatasourceErrroCount = 0; private var _didTriggerDatasourceErrorCount = 0;
private var _didTriggerDatasourceError = false; private var _didTriggerDatasourceError = false;
private fun onDataSourceError(exception: Throwable) { private fun onDataSourceError(exception: Throwable) {
Logger.e(TAG, "onDataSourceError", exception); Logger.e(TAG, "onDataSourceError", exception);
@ -1841,32 +1831,53 @@ class VideoDetailView : ConstraintLayout {
return; return;
val config = currentVideo.sourceConfig; val config = currentVideo.sourceConfig;
if(_didTriggerDatasourceErrroCount <= 3) { if(_didTriggerDatasourceErrorCount <= 3) {
_didTriggerDatasourceError = true; _didTriggerDatasourceError = true;
_didTriggerDatasourceErrroCount++; _didTriggerDatasourceErrorCount++;
UIDialogs.toast("Detected video error, attempting automatic reload (${_didTriggerDatasourceErrorCount})");
Logger.i(TAG, "Block detected, attempting bypass (_didTriggerDatasourceErrorCount = ${_didTriggerDatasourceErrorCount})");
UIDialogs.toast("Block detected, attempting bypass");
//return; //return;
fragment.lifecycleScope.launch(Dispatchers.IO) { fragment.lifecycleScope.launch(Dispatchers.IO) {
val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await(); try {
val previousVideoSource = _lastVideoSource; val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await();
val previousAudioSource = _lastAudioSource; val previousVideoSource = _lastVideoSource;
val previousAudioSource = _lastAudioSource;
if(newDetails is IPlatformVideoDetails) { if (newDetails is IPlatformVideoDetails) {
val newVideoSource = if(previousVideoSource != null) val newVideoSource = if (previousVideoSource != null)
VideoHelper.selectBestVideoSource(newDetails.video, previousVideoSource.height * previousVideoSource.width, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS); VideoHelper.selectBestVideoSource(
else null; newDetails.video,
val newAudioSource = if(previousAudioSource != null) previousVideoSource.height * previousVideoSource.width,
VideoHelper.selectBestAudioSource(newDetails.video, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS, previousAudioSource.language, previousAudioSource.bitrate.toLong()); FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
else null; );
withContext(Dispatchers.Main) { else null;
video = newDetails; val newAudioSource = if (previousAudioSource != null)
_player.setSource(newVideoSource, newAudioSource, true, true); VideoHelper.selectBestAudioSource(
newDetails.video,
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
previousAudioSource.language,
previousAudioSource.bitrate.toLong()
);
else null;
withContext(Dispatchers.Main) {
video = newDetails;
_player.setSource(newVideoSource, newAudioSource, true, true);
}
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to get video details, attempting retrying without reloading.", e)
fragment.lifecycleScope.launch(Dispatchers.Main) {
video?.let {
_videoResumePositionMilliseconds = _player.position
setVideoDetails(it, false)
}
} }
} }
} }
} }
else if(_didTriggerDatasourceErrroCount > 3) { else if(_didTriggerDatasourceErrorCount > 3) {
UIDialogs.showDialog(context, R.drawable.ic_error_pred, UIDialogs.showDialog(context, R.drawable.ic_error_pred,
context.getString(R.string.media_error), context.getString(R.string.media_error),
context.getString(R.string.the_media_source_encountered_an_unauthorized_error_this_might_be_solved_by_a_plugin_reload_would_you_like_to_reload_experimental), context.getString(R.string.the_media_source_encountered_an_unauthorized_error_this_might_be_solved_by_a_plugin_reload_would_you_like_to_reload_experimental),
@ -2595,8 +2606,13 @@ class VideoDetailView : ConstraintLayout {
onAddToWatchLaterClicked.subscribe(this) { onAddToWatchLaterClicked.subscribe(this) {
if(it is IPlatformVideo) { if(it is IPlatformVideo) {
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true); if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
UIDialogs.toast("Added to watch later\n[${it.name}]"); UIDialogs.toast("Added to watch later\n[${it.name}]");
}
}
onAddToQueueClicked.subscribe(this) {
if(it is IPlatformVideo) {
StatePlayer.instance.addToQueue(it);
} }
} }
}) })
@ -2768,13 +2784,12 @@ class VideoDetailView : ConstraintLayout {
} }
} }
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) { private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
_polycentricProfile = cachedPolycentricProfile; _polycentricProfile = profile
val dp_35 = 35.dp(context.resources) val dp_35 = 35.dp(context.resources)
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35) val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }; ?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }
if (avatar != null) { if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate); _creatorThumbnail.setThumbnail(avatar, animate);
@ -2783,12 +2798,12 @@ class VideoDetailView : ConstraintLayout {
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
} }
val username = cachedPolycentricProfile?.profile?.systemState?.username val username = profile?.systemState?.username
if (username != null) { if (username != null) {
_channelName.text = username _channelName.text = username
} }
_monetization.setPolycentricProfile(cachedPolycentricProfile); _monetization.setPolycentricProfile(profile);
} }
fun setProgressBarOverlayed(isOverlayed: Boolean?) { fun setProgressBarOverlayed(isOverlayed: Boolean?) {
@ -2976,7 +2991,7 @@ class VideoDetailView : ConstraintLayout {
Logger.w(TAG, "Failed to load recommendations.", it); Logger.w(TAG, "Failed to load recommendations.", it);
}; };
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) }) private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) })
.success { it -> setPolycentricProfile(it, animate = true) } .success { it -> setPolycentricProfile(it, animate = true) }
.exception<Throwable> { .exception<Throwable> {
Logger.w(TAG, "Failed to load claims.", it); Logger.w(TAG, "Failed to load claims.", it);

View file

@ -34,6 +34,7 @@ abstract class VideoListEditorView : LinearLayout {
protected var overlayContainer: FrameLayout protected var overlayContainer: FrameLayout
private set; private set;
protected var _buttonDownload: ImageButton; protected var _buttonDownload: ImageButton;
protected var _buttonExport: ImageButton;
private var _buttonShare: ImageButton; private var _buttonShare: ImageButton;
private var _buttonEdit: ImageButton; private var _buttonEdit: ImageButton;
@ -54,6 +55,8 @@ abstract class VideoListEditorView : LinearLayout {
_buttonEdit = findViewById(R.id.button_edit); _buttonEdit = findViewById(R.id.button_edit);
_buttonDownload = findViewById(R.id.button_download); _buttonDownload = findViewById(R.id.button_download);
_buttonDownload.visibility = View.GONE; _buttonDownload.visibility = View.GONE;
_buttonExport = findViewById(R.id.button_export);
_buttonExport.visibility = View.GONE;
_buttonShare = findViewById(R.id.button_share); _buttonShare = findViewById(R.id.button_share);
val onShare = _onShare; val onShare = _onShare;
@ -68,6 +71,7 @@ abstract class VideoListEditorView : LinearLayout {
buttonShuffle.setOnClickListener { onShuffleClick(); }; buttonShuffle.setOnClickListener { onShuffleClick(); };
_buttonEdit.setOnClickListener { onEditClick(); }; _buttonEdit.setOnClickListener { onEditClick(); };
setButtonExportVisible(false);
setButtonDownloadVisible(canEdit()); setButtonDownloadVisible(canEdit());
videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged); videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged);
@ -108,6 +112,7 @@ abstract class VideoListEditorView : LinearLayout {
_buttonDownload.setBackgroundResource(R.drawable.background_button_round); _buttonDownload.setBackgroundResource(R.drawable.background_button_round);
if(isDownloading) { if(isDownloading) {
setButtonExportVisible(false);
_buttonDownload.setImageResource(R.drawable.ic_loader_animated); _buttonDownload.setImageResource(R.drawable.ic_loader_animated);
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() }; _buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
_buttonDownload.setOnClickListener { _buttonDownload.setOnClickListener {
@ -117,6 +122,7 @@ abstract class VideoListEditorView : LinearLayout {
} }
} }
else if(isDownloaded) { else if(isDownloaded) {
setButtonExportVisible(true)
_buttonDownload.setImageResource(R.drawable.ic_download_off); _buttonDownload.setImageResource(R.drawable.ic_download_off);
_buttonDownload.setOnClickListener { _buttonDownload.setOnClickListener {
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), { UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
@ -125,6 +131,7 @@ abstract class VideoListEditorView : LinearLayout {
} }
} }
else { else {
setButtonExportVisible(false);
_buttonDownload.setImageResource(R.drawable.ic_download); _buttonDownload.setImageResource(R.drawable.ic_download);
_buttonDownload.setOnClickListener { _buttonDownload.setOnClickListener {
onDownload(); onDownload();
@ -171,6 +178,9 @@ abstract class VideoListEditorView : LinearLayout {
protected fun setButtonDownloadVisible(isVisible: Boolean) { protected fun setButtonDownloadVisible(isVisible: Boolean) {
_buttonDownload.visibility = if (isVisible) View.VISIBLE else View.GONE; _buttonDownload.visibility = if (isVisible) View.VISIBLE else View.GONE;
} }
protected fun setButtonExportVisible(isVisible: Boolean) {
_buttonExport.visibility = if (isVisible) View.VISIBLE else View.GONE;
}
protected fun setButtonEditVisible(isVisible: Boolean) { protected fun setButtonEditVisible(isVisible: Boolean) {
_buttonEdit.visibility = if (isVisible) View.VISIBLE else View.GONE; _buttonEdit.visibility = if (isVisible) View.VISIBLE else View.GONE;

View file

@ -14,9 +14,9 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.IPlatformClient import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.views.casting.CastButton import com.futo.platformplayer.views.casting.CastButton
import com.futo.polycentric.core.PolycentricProfile
class NavigationTopBarFragment : TopFragment() { class NavigationTopBarFragment : TopFragment() {
private var _buttonBack: ImageButton? = null; private var _buttonBack: ImageButton? = null;

View file

@ -9,6 +9,7 @@ import androidx.media3.datasource.ResolvingDataSource
import androidx.media3.exoplayer.dash.DashMediaSource import androidx.media3.exoplayer.dash.DashMediaSource
import androidx.media3.exoplayer.dash.manifest.DashManifestParser import androidx.media3.exoplayer.dash.manifest.DashManifestParser
import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.MediaSource
import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
@ -85,12 +86,17 @@ class VideoHelper {
return selectBestAudioSource((desc as VideoUnMuxedSourceDescriptor).audioSources.toList(), prefContainers, prefLanguage, targetBitrate); return selectBestAudioSource((desc as VideoUnMuxedSourceDescriptor).audioSources.toList(), prefContainers, prefLanguage, targetBitrate);
} }
fun selectBestAudioSource(altSources : Iterable<IAudioSource>, prefContainers : Array<String>, preferredLanguage: String? = null, targetBitrate: Long? = null) : IAudioSource? { fun selectBestAudioSource(sources : Iterable<IAudioSource>, prefContainers : Array<String>, preferredLanguage: String? = null, targetBitrate: Long? = null) : IAudioSource? {
val hasPriority = sources.any { it.priority };
var altSources = if(hasPriority) sources.filter { it.priority } else sources;
val hasOriginal = altSources.any { it.original };
if(hasOriginal && Settings.instance.playback.preferOriginalAudio)
altSources = altSources.filter { it.original };
val languageToFilter = if(preferredLanguage != null && altSources.any { it.language == preferredLanguage }) { val languageToFilter = if(preferredLanguage != null && altSources.any { it.language == preferredLanguage }) {
preferredLanguage preferredLanguage
} else { } else {
if(altSources.any { it.language == Language.ENGLISH }) if(altSources.any { it.language == Language.ENGLISH })
Language.ENGLISH Language.ENGLISH;
else else
Language.UNKNOWN; Language.UNKNOWN;
} }

View file

@ -1,5 +1,7 @@
package com.futo.platformplayer.images; package com.futo.platformplayer.images;
import static com.futo.platformplayer.Extensions_PolycentricKt.getDataLinkFromUrl;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -12,10 +14,14 @@ import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.bumptech.glide.load.model.MultiModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import com.bumptech.glide.signature.ObjectKey; import com.bumptech.glide.signature.ObjectKey;
import com.futo.platformplayer.polycentric.PolycentricCache; import com.futo.polycentric.core.ApiMethods;
import kotlin.Unit; import kotlin.Unit;
import kotlinx.coroutines.CoroutineScopeKt;
import kotlinx.coroutines.Deferred; import kotlinx.coroutines.Deferred;
import kotlinx.coroutines.Dispatchers;
import userpackage.Protocol;
import java.lang.Exception; import java.lang.Exception;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
@ -60,7 +66,14 @@ public class PolycentricModelLoader implements ModelLoader<String, ByteBuffer> {
@Override @Override
public void loadData(@NonNull Priority priority, @NonNull DataFetcher.DataCallback<? super ByteBuffer> callback) { public void loadData(@NonNull Priority priority, @NonNull DataFetcher.DataCallback<? super ByteBuffer> callback) {
Log.i("PolycentricModelLoader", this._model); Log.i("PolycentricModelLoader", this._model);
_deferred = PolycentricCache.getInstance().getDataAsync(_model);
Protocol.URLInfoDataLink dataLink = getDataLinkFromUrl(_model);
if (dataLink == null) {
callback.onLoadFailed(new Exception("Data link cannot be null"));
return;
}
_deferred = ApiMethods.Companion.getDataFromServerAndReassemble(CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()), dataLink);
_deferred.invokeOnCompletion(throwable -> { _deferred.invokeOnCompletion(throwable -> {
if (throwable != null) { if (throwable != null) {
Log.e("PolycentricModelLoader", "getDataAsync failed throwable: " + throwable.toString()); Log.e("PolycentricModelLoader", "getDataAsync failed throwable: " + throwable.toString());

View file

@ -7,6 +7,7 @@ import android.widget.ImageView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.futo.platformplayer.PresetImages import com.futo.platformplayer.PresetImages
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.logging.Logger
import kotlinx.serialization.Contextual import kotlinx.serialization.Contextual
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import java.io.File import java.io.File
@ -18,7 +19,8 @@ data class ImageVariable(
@Transient @Transient
@Contextual @Contextual
private val bitmap: Bitmap? = null, private val bitmap: Bitmap? = null,
val presetName: String? = null) { val presetName: String? = null,
var subscriptionUrl: String? = null) {
@SuppressLint("DiscouragedApi") @SuppressLint("DiscouragedApi")
fun setImageView(imageView: ImageView, fallbackResId: Int = -1) { fun setImageView(imageView: ImageView, fallbackResId: Int = -1) {
@ -63,7 +65,13 @@ data class ImageVariable(
return ImageVariable(null, null, null, str); return ImageVariable(null, null, null, str);
} }
fun fromFile(file: File): ImageVariable { fun fromFile(file: File): ImageVariable {
return ImageVariable.fromBitmap(BitmapFactory.decodeFile(file.absolutePath)); try {
return ImageVariable.fromBitmap(BitmapFactory.decodeFile(file.absolutePath));
}
catch(ex: Throwable) {
Logger.e("ImageVariable", "Unsupported image format? " + ex.message, ex);
return fromResource(R.drawable.ic_error_pred);
}
} }
} }
} }

View file

@ -125,7 +125,7 @@ class HLS {
return if (source is IHLSManifestSource) { return if (source is IHLSManifestSource) {
listOf() listOf()
} else if (source is IHLSManifestAudioSource) { } else if (source is IHLSManifestAudioSource) {
listOf(HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, url)) listOf(HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, url))
} else { } else {
throw NotImplementedError() throw NotImplementedError()
} }
@ -346,7 +346,7 @@ class HLS {
val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ") val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
return@mapNotNull when (it.type) { return@mapNotNull when (it.type) {
"AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, it.uri) "AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, false, it.uri)
else -> null else -> null
} }
} }

View file

@ -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
}
}
}

View file

@ -6,7 +6,6 @@ import com.futo.platformplayer.api.media.structures.DedupContentPager
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.resolveChannelUrl import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.serializers.PlatformContentSerializer import com.futo.platformplayer.serializers.PlatformContentSerializer
import com.futo.platformplayer.stores.db.ManagedDBStore import com.futo.platformplayer.stores.db.ManagedDBStore
@ -50,14 +49,7 @@ class StateCache {
val subs = StateSubscriptions.instance.getSubscriptions(); val subs = StateSubscriptions.instance.getSubscriptions();
Logger.i(TAG, "Subscriptions CachePager polycentric urls"); Logger.i(TAG, "Subscriptions CachePager polycentric urls");
val allUrls = subs val allUrls = subs
.map { .map { it.channel.url }
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
if(!otherUrls.contains(it.channel.url))
return@map listOf(listOf(it.channel.url), otherUrls).flatten();
else
return@map otherUrls;
}
.flatten()
.distinct() .distinct()
.filter { StatePlatform.instance.hasEnabledChannelClient(it) }; .filter { StatePlatform.instance.hasEnabledChannelClient(it) };

View file

@ -3,9 +3,11 @@ package com.futo.platformplayer.states
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.os.StatFs import android.os.StatFs
import androidx.documentfile.provider.DocumentFile
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
@ -46,6 +48,17 @@ class StateDownloads {
private val _downloadsStat = StatFs(_downloadsDirectory.absolutePath); private val _downloadsStat = StatFs(_downloadsDirectory.absolutePath);
private val _downloaded = FragmentedStorage.storeJson<VideoLocal>("downloaded") private val _downloaded = FragmentedStorage.storeJson<VideoLocal>("downloaded")
.withOnModified({
synchronized(_downloadedSet) {
if(!_downloadedSet.contains(it.id))
_downloadedSet.add(it.id);
}
}, {
synchronized(_downloadedSet) {
if(_downloadedSet.contains(it.id))
_downloadedSet.remove(it.id);
}
})
.load() .load()
.apply { afterLoadingDownloaded(this) }; .apply { afterLoadingDownloaded(this) };
private val _downloading = FragmentedStorage.storeJson<VideoDownload>("downloading") private val _downloading = FragmentedStorage.storeJson<VideoDownload>("downloading")
@ -85,9 +98,6 @@ class StateDownloads {
Logger.i("StateDownloads", "Deleting local video ${id.value}"); Logger.i("StateDownloads", "Deleting local video ${id.value}");
val downloaded = getCachedVideo(id); val downloaded = getCachedVideo(id);
if(downloaded != null) { if(downloaded != null) {
synchronized(_downloadedSet) {
_downloadedSet.remove(id);
}
_downloaded.delete(downloaded); _downloaded.delete(downloaded);
} }
onDownloadedChanged.emit(); onDownloadedChanged.emit();
@ -261,9 +271,6 @@ class StateDownloads {
if(existing.groupID == null) { if(existing.groupID == null) {
existing.groupID = VideoDownload.GROUP_WATCHLATER; existing.groupID = VideoDownload.GROUP_WATCHLATER;
existing.groupType = VideoDownload.GROUP_WATCHLATER; existing.groupType = VideoDownload.GROUP_WATCHLATER;
synchronized(_downloadedSet) {
_downloadedSet.add(existing.id);
}
_downloaded.save(existing); _downloaded.save(existing);
} }
} }
@ -306,9 +313,6 @@ class StateDownloads {
if(existing.groupID == null) { if(existing.groupID == null) {
existing.groupID = playlist.id; existing.groupID = playlist.id;
existing.groupType = VideoDownload.GROUP_PLAYLIST; existing.groupType = VideoDownload.GROUP_PLAYLIST;
synchronized(_downloadedSet) {
_downloadedSet.add(existing.id);
}
_downloaded.save(existing); _downloaded.save(existing);
} }
} }
@ -466,6 +470,65 @@ class StateDownloads {
return _downloadsDirectory; return _downloadsDirectory;
} }
fun exportPlaylist(context: Context, playlistId: String) {
if(context is IWithResultLauncher)
StateApp.instance.requestDirectoryAccess(context, "Export Playlist", "To export playlist to directory", null) {
if (it == null)
return@requestDirectoryAccess;
val root = DocumentFile.fromTreeUri(context, it!!);
val playlist = StatePlaylists.instance.getPlaylist(playlistId);
var localVideos = StateDownloads.instance.getDownloadedVideosPlaylist(playlistId);
if(playlist != null) {
val missing = playlist.videos
.filter { vid -> !localVideos.any { it.id.value == null || it.id.value == vid.id.value } }
.map { getCachedVideo(it.id) }
.filterNotNull();
if(missing.size > 0)
localVideos = localVideos + missing;
};
var lastNotifyTime = -1L;
UIDialogs.showDialogProgress(context) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
it.setText("Exporting videos..");
var i = 0;
var success = 0;
for (video in localVideos) {
withContext(Dispatchers.Main) {
it.setText("Exporting videos...(${i}/${localVideos.size})");
//it.setProgress(i.toDouble() / localVideos.size);
}
try {
val export = VideoExport(video, video.videoSource.firstOrNull(), video.audioSource.firstOrNull(), video.subtitlesSources.firstOrNull());
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
val file = export.export(context, { progress ->
val now = System.currentTimeMillis();
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
it.setProgress(progress);
lastNotifyTime = now;
}
}, root);
success++;
} catch(ex: Throwable) {
Logger.e(TAG, "Failed export [${video.name}]: ${ex.message}", ex);
}
i++;
}
withContext(Dispatchers.Main) {
it.setProgress(1f);
it.dismiss();
UIDialogs.appToast("Finished exporting playlist (${success} videos${if(i < success) ", ${i} errors" else ""})");
}
};
}
}
}
fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) { fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
var lastNotifyTime = -1L; var lastNotifyTime = -1L;
@ -477,13 +540,13 @@ class StateDownloads {
try { try {
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started"); Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
val file = export.export(context) { progress -> val file = export.export(context, { progress ->
val now = System.currentTimeMillis(); val now = System.currentTimeMillis();
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) { if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
it.setProgress(progress); it.setProgress(progress);
lastNotifyTime = now; lastNotifyTime = now;
} }
} }, null);
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
it.setProgress(100.0f) it.setProgress(100.0f)

View file

@ -14,7 +14,7 @@ class StateMeta {
return when(lastCommentSection.value){ return when(lastCommentSection.value){
"Polycentric" -> 0; "Polycentric" -> 0;
"Platform" -> 1; "Platform" -> 1;
else -> 1 else -> 0
} }
} }
fun setLastCommentSection(value: Int) { fun setLastCommentSection(value: Int) {

View file

@ -13,6 +13,7 @@ import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@ -130,6 +131,12 @@ class StatePlayer {
closeMediaSession(); closeMediaSession();
} }
fun saveQueueAsPlaylist(name: String){
val videos = _queue.toList();
val playlist = Playlist(name, videos.map { SerializedPlatformVideo.fromVideo(it) });
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
}
//Notifications //Notifications
fun hasMediaSession() : Boolean { fun hasMediaSession() : Boolean {
return MediaPlaybackService.getService() != null; return MediaPlaybackService.getService() != null;

View file

@ -177,11 +177,14 @@ class StatePlaylists {
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER); StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
} }
} }
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1) { fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1): Boolean {
var wasNew = false;
synchronized(_watchlistStore) { synchronized(_watchlistStore) {
if(!_watchlistStore.hasItem { it.url == video.url })
wasNew = true;
_watchlistStore.saveAsync(video); _watchlistStore.saveAsync(video);
if(orderPosition == -1) if(orderPosition == -1)
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values) .toTypedArray()); _watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray());
else { else {
val existing = _watchlistOrderStore.getAllValues().toMutableList(); val existing = _watchlistOrderStore.getAllValues().toMutableList();
existing.add(orderPosition, video.url); existing.add(orderPosition, video.url);
@ -198,6 +201,7 @@ class StatePlaylists {
} }
StateDownloads.instance.checkForOutdatedPlaylists(); StateDownloads.instance.checkForOutdatedPlaylists();
return wasNew;
} }
fun getLastPlayedPlaylist() : Playlist? { 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) { private fun broadcastWatchLater(orderOnly: Boolean = false) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try { try {
StateSync.instance.broadcastJsonData( StateSync.instance.broadcastJsonData(
GJSyncOpcodes.syncWatchLater, SyncWatchLaterPackage( GJSyncOpcodes.syncWatchLater, getWatchLaterSyncPacket(orderOnly)
if (orderOnly) listOf() else getWatchLater(),
if (orderOnly) mapOf() else _watchLaterAdds.all(),
if (orderOnly) mapOf() else _watchLaterRemovals.all(),
getWatchLaterLastReorderTime().toEpochSecond(),
_watchlistOrderStore.values.toList()
)
); );
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.w(TAG, "Failed to broadcast watch later", e) Logger.w(TAG, "Failed to broadcast watch later", e)

View file

@ -2,6 +2,7 @@ package com.futo.platformplayer.states
import android.content.Context import android.content.Context
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.LoginActivity import com.futo.platformplayer.activities.LoginActivity
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
@ -101,6 +102,8 @@ class StatePlugins {
if (availableClient !is JSClient) { if (availableClient !is JSClient) {
continue continue
} }
if(!Settings.instance.plugins.checkDisabledPluginsForUpdates && !StatePlatform.instance.isClientEnabled(availableClient.id))
continue;
val newConfig = checkForUpdates(availableClient.config); val newConfig = checkForUpdates(availableClient.config);
if (newConfig != null) { if (newConfig != null) {

View file

@ -21,9 +21,7 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
import com.futo.platformplayer.awaitFirstDeferred import com.futo.platformplayer.awaitFirstDeferred
import com.futo.platformplayer.dp import com.futo.platformplayer.dp
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.resolveChannelUrl import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
@ -33,6 +31,7 @@ import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ClaimType import com.futo.polycentric.core.ClaimType
import com.futo.polycentric.core.ContentType import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Opinion import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.ProcessHandle import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.PublicKey import com.futo.polycentric.core.PublicKey
import com.futo.polycentric.core.SignedEvent import com.futo.polycentric.core.SignedEvent
@ -234,34 +233,7 @@ class StatePolycentric {
if (!enabled) { if (!enabled) {
return Pair(false, listOf(url)); return Pair(false, listOf(url));
} }
var polycentricProfile: PolycentricProfile? = null; return Pair(didUpdate, listOf(url));
try {
val polycentricCached = PolycentricCache.instance.getCachedProfile(url, cacheOnly)
polycentricProfile = polycentricCached?.profile;
if (polycentricCached == null && channelId != null) {
Logger.i("StateSubscriptions", "Get polycentric profile not cached");
if(!cacheOnly) {
polycentricProfile = runBlocking { PolycentricCache.instance.getProfileAsync(channelId, if(doCacheNull) url else null) }?.profile;
didUpdate = true;
}
} else {
Logger.i("StateSubscriptions", "Get polycentric profile cached");
}
}
catch(ex: Throwable) {
Logger.w(StateSubscriptions.TAG, "Polycentric getCachedProfile failed for subscriptions", ex);
//TODO: Some way to communicate polycentric failing without blocking here
}
if(polycentricProfile != null) {
val urls = polycentricProfile.ownedClaims.groupBy { it.claim.claimType }
.mapNotNull { it.value.firstOrNull()?.claim?.resolveChannelUrl() }.toMutableList();
if(urls.any { it.equals(url, true) })
return Pair(didUpdate, urls);
else
return Pair(didUpdate, listOf(url) + urls);
}
else
return Pair(didUpdate, listOf(url));
} }
fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager<IPlatformContent>? { fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager<IPlatformContent>? {
@ -325,7 +297,7 @@ class StatePolycentric {
id = PlatformID("polycentric", author, null, ClaimType.POLYCENTRIC.value.toInt()), id = PlatformID("polycentric", author, null, ClaimType.POLYCENTRIC.value.toInt()),
name = systemState.username, name = systemState.username,
url = author, url = author,
thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(PolycentricCache.SERVER)) }, thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
subscribers = null subscribers = null
), ),
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content, msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
@ -349,7 +321,7 @@ class StatePolycentric {
suspend fun getLikesDislikesReplies(reference: Protocol.Reference): LikesDislikesReplies { suspend fun getLikesDislikesReplies(reference: Protocol.Reference): LikesDislikesReplies {
ensureEnabled() ensureEnabled()
val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null, val response = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, null,
null, null,
listOf( listOf(
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder() Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
@ -382,7 +354,7 @@ class StatePolycentric {
} }
val pointer = Protocol.Pointer.parseFrom(reference.reference) val pointer = Protocol.Pointer.parseFrom(reference.reference)
val events = ApiMethods.getEvents(PolycentricCache.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder() val events = ApiMethods.getEvents(ApiMethods.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder()
.addRangesForProcesses(Protocol.RangesForProcess.newBuilder() .addRangesForProcesses(Protocol.RangesForProcess.newBuilder()
.setProcess(pointer.process) .setProcess(pointer.process)
.addRanges(Protocol.Range.newBuilder() .addRanges(Protocol.Range.newBuilder()
@ -400,11 +372,11 @@ class StatePolycentric {
} }
val post = Protocol.Post.parseFrom(ev.content); val post = Protocol.Post.parseFrom(ev.content);
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER)); val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(ApiMethods.SERVER));
val dp_25 = 25.dp(StateApp.instance.context.resources) val dp_25 = 25.dp(StateApp.instance.context.resources)
val profileEvents = ApiMethods.getQueryLatest( val profileEvents = ApiMethods.getQueryLatest(
PolycentricCache.SERVER, ApiMethods.SERVER,
ev.system.toProto(), ev.system.toProto(),
listOf( listOf(
ContentType.AVATAR.value, ContentType.AVATAR.value,
@ -433,7 +405,7 @@ class StatePolycentric {
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()), id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown", name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
url = systemLinkUrl, url = systemLinkUrl,
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) }, thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
subscribers = null subscribers = null
), ),
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content, msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
@ -445,12 +417,12 @@ class StatePolycentric {
) )
} }
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> { suspend fun getCommentPager(contextUrl: String, reference: Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
if (!enabled) { if (!enabled) {
return EmptyPager() return EmptyPager()
} }
val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null, val response = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, null,
Protocol.QueryReferencesRequestEvents.newBuilder() Protocol.QueryReferencesRequestEvents.newBuilder()
.setFromType(ContentType.POST.value) .setFromType(ContentType.POST.value)
.addAllCountLwwElementReferences(arrayListOf( .addAllCountLwwElementReferences(arrayListOf(
@ -486,7 +458,7 @@ class StatePolycentric {
} }
override suspend fun nextPageAsync() { override suspend fun nextPageAsync() {
val nextPageResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, _cursor, val nextPageResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, _cursor,
Protocol.QueryReferencesRequestEvents.newBuilder() Protocol.QueryReferencesRequestEvents.newBuilder()
.setFromType(ContentType.POST.value) .setFromType(ContentType.POST.value)
.addAllCountLwwElementReferences(arrayListOf( .addAllCountLwwElementReferences(arrayListOf(
@ -534,7 +506,7 @@ class StatePolycentric {
return@mapNotNull LazyComment(scope.async(_commentPoolDispatcher){ return@mapNotNull LazyComment(scope.async(_commentPoolDispatcher){
Logger.i(TAG, "Fetching comment data for [" + ev.system.key.toBase64() + "]"); Logger.i(TAG, "Fetching comment data for [" + ev.system.key.toBase64() + "]");
val profileEvents = ApiMethods.getQueryLatest( val profileEvents = ApiMethods.getQueryLatest(
PolycentricCache.SERVER, ApiMethods.SERVER,
ev.system.toProto(), ev.system.toProto(),
listOf( listOf(
ContentType.AVATAR.value, ContentType.AVATAR.value,
@ -558,7 +530,7 @@ class StatePolycentric {
val unixMilliseconds = ev.unixMilliseconds val unixMilliseconds = ev.unixMilliseconds
//TODO: Don't use single hardcoded sderver here //TODO: Don't use single hardcoded sderver here
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER)); val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(ApiMethods.SERVER));
val dp_25 = 25.dp(StateApp.instance.context.resources) val dp_25 = 25.dp(StateApp.instance.context.resources)
return@async PolycentricPlatformComment( return@async PolycentricPlatformComment(
contextUrl = contextUrl, contextUrl = contextUrl,
@ -566,7 +538,7 @@ class StatePolycentric {
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()), id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown", name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
url = systemLinkUrl, url = systemLinkUrl,
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) }, thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
subscribers = null subscribers = null
), ),
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content, msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,

View file

@ -1,54 +1,17 @@
package com.futo.platformplayer.states package com.futo.platformplayer.states
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.*
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.findNonRuntimeException
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.getNowDiffDays
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.states.StateHistory.Companion
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringDateMapStorage import com.futo.platformplayer.stores.StringDateMapStorage
import com.futo.platformplayer.stores.SubscriptionStorage
import com.futo.platformplayer.stores.v2.ReconstructStore
import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
import com.futo.platformplayer.sync.internal.GJSyncOpcodes import com.futo.platformplayer.sync.internal.GJSyncOpcodes
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.* import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.concurrent.ExecutionException
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinTask
import kotlin.collections.ArrayList
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlin.streams.asSequence
import kotlin.streams.toList
import kotlin.system.measureTimeMillis
/*** /***
* Used to maintain subscription groups * Used to maintain subscription groups

View file

@ -15,7 +15,6 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.resolveChannelUrl import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringDateMapStorage import com.futo.platformplayer.stores.StringDateMapStorage
@ -335,12 +334,6 @@ class StateSubscriptions {
return true; return true;
} }
//TODO: This causes issues, because what if the profile is not cached yet when the susbcribe button is loaded for example?
val cachedProfile = PolycentricCache.instance.getCachedProfile(urls.first(), true)?.profile;
if (cachedProfile != null) {
return cachedProfile.ownedClaims.any { c -> _subscriptions.hasItem { s -> c.claim.resolveChannelUrl() == s.channel.url } };
}
return false; return false;
} }
} }

View file

@ -44,6 +44,7 @@ import kotlin.system.measureTimeMillis
class StateSync { class StateSync {
private val _authorizedDevices = FragmentedStorage.get<StringArrayStorage>("authorized_devices") private val _authorizedDevices = FragmentedStorage.get<StringArrayStorage>("authorized_devices")
private val _nameStorage = FragmentedStorage.get<StringStringMapStorage>("sync_remembered_name_storage")
private val _syncKeyPair = FragmentedStorage.get<StringStorage>("sync_key_pair") private val _syncKeyPair = FragmentedStorage.get<StringStorage>("sync_key_pair")
private val _lastAddressStorage = FragmentedStorage.get<StringStringMapStorage>("sync_last_address_storage") private val _lastAddressStorage = FragmentedStorage.get<StringStringMapStorage>("sync_last_address_storage")
private val _syncSessionData = FragmentedStorage.get<StringTMapStorage<SyncSessionData>>("syncSessionData") private val _syncSessionData = FragmentedStorage.get<StringTMapStorage<SyncSessionData>>("syncSessionData")
@ -305,12 +306,22 @@ class StateSync {
synchronized(_sessions) { synchronized(_sessions) {
session = _sessions[s.remotePublicKey] session = _sessions[s.remotePublicKey]
if (session == null) { if (session == null) {
val remoteDeviceName = synchronized(_nameStorage) {
_nameStorage.get(remotePublicKey)
}
session = SyncSession(remotePublicKey, onAuthorized = { it, isNewlyAuthorized, isNewSession -> session = SyncSession(remotePublicKey, onAuthorized = { it, isNewlyAuthorized, isNewSession ->
if (!isNewSession) { if (!isNewSession) {
return@SyncSession return@SyncSession
} }
Logger.i(TAG, "${s.remotePublicKey} authorized") it.remoteDeviceName?.let { remoteDeviceName ->
synchronized(_nameStorage) {
_nameStorage.setAndSave(remotePublicKey, remoteDeviceName)
}
}
Logger.i(TAG, "${s.remotePublicKey} authorized (name: ${it.displayName})")
synchronized(_lastAddressStorage) { synchronized(_lastAddressStorage) {
_lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress) _lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress)
} }
@ -341,7 +352,7 @@ class StateSync {
deviceRemoved.emit(it.remotePublicKey) deviceRemoved.emit(it.remotePublicKey)
}) }, remoteDeviceName)
_sessions[remotePublicKey] = session!! _sessions[remotePublicKey] = session!!
} }
session!!.addSocketSession(s) session!!.addSocketSession(s)
@ -469,6 +480,12 @@ class StateSync {
} }
} }
fun getCachedName(publicKey: String): String? {
return synchronized(_nameStorage) {
_nameStorage.get(publicKey)
}
}
suspend fun delete(publicKey: String) { suspend fun delete(publicKey: String) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {

View file

@ -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;
}
}

View file

@ -33,6 +33,9 @@ class ManagedStore<T>{
val className: String? get() = _class.classifier?.assume<KClass<*>>()?.simpleName; val className: String? get() = _class.classifier?.assume<KClass<*>>()?.simpleName;
private var _onModificationCreate: ((T) -> Unit)? = null;
private var _onModificationDelete: ((T) -> Unit)? = null;
val name: String; val name: String;
constructor(name: String, dir: File, clazz: KType, serializer: StoreSerializer<T>, niceName: String? = null) { constructor(name: String, dir: File, clazz: KType, serializer: StoreSerializer<T>, niceName: String? = null) {
@ -62,6 +65,12 @@ class ManagedStore<T>{
return this; return this;
} }
fun withOnModified(created: (T)->Unit, deleted: (T)->Unit): ManagedStore<T> {
_onModificationCreate = created;
_onModificationDelete = deleted;
return this;
}
fun load(): ManagedStore<T> { fun load(): ManagedStore<T> {
synchronized(_files) { synchronized(_files) {
_files.clear(); _files.clear();
@ -265,6 +274,7 @@ class ManagedStore<T>{
file = saveNew(obj); file = saveNew(obj);
if(_reconstructStore != null && (_reconstructStore!!.backupOnCreate || withReconstruction)) if(_reconstructStore != null && (_reconstructStore!!.backupOnCreate || withReconstruction))
saveReconstruction(file, obj); saveReconstruction(file, obj);
_onModificationCreate?.invoke(obj)
} }
} }
} }
@ -300,6 +310,7 @@ class ManagedStore<T>{
_files.remove(item); _files.remove(item);
Logger.v(TAG, "Deleting file ${logName(file.id)}"); Logger.v(TAG, "Deleting file ${logName(file.id)}");
file.delete(); file.delete();
_onModificationDelete?.invoke(item)
} }
} }
} }

View file

@ -6,12 +6,10 @@ import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.HistoryVideo import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.smartMerge import com.futo.platformplayer.smartMerge
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.states.StateHistory import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.states.StateSubscriptionGroups import com.futo.platformplayer.states.StateSubscriptionGroups
import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.states.StateSubscriptions
@ -30,6 +28,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.time.Instant import java.time.Instant
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
@ -53,6 +52,9 @@ class SyncSession : IAuthorizable {
private val _id = UUID.randomUUID() private val _id = UUID.randomUUID()
private var _remoteId: UUID? = null private var _remoteId: UUID? = null
private var _lastAuthorizedRemoteId: UUID? = null private var _lastAuthorizedRemoteId: UUID? = null
var remoteDeviceName: String? = null
private set
val displayName: String get() = remoteDeviceName ?: remotePublicKey
var connected: Boolean = false var connected: Boolean = false
private set(v) { 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 this.remotePublicKey = remotePublicKey
_onAuthorized = onAuthorized _onAuthorized = onAuthorized
_onUnauthorized = onUnauthorized _onUnauthorized = onUnauthorized
@ -85,7 +87,20 @@ class SyncSession : IAuthorizable {
fun authorize(socketSession: SyncSocketSession) { fun authorize(socketSession: SyncSocketSession) {
Logger.i(TAG, "Sent AUTHORIZED with session id $_id") 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 _authorized = true
checkAuthorized() checkAuthorized()
} }
@ -138,15 +153,37 @@ class SyncSession : IAuthorizable {
when (opcode) { when (opcode) {
Opcode.NOTIFY_AUTHORIZED.value -> { Opcode.NOTIFY_AUTHORIZED.value -> {
val str = data.toUtf8String() if (socketSession.remoteVersion >= 3) {
_remoteId = if (data.remaining() >= 0) UUID.fromString(str) else UUID.fromString("00000000-0000-0000-0000-000000000000") 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 _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() checkAuthorized()
return return
} }
Opcode.NOTIFY_UNAUTHORIZED.value -> { Opcode.NOTIFY_UNAUTHORIZED.value -> {
_remoteId = null _remoteId = null
remoteDeviceName = null
_lastAuthorizedRemoteId = null _lastAuthorizedRemoteId = null
_remoteAuthorized = false _remoteAuthorized = false
_onUnauthorized(this) _onUnauthorized(this)
@ -195,6 +232,8 @@ class SyncSession : IAuthorizable {
sendData(GJSyncOpcodes.syncSubscriptionGroups, StateSubscriptionGroups.instance.getSyncSubscriptionGroupsPackageString()); sendData(GJSyncOpcodes.syncSubscriptionGroups, StateSubscriptionGroups.instance.getSyncSubscriptionGroupsPackageString());
sendData(GJSyncOpcodes.syncPlaylists, StatePlaylists.instance.getSyncPlaylistsPackageString()) sendData(GJSyncOpcodes.syncPlaylists, StatePlaylists.instance.getSyncPlaylistsPackageString())
sendData(GJSyncOpcodes.syncWatchLater, Json.encodeToString(StatePlaylists.instance.getWatchLaterSyncPacket(false)));
val recentHistory = StateHistory.instance.getRecentHistory(syncSessionData.lastHistory); val recentHistory = StateHistory.instance.getRecentHistory(syncSessionData.lastHistory);
if(recentHistory.size > 0) if(recentHistory.size > 0)
sendJsonData(GJSyncOpcodes.syncHistory, recentHistory); sendJsonData(GJSyncOpcodes.syncHistory, recentHistory);

View file

@ -46,6 +46,8 @@ class SyncSocketSession {
val localPublicKey: String get() = _localPublicKey val localPublicKey: String get() = _localPublicKey
private val _onData: (session: SyncSocketSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) -> Unit private val _onData: (session: SyncSocketSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) -> Unit
var authorizable: IAuthorizable? = null var authorizable: IAuthorizable? = null
var remoteVersion: Int = -1
private set
val remoteAddress: String val remoteAddress: String
@ -162,11 +164,12 @@ class SyncSocketSession {
} }
private fun performVersionCheck() { private fun performVersionCheck() {
val CURRENT_VERSION = 2 val CURRENT_VERSION = 3
val MINIMUM_VERSION = 2
_outputStream.writeInt(CURRENT_VERSION) _outputStream.writeInt(CURRENT_VERSION)
val version = _inputStream.readInt() remoteVersion = _inputStream.readInt()
Logger.i(TAG, "performVersionCheck (version = $version)") Logger.i(TAG, "performVersionCheck (version = $remoteVersion)")
if (version != CURRENT_VERSION) if (remoteVersion < MINIMUM_VERSION)
throw Exception("Invalid version") throw Exception("Invalid version")
} }

View file

@ -16,11 +16,11 @@ import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.adapters.viewholders.StoreItemViewHolder import com.futo.platformplayer.views.adapters.viewholders.StoreItemViewHolder
import com.futo.platformplayer.views.platform.PlatformIndicator import com.futo.platformplayer.views.platform.PlatformIndicator
import com.futo.polycentric.core.PolycentricProfile
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -125,8 +125,7 @@ class MonetizationView : LinearLayout {
} }
} }
fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?) { fun setPolycentricProfile(profile: PolycentricProfile?) {
val profile = cachedPolycentricProfile?.profile;
if (profile != null) { if (profile != null) {
if (profile.systemState.store.isNotEmpty()) { if (profile.systemState.store.isNotEmpty()) {
_buttonStore.visibility = View.VISIBLE; _buttonStore.visibility = View.VISIBLE;

View file

@ -14,10 +14,10 @@ import androidx.core.view.isVisible
import androidx.core.view.size import androidx.core.view.size
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.views.buttons.BigButton import com.futo.platformplayer.views.buttons.BigButton
import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.imageview.ShapeableImageView

View file

@ -0,0 +1,74 @@
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.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 {
this.setInfo(button.name, button.isActive);
this.onClick.subscribe { button.action(it); };
});
}
}
class Toggle {
val name: String;
val icon: Int;
val action: (Boolean)->Unit;
val isActive: Boolean;
constructor(name: String, icon: Int, isActive: Boolean = false, action: (Boolean)->Unit) {
this.name = name;
this.icon = icon;
this.action = action;
this.isActive = isActive;
}
constructor(name: String, isActive: Boolean = false, action: (Boolean)->Unit) {
this.name = name;
this.icon = 0;
this.action = action;
this.isActive = isActive;
}
}
}

View file

@ -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.ChannelMonetizationFragment
import com.futo.platformplayer.fragment.channel.tab.ChannelPlaylistsFragment import com.futo.platformplayer.fragment.channel.tab.ChannelPlaylistsFragment
import com.futo.platformplayer.fragment.channel.tab.IChannelTabFragment 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 import com.google.android.material.tabs.TabLayout

View file

@ -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.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.fixHtmlLinks 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.setPlatformPlayerLinkMovementMethod
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric 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.others.CreatorThumbnail
import com.futo.platformplayer.views.pills.PillButton import com.futo.platformplayer.views.pills.PillButton
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.Opinion import com.futo.polycentric.core.Opinion
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -81,24 +80,18 @@ class CommentViewHolder : ViewHolder {
throw Exception("Not implemented for non polycentric comments") throw Exception("Not implemented for non polycentric comments")
} }
if (args.hasLiked) { val newOpinion: Opinion = if (args.hasLiked) {
args.processHandle.opinion(c.reference, Opinion.like); Opinion.like
} else if (args.hasDisliked) { } else if (args.hasDisliked) {
args.processHandle.opinion(c.reference, Opinion.dislike); Opinion.dislike
} else { } 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; _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) { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try { ApiMethods.setOpinion(args.processHandle, c.reference, newOpinion)
Logger.i(TAG, "Started backfill");
args.processHandle.fullyBackfillServersAnnounceExceptions();
Logger.i(TAG, "Finished backfill");
} catch (e: Throwable) {
Logger.e(TAG, "Failed to backfill servers.", e)
}
} }
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked) StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)

View file

@ -16,7 +16,6 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.fixHtmlLinks import com.futo.platformplayer.fixHtmlLinks
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
import com.futo.platformplayer.states.StateApp 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.PillButton
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
import com.futo.polycentric.core.Opinion import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.IdentityHashMap import java.util.IdentityHashMap

View file

@ -16,7 +16,6 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.others.CreatorThumbnail
@ -29,21 +28,12 @@ open class PlaylistView : LinearLayout {
protected val _imageThumbnail: ImageView protected val _imageThumbnail: ImageView
protected val _imageChannel: ImageView? protected val _imageChannel: ImageView?
protected val _creatorThumbnail: CreatorThumbnail? protected val _creatorThumbnail: CreatorThumbnail?
protected val _imageNeopassChannel: ImageView?;
protected val _platformIndicator: PlatformIndicator; protected val _platformIndicator: PlatformIndicator;
protected val _textPlaylistName: TextView protected val _textPlaylistName: TextView
protected val _textVideoCount: TextView protected val _textVideoCount: TextView
protected val _textVideoCountLabel: TextView; protected val _textVideoCountLabel: TextView;
protected val _textPlaylistItems: TextView protected val _textPlaylistItems: TextView
protected val _textChannelName: 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 onPlaylistClicked = Event1<IPlatformPlaylist>();
val onChannelClicked = Event1<PlatformAuthorLink>(); val onChannelClicked = Event1<PlatformAuthorLink>();
@ -66,7 +56,6 @@ open class PlaylistView : LinearLayout {
_textVideoCountLabel = findViewById(R.id.text_video_count_label); _textVideoCountLabel = findViewById(R.id.text_video_count_label);
_textChannelName = findViewById(R.id.text_channel_name); _textChannelName = findViewById(R.id.text_channel_name);
_textPlaylistItems = findViewById(R.id.text_playlist_items); _textPlaylistItems = findViewById(R.id.text_playlist_items);
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
setOnClickListener { onOpenClicked() }; setOnClickListener { onOpenClicked() };
_imageChannel?.setOnClickListener { currentPlaylist?.let { onChannelClicked.emit(it.author) } }; _imageChannel?.setOnClickListener { currentPlaylist?.let { onChannelClicked.emit(it.author) } };
@ -88,20 +77,6 @@ open class PlaylistView : LinearLayout {
open fun bind(content: IPlatformContent) { 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; isClickable = true;
_imageChannel?.let { _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 { companion object {
private val TAG = "VideoPreviewViewHolder" private val TAG = "VideoPreviewViewHolder"
} }

View file

@ -16,6 +16,7 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
private lateinit var _sortedDataset: List<Subscription>; private lateinit var _sortedDataset: List<Subscription>;
private val _inflater: LayoutInflater; private val _inflater: LayoutInflater;
private val _confirmationMessage: String; private val _confirmationMessage: String;
private val _onDatasetChanged: ((List<Subscription>)->Unit)?;
var onClick = Event1<Subscription>(); var onClick = Event1<Subscription>();
var onSettings = Event1<Subscription>(); var onSettings = Event1<Subscription>();
@ -30,9 +31,10 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
updateDataset(); updateDataset();
} }
constructor(inflater: LayoutInflater, confirmationMessage: String) : super() { constructor(inflater: LayoutInflater, confirmationMessage: String, onDatasetChanged: ((List<Subscription>)->Unit)? = null) : super() {
_inflater = inflater; _inflater = inflater;
_confirmationMessage = confirmationMessage; _confirmationMessage = confirmationMessage;
_onDatasetChanged = onDatasetChanged;
StateSubscriptions.instance.onSubscriptionsChanged.subscribe { _, _ -> if(Looper.myLooper() != Looper.getMainLooper()) StateSubscriptions.instance.onSubscriptionsChanged.subscribe { _, _ -> if(Looper.myLooper() != Looper.getMainLooper())
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { updateDataset() } 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)) } .filter { (queryLower.isNullOrBlank() || it.channel.name.lowercase().contains(queryLower)) }
.toList(); .toList();
_onDatasetChanged?.invoke(_sortedDataset);
notifyDataSetChanged(); notifyDataSetChanged();
} }

View file

@ -15,7 +15,6 @@ import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.toHumanTimeIndicator import com.futo.platformplayer.toHumanTimeIndicator
@ -32,14 +31,6 @@ class SubscriptionViewHolder : ViewHolder {
private val _platformIndicator : PlatformIndicator; private val _platformIndicator : PlatformIndicator;
private val _textMeta: TextView; 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 var subscription: Subscription? = null
private set; private set;
@ -74,45 +65,12 @@ class SubscriptionViewHolder : ViewHolder {
} }
fun bind(sub: Subscription) { fun bind(sub: Subscription) {
_taskLoadProfile.cancel();
this.subscription = sub; this.subscription = sub;
_creatorThumbnail.setThumbnail(sub.channel.thumbnail, false); _creatorThumbnail.setThumbnail(sub.channel.thumbnail, false);
_textName.text = sub.channel.name; _textName.text = sub.channel.name;
bindViewMetrics(sub); bindViewMetrics(sub);
_platformIndicator.setPlatformFromClientID(sub.channel.id.pluginId); _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?) { fun bindViewMetrics(sub: Subscription?) {

View file

@ -30,7 +30,6 @@ import com.futo.platformplayer.dp
import com.futo.platformplayer.fixHtmlWhitespace import com.futo.platformplayer.fixHtmlWhitespace
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.toHumanNowDiffString import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.views.FeedStyle import com.futo.platformplayer.views.FeedStyle
@ -44,7 +43,6 @@ class PreviewPostView : LinearLayout {
private val _imageAuthorThumbnail: ImageView; private val _imageAuthorThumbnail: ImageView;
private val _textAuthorName: TextView; private val _textAuthorName: TextView;
private val _imageNeopassChannel: ImageView;
private val _textMetadata: TextView; private val _textMetadata: TextView;
private val _textTitle: TextView; private val _textTitle: TextView;
private val _textDescription: TextView; private val _textDescription: TextView;
@ -64,15 +62,6 @@ class PreviewPostView : LinearLayout {
private val _layoutComments: LinearLayout?; private val _layoutComments: LinearLayout?;
private val _textComments: TextView?; 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 content: IPlatformContent? get() = _content;
val onContentClicked = Event1<IPlatformContent>(); val onContentClicked = Event1<IPlatformContent>();
@ -83,7 +72,6 @@ class PreviewPostView : LinearLayout {
_imageAuthorThumbnail = findViewById(R.id.image_author_thumbnail); _imageAuthorThumbnail = findViewById(R.id.image_author_thumbnail);
_textAuthorName = findViewById(R.id.text_author_name); _textAuthorName = findViewById(R.id.text_author_name);
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
_textMetadata = findViewById(R.id.text_metadata); _textMetadata = findViewById(R.id.text_metadata);
_textTitle = findViewById(R.id.text_title); _textTitle = findViewById(R.id.text_title);
_textDescription = findViewById(R.id.text_description); _textDescription = findViewById(R.id.text_description);
@ -130,21 +118,8 @@ class PreviewPostView : LinearLayout {
} }
fun bind(content: IPlatformContent) { fun bind(content: IPlatformContent) {
_taskLoadValidClaims.cancel();
_content = content; _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; _textAuthorName.text = content.author.name;
_textMetadata.text = content.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: ""; _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 { companion object {
val TAG = "PreviewPostView"; val TAG = "PreviewPostView";
} }

View file

@ -24,7 +24,6 @@ import com.futo.platformplayer.getNowDiffSeconds
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.images.GlideHelper.Companion.loadThumbnails import com.futo.platformplayer.images.GlideHelper.Companion.loadThumbnails
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StateDownloads
@ -47,7 +46,6 @@ open class PreviewVideoView : LinearLayout {
protected val _imageVideo: ImageView protected val _imageVideo: ImageView
protected val _imageChannel: ImageView? protected val _imageChannel: ImageView?
protected val _creatorThumbnail: CreatorThumbnail? protected val _creatorThumbnail: CreatorThumbnail?
protected val _imageNeopassChannel: ImageView?;
protected val _platformIndicator: PlatformIndicator; protected val _platformIndicator: PlatformIndicator;
protected val _textVideoName: TextView protected val _textVideoName: TextView
protected val _textChannelName: TextView protected val _textChannelName: TextView
@ -57,7 +55,6 @@ open class PreviewVideoView : LinearLayout {
protected var _playerVideoThumbnail: FutoThumbnailPlayer? = null; protected var _playerVideoThumbnail: FutoThumbnailPlayer? = null;
protected val _containerLive: LinearLayout; protected val _containerLive: LinearLayout;
protected val _playerContainer: FrameLayout; protected val _playerContainer: FrameLayout;
protected var _neopassAnimator: ObjectAnimator? = null;
protected val _layoutDownloaded: FrameLayout; protected val _layoutDownloaded: FrameLayout;
protected val _button_add_to_queue : View; protected val _button_add_to_queue : View;
@ -65,15 +62,6 @@ open class PreviewVideoView : LinearLayout {
protected val _button_add_to : View; protected val _button_add_to : View;
protected val _exoPlayer: PlayerManager?; 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?; private val _timeBar: ProgressBar?;
val onVideoClicked = Event2<IPlatformVideo, Long>(); 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_queue = findViewById(R.id.button_add_to_queue);
_button_add_to_watch_later = findViewById(R.id.button_add_to_watch_later); _button_add_to_watch_later = findViewById(R.id.button_add_to_watch_later);
_button_add_to = findViewById(R.id.button_add_to); _button_add_to = findViewById(R.id.button_add_to);
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
_layoutDownloaded = findViewById(R.id.layout_downloaded); _layoutDownloaded = findViewById(R.id.layout_downloaded);
_timeBar = findViewById(R.id.time_bar) _timeBar = findViewById(R.id.time_bar)
@ -132,7 +119,7 @@ open class PreviewVideoView : LinearLayout {
fun hideAddTo() { fun hideAddTo() {
_button_add_to.visibility = View.GONE _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) { protected open fun inflate(feedStyle: FeedStyle) {
@ -160,15 +147,12 @@ open class PreviewVideoView : LinearLayout {
open fun bind(content: IPlatformContent) { open fun bind(content: IPlatformContent) {
_taskLoadProfile.cancel();
isClickable = true; isClickable = true;
val isPlanned = (content.datetime?.getNowDiffSeconds() ?: 0) < 0; val isPlanned = (content.datetime?.getNowDiffSeconds() ?: 0) < 0;
stopPreview(); stopPreview();
_imageNeopassChannel?.visibility = View.GONE;
_creatorThumbnail?.setThumbnail(content.author.thumbnail, false); _creatorThumbnail?.setThumbnail(content.author.thumbnail, false);
val thumbnail = content.author.thumbnail val thumbnail = content.author.thumbnail
@ -186,16 +170,6 @@ open class PreviewVideoView : LinearLayout {
_textChannelName.text = content.author.name _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; _imageChannel?.clipToOutline = true;
_textVideoName.text = content.name; _textVideoName.text = content.name;
@ -335,52 +309,6 @@ open class PreviewVideoView : LinearLayout {
_playerVideoThumbnail?.setMuteChangedListener(callback); _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 { companion object {
private val TAG = "VideoPreviewViewHolder" private val TAG = "VideoPreviewViewHolder"
} }

View file

@ -11,7 +11,6 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.adapters.AnyAdapter import com.futo.platformplayer.views.adapters.AnyAdapter
@ -27,14 +26,6 @@ class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyVi
val onClick = Event1<IPlatformChannel>(); 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 { init {
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail); _creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
_name = _view.findViewById(R.id.text_channel_name); _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) { override fun bind(value: IPlatformChannel) {
_taskLoadProfile.cancel();
_channel = value; _channel = value;
_creatorThumbnail.setThumbnail(value.thumbnail, false); _creatorThumbnail.setThumbnail(value.thumbnail, false);
_name.text = value.name; _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 { companion object {
@ -94,14 +55,6 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
val onClick = Event1<Selectable>(); 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 { init {
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail); _creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
_name = _view.findViewById(R.id.text_channel_name); _name = _view.findViewById(R.id.text_channel_name);
@ -112,8 +65,6 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
} }
override fun bind(value: Selectable) { override fun bind(value: Selectable) {
_taskLoadProfile.cancel();
_channel = value; _channel = value;
if(value.active) if(value.active)
@ -123,34 +74,6 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
_creatorThumbnail.setThumbnail(value.channel.thumbnail, false); _creatorThumbnail.setThumbnail(value.channel.thumbnail, false);
_name.text = value.channel.name; _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 { companion object {

View file

@ -12,7 +12,6 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.toHumanNumber import com.futo.platformplayer.toHumanNumber
@ -34,14 +33,6 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
val onClick = Event1<PlatformAuthorLink>(); 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 { init {
_textName = _view.findViewById(R.id.text_channel_name); _textName = _view.findViewById(R.id.text_channel_name);
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail); _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) { override fun bind(value: PlatformAuthorLink) {
_taskLoadProfile.cancel();
_creatorThumbnail.setThumbnail(value.thumbnail, false); _creatorThumbnail.setThumbnail(value.thumbnail, false);
_textName.text = value.name; _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) if(value.subscribers == null || (value.subscribers ?: 0) <= 0L)
_textMetadata.visibility = View.GONE; _textMetadata.visibility = View.GONE;
else { else {
@ -87,25 +66,6 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
_authorLink = value; _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 { companion object {
private const val TAG = "CreatorViewHolder"; private const val TAG = "CreatorViewHolder";
} }

View file

@ -12,7 +12,6 @@ import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.adapters.AnyAdapter 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 _subscription: Subscription? = null;
private var _channel: SerializedChannel? = 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>(); val onClick = Event1<Subscription>();
init { init {
@ -47,44 +38,14 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
} }
override fun bind(value: Subscription) { override fun bind(value: Subscription) {
_taskLoadProfile.cancel();
_channel = value.channel; _channel = value.channel;
_creatorThumbnail.setThumbnail(value.channel.thumbnail, false); _creatorThumbnail.setThumbnail(value.channel.thumbnail, false);
_name.text = value.channel.name; _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; _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 { companion object {
private const val TAG = "SubscriptionBarViewHolder"; private const val TAG = "SubscriptionBarViewHolder";
} }

View file

@ -19,7 +19,6 @@ import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.adapters.AnyAdapter import com.futo.platformplayer.views.adapters.AnyAdapter

View file

@ -11,8 +11,8 @@ import androidx.constraintlayout.widget.ConstraintLayout
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.getDataLinkFromUrl
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.views.IdenticonView import com.futo.platformplayer.views.IdenticonView
import userpackage.Protocol import userpackage.Protocol
@ -68,7 +68,7 @@ class CreatorThumbnail : ConstraintLayout {
if (url.startsWith("polycentric://")) { if (url.startsWith("polycentric://")) {
try { try {
val dataLink = PolycentricCache.getDataLinkFromUrl(url) val dataLink = url.getDataLinkFromUrl()
setHarborAvailable(true, animate, dataLink?.system); setHarborAvailable(true, animate, dataLink?.system);
} catch (e: Throwable) { } catch (e: Throwable) {
setHarborAvailable(false, animate, null); setHarborAvailable(false, animate, null);

View file

@ -2,16 +2,26 @@ package com.futo.platformplayer.views.overlays
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.views.lists.VideoListEditorView import com.futo.platformplayer.views.lists.VideoListEditorView
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
class QueueEditorOverlay : LinearLayout { class QueueEditorOverlay : LinearLayout {
private val _topbar : OverlayTopbar; private val _topbar : OverlayTopbar;
private val _editor : VideoListEditorView; private val _editor : VideoListEditorView;
private val _btnSettings: ImageView;
private val _overlayContainer: FrameLayout;
val onClose = Event0(); val onClose = Event0();
@ -19,6 +29,9 @@ class QueueEditorOverlay : LinearLayout {
inflate(context, R.layout.overlay_queue, this) inflate(context, R.layout.overlay_queue, this)
_topbar = findViewById(R.id.topbar); _topbar = findViewById(R.id.topbar);
_editor = findViewById(R.id.editor); _editor = findViewById(R.id.editor);
_btnSettings = findViewById(R.id.button_settings);
_overlayContainer = findViewById(R.id.overlay_container_queue);
_topbar.onClose.subscribe(this, onClose::emit); _topbar.onClose.subscribe(this, onClose::emit);
_editor.onVideoOrderChanged.subscribe { StatePlayer.instance.setQueueWithExisting(it) } _editor.onVideoOrderChanged.subscribe { StatePlayer.instance.setQueueWithExisting(it) }
@ -28,6 +41,10 @@ class QueueEditorOverlay : LinearLayout {
} }
_editor.onVideoClicked.subscribe { v -> StatePlayer.instance.setQueuePosition(v) } _editor.onVideoClicked.subscribe { v -> StatePlayer.instance.setQueuePosition(v) }
_btnSettings.setOnClickListener {
handleSettings();
}
_topbar.setInfo(context.getString(R.string.queue), ""); _topbar.setInfo(context.getString(R.string.queue), "");
} }
@ -40,4 +57,8 @@ class QueueEditorOverlay : LinearLayout {
fun cleanup() { fun cleanup() {
_topbar.onClose.remove(this); _topbar.onClose.remove(this);
} }
fun handleSettings() {
UISlideOverlays.showQueueOptionsOverlay(context, _overlayContainer);
}
} }

View file

@ -5,8 +5,8 @@ import android.util.AttributeSet
import android.widget.LinearLayout import android.widget.LinearLayout
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.views.SupportView import com.futo.platformplayer.views.SupportView
import com.futo.polycentric.core.PolycentricProfile
class SupportOverlay : LinearLayout { class SupportOverlay : LinearLayout {
val onClose = Event0(); val onClose = Event0();

View file

@ -6,9 +6,7 @@ import android.webkit.WebView
import android.widget.LinearLayout import android.widget.LinearLayout
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.views.SupportView
class WebviewOverlay : LinearLayout { class WebviewOverlay : LinearLayout {
val onClose = Event0(); val onClose = Event0();

View file

@ -22,12 +22,12 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.adapters.CommentViewHolder import com.futo.platformplayer.views.adapters.CommentViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.UnknownHostException import java.net.UnknownHostException

View file

@ -96,6 +96,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
val exoPlayerStateName: String; val exoPlayerStateName: String;
var playing: Boolean = false; var playing: Boolean = false;
val activelyPlaying: Boolean get() = (exoPlayer?.player?.playbackState == Player.STATE_READY) && (exoPlayer?.player?.playWhenReady ?: false)
val position: Long get() = exoPlayer?.player?.currentPosition ?: 0; val position: Long get() = exoPlayer?.player?.currentPosition ?: 0;
val duration: Long get() = exoPlayer?.player?.duration ?: 0; val duration: Long get() = exoPlayer?.player?.duration ?: 0;
@ -829,7 +830,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
Logger.i(TAG, "onPlayerError error=$error error.errorCode=${error.errorCode} connectivityLoss"); Logger.i(TAG, "onPlayerError error=$error error.errorCode=${error.errorCode} connectivityLoss");
when (error.errorCode) { when (error.errorCode) {
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> { PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
Logger.w(TAG, "ERROR_CODE_IO_BAD_HTTP_STATUS ${error.cause?.javaClass?.simpleName}"); Logger.w(TAG, "ERROR_CODE_IO_BAD_HTTP_STATUS ${error.cause?.javaClass?.simpleName}");
if(error.cause is HttpDataSource.InvalidResponseCodeException) { if(error.cause is HttpDataSource.InvalidResponseCodeException) {
val cause = error.cause as HttpDataSource.InvalidResponseCodeException val cause = error.cause as HttpDataSource.InvalidResponseCodeException

View file

@ -16,7 +16,7 @@
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="100dp" android:layout_height="110dp"
android:minHeight="0dp" android:minHeight="0dp"
app:layout_scrollFlags="scroll" app:layout_scrollFlags="scroll"
app:contentInsetStart="0dp" app:contentInsetStart="0dp"
@ -77,7 +77,16 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="20dp" android:paddingStart="20dp"
android:paddingEnd="20dp" /> android:paddingEnd="20dp" />
</LinearLayout> </LinearLayout>
<TextView
android:id="@+id/text_meta"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="9dp"
android:textAlignment="center"
android:textColor="#333333"
android:text="0 creators" />
</LinearLayout> </LinearLayout>
</androidx.appcompat.widget.Toolbar> </androidx.appcompat.widget.Toolbar>

View file

@ -54,6 +54,22 @@
<ImageButton
android:id="@+id/button_export"
android:layout_width="40dp"
android:layout_height="40dp"
android:contentDescription="@string/cd_button_download"
android:background="@drawable/background_button_round"
android:gravity="center"
android:layout_marginStart="10dp"
android:orientation="horizontal"
app:srcCompat="@drawable/ic_export"
app:layout_constraintRight_toLeftOf="@id/button_share"
app:layout_constraintTop_toTopOf="@id/button_share"
app:tint="@color/white"
android:padding="8dp"
android:layout_marginRight="10dp"
android:scaleType="fitCenter" />
<ImageButton <ImageButton
android:id="@+id/button_share" android:id="@+id/button_share"
android:layout_width="40dp" android:layout_width="40dp"

View file

@ -254,7 +254,7 @@
<TextView <TextView
android:id="@+id/text_channel_name" android:id="@+id/text_channel_name"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:gravity="center_vertical" android:gravity="center_vertical"
@ -268,23 +268,10 @@
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintLeft_toRightOf="@id/image_channel_thumbnail" app:layout_constraintLeft_toRightOf="@id/image_channel_thumbnail"
app:layout_constraintRight_toLeftOf="@id/image_neopass_channel" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_video_name" app:layout_constraintTop_toBottomOf="@id/text_video_name"
android:layout_marginStart="4dp" /> android:layout_marginStart="4dp" />
<ImageView
android:id="@+id/image_neopass_channel"
android:layout_width="10dp"
android:layout_height="10dp"
android:contentDescription="@string/neopass_channel"
app:srcCompat="@drawable/neopass"
app:layout_constraintLeft_toRightOf="@id/text_channel_name"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/text_channel_name"
app:layout_constraintBottom_toBottomOf="@id/text_channel_name"
android:layout_marginStart="4dp"
android:visibility="gone"/>
<TextView <TextView
android:id="@+id/text_video_metadata" android:id="@+id/text_video_metadata"
android:layout_width="0dp" android:layout_width="0dp"

View file

@ -104,7 +104,7 @@
<TextView <TextView
android:id="@+id/text_channel_name" android:id="@+id/text_channel_name"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:maxLines="1" android:maxLines="1"
@ -117,21 +117,8 @@
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/image_neopass_channel"
app:layout_constraintTop_toBottomOf="@id/text_playlist_name" />
<ImageView
android:id="@+id/image_neopass_channel"
android:layout_width="10dp"
android:layout_height="10dp"
android:contentDescription="@string/neopass_channel"
app:srcCompat="@drawable/neopass"
app:layout_constraintLeft_toRightOf="@id/text_channel_name"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/text_channel_name" app:layout_constraintTop_toBottomOf="@id/text_playlist_name" />
app:layout_constraintBottom_toBottomOf="@id/text_channel_name"
android:layout_marginStart="4dp"
android:visibility="gone"/>
<TextView <TextView
android:id="@+id/text_playlist_items" android:id="@+id/text_playlist_items"

View file

@ -37,7 +37,7 @@
<TextView <TextView
android:id="@+id/text_author_name" android:id="@+id/text_author_name"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
android:layout_weight="1" android:layout_weight="1"
@ -51,24 +51,10 @@
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintLeft_toRightOf="@id/image_author_thumbnail" app:layout_constraintLeft_toRightOf="@id/image_author_thumbnail"
app:layout_constraintRight_toLeftOf="@id/image_neopass_channel" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/image_author_thumbnail" app:layout_constraintTop_toTopOf="@id/image_author_thumbnail"
tools:text="Two Minute Papers" /> tools:text="Two Minute Papers" />
<ImageView
android:id="@+id/image_neopass_channel"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="8dp"
android:contentDescription="@string/neopass_channel"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/text_author_name"
app:layout_constraintLeft_toRightOf="@id/text_author_name"
app:layout_constraintRight_toLeftOf="@id/platform_indicator"
app:layout_constraintTop_toTopOf="@id/text_author_name"
app:srcCompat="@drawable/neopass" />
<TextView <TextView
android:id="@+id/text_metadata" android:id="@+id/text_metadata"
android:layout_width="0dp" android:layout_width="0dp"

View file

@ -36,9 +36,10 @@
<TextView <TextView
android:id="@+id/text_author_name" android:id="@+id/text_author_name"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
android:layout_marginEnd="8dp"
android:layout_weight="1" android:layout_weight="1"
android:ellipsize="end" android:ellipsize="end"
android:fontFamily="@font/inter_extra_light" android:fontFamily="@font/inter_extra_light"
@ -50,24 +51,10 @@
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintLeft_toRightOf="@id/image_author_thumbnail" app:layout_constraintLeft_toRightOf="@id/image_author_thumbnail"
app:layout_constraintRight_toLeftOf="@id/image_neopass_channel" app:layout_constraintRight_toLeftOf="@id/platform_indicator"
app:layout_constraintTop_toTopOf="@id/image_author_thumbnail" app:layout_constraintTop_toTopOf="@id/image_author_thumbnail"
tools:text="Two Minute Papers" /> tools:text="Two Minute Papers" />
<ImageView
android:id="@+id/image_neopass_channel"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="8dp"
android:contentDescription="@string/neopass_channel"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/text_author_name"
app:layout_constraintLeft_toRightOf="@id/text_author_name"
app:layout_constraintRight_toLeftOf="@id/platform_indicator"
app:layout_constraintTop_toTopOf="@id/text_author_name"
app:srcCompat="@drawable/neopass" />
<TextView <TextView
android:id="@+id/text_metadata" android:id="@+id/text_metadata"
android:layout_width="0dp" android:layout_width="0dp"

View file

@ -232,7 +232,7 @@
<TextView <TextView
android:id="@+id/text_channel_name" android:id="@+id/text_channel_name"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:gravity="center_vertical" android:gravity="center_vertical"
@ -246,23 +246,10 @@
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintLeft_toRightOf="@id/image_channel_thumbnail" app:layout_constraintLeft_toRightOf="@id/image_channel_thumbnail"
app:layout_constraintRight_toLeftOf="@id/image_neopass_channel" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_video_name" app:layout_constraintTop_toBottomOf="@id/text_video_name"
android:layout_marginStart="4dp" /> android:layout_marginStart="4dp" />
<ImageView
android:id="@+id/image_neopass_channel"
android:layout_width="10dp"
android:layout_height="10dp"
android:contentDescription="@string/neopass_channel"
app:srcCompat="@drawable/neopass"
app:layout_constraintLeft_toRightOf="@id/text_channel_name"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/text_channel_name"
app:layout_constraintBottom_toBottomOf="@id/text_channel_name"
android:layout_marginStart="4dp"
android:visibility="gone"/>
<TextView <TextView
android:id="@+id/text_video_metadata" android:id="@+id/text_video_metadata"
android:layout_width="0dp" android:layout_width="0dp"

View file

@ -270,7 +270,7 @@
<TextView <TextView
android:id="@+id/text_channel_name" android:id="@+id/text_channel_name"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:gravity="center_vertical" android:gravity="center_vertical"
@ -284,23 +284,10 @@
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintLeft_toRightOf="@id/image_channel_thumbnail" app:layout_constraintLeft_toRightOf="@id/image_channel_thumbnail"
app:layout_constraintRight_toLeftOf="@id/image_neopass_channel" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_video_name" app:layout_constraintTop_toBottomOf="@id/text_video_name"
android:layout_marginStart="4dp" /> android:layout_marginStart="4dp" />
<ImageView
android:id="@+id/image_neopass_channel"
android:layout_width="10dp"
android:layout_height="10dp"
android:contentDescription="@string/neopass_channel"
app:srcCompat="@drawable/neopass"
app:layout_constraintLeft_toRightOf="@id/text_channel_name"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/text_channel_name"
app:layout_constraintBottom_toBottomOf="@id/text_channel_name"
android:layout_marginStart="4dp"
android:visibility="gone"/>
<TextView <TextView
android:id="@+id/text_video_metadata" android:id="@+id/text_video_metadata"
android:layout_width="0dp" android:layout_width="0dp"
@ -317,8 +304,6 @@
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
android:layout_marginStart="4dp"/> android:layout_marginStart="4dp"/>
<com.futo.platformplayer.views.platform.PlatformIndicator <com.futo.platformplayer.views.platform.PlatformIndicator
android:id="@+id/thumbnail_platform_nested" android:id="@+id/thumbnail_platform_nested"
android:layout_width="20dp" android:layout_width="20dp"

View file

@ -21,5 +21,20 @@
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/topbar" app:layout_constraintTop_toBottomOf="@id/topbar"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintBottom_toBottomOf="parent" />
<ImageView
android:id="@+id/button_settings"
android:background="@drawable/background_pill"
android:padding="5dp"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:srcCompat="@drawable/ic_settings" />
<FrameLayout
android:id="@+id/overlay_container_queue"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="horizontal">
<LinearLayout
android:id="@+id/container_tags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" />
</ScrollView>
</LinearLayout>

View file

@ -286,6 +286,8 @@
<string name="also_removes_any_data_related_plugin_like_login_or_settings">Also removes any data related plugin like login or settings</string> <string name="also_removes_any_data_related_plugin_like_login_or_settings">Also removes any data related plugin like login or settings</string>
<string name="announcement">Announcement</string> <string name="announcement">Announcement</string>
<string name="notifications">Notifications</string> <string name="notifications">Notifications</string>
<string name="check_disabled_plugin_updates">Check disabled plugins for updates</string>
<string name="check_disabled_plugin_updates_description">Check disabled plugins for updates</string>
<string name="planned_content_notifications">Planned Content Notifications</string> <string name="planned_content_notifications">Planned Content Notifications</string>
<string name="planned_content_notifications_description">Schedules discovered planned content as notifications, resulting in more accurate notifications for this content.</string> <string name="planned_content_notifications_description">Schedules discovered planned content as notifications, resulting in more accurate notifications for this content.</string>
<string name="attempt_to_utilize_byte_ranges">Attempt to utilize byte ranges</string> <string name="attempt_to_utilize_byte_ranges">Attempt to utilize byte ranges</string>
@ -416,7 +418,7 @@
<string name="log_level">Log Level</string> <string name="log_level">Log Level</string>
<string name="logging">Logging</string> <string name="logging">Logging</string>
<string name="sync_grayjay">Sync Grayjay</string> <string name="sync_grayjay">Sync Grayjay</string>
<string name="sync_grayjay_description">Sync your settings across multiple devices</string> <string name="sync_grayjay_description">Sync your data across multiple devices</string>
<string name="manage_polycentric_identity">Manage Polycentric identity</string> <string name="manage_polycentric_identity">Manage Polycentric identity</string>
<string name="manage_your_polycentric_identity">Manage your Polycentric identity</string> <string name="manage_your_polycentric_identity">Manage your Polycentric identity</string>
<string name="manual_check">Manual check</string> <string name="manual_check">Manual check</string>
@ -445,6 +447,8 @@
<string name="preferred_preview_quality">Preferred Preview Quality</string> <string name="preferred_preview_quality">Preferred Preview Quality</string>
<string name="preferred_preview_quality_description">Default quality while previewing a video in a feed</string> <string name="preferred_preview_quality_description">Default quality while previewing a video in a feed</string>
<string name="primary_language">Primary Language</string> <string name="primary_language">Primary Language</string>
<string name="prefer_original_audio">Prefer Original Audio</string>
<string name="prefer_original_audio_description">Use original audio instead of preferred language when it is known</string>
<string name="default_comment_section">Default Comment Section</string> <string name="default_comment_section">Default Comment Section</string>
<string name="hide_recommendations">Hide Recommendations</string> <string name="hide_recommendations">Hide Recommendations</string>
<string name="hide_recommendations_description">Fully hide the recommendations tab.</string> <string name="hide_recommendations_description">Fully hide the recommendations tab.</string>

@ -1 +0,0 @@
Subproject commit f79c7141bcb11464103abc56fd7be492fe8568ab

@ -0,0 +1 @@
Subproject commit 07e39f9df71b1937adf5bfb718a115fc232aa6f8

@ -1 +1 @@
Subproject commit 13b30fd76e30a60c114c97b876542f7f106b5881 Subproject commit ce0571bdeaed4e341351ef477ef4b6599aa4d0fb

@ -1 +1 @@
Subproject commit 8d7c0e252738450f2a8bb2a48e9f8bdc24cfea54 Subproject commit 3fbd872ad8bd7df62c5fbec7437e1200d82b74e1

@ -1 +1 @@
Subproject commit d00c7ff8e557d8b5624c162e4e554f65625c5e29 Subproject commit b34134ca2dbb1662b060b4a67f14e7c5d077889d

Some files were not shown because too many files have changed in this diff Show more