Compare commits

...

12 Commits

Author SHA1 Message Date
Koen J c83a9924e2 Implemented new ApiMethods calls. 2025-03-05 17:04:48 +01:00
Koen J bbeb9b83a0 Removed dynamic Polycentric calls. 2025-03-05 11:58:09 +01:00
Kelvin 06478f3e36 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-02-27 14:34:59 +01:00
Kelvin 40f20002b2 submods 2025-02-27 14:34:51 +01:00
Koen J 442272f517 SettingsActivity can now be landscape. 2025-02-27 10:38:03 +01:00
Kelvin 88dae8e9c4 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2025-02-26 21:29:40 +01:00
Kelvin 1bbfa7d39e WIP home filtering 2025-02-26 21:29:06 +01:00
Koen J edc2b3d295 Fixed issue where video reload would reset video timestamp. 2025-02-25 15:32:30 +01:00
Koen J 0006da7385 Implemented sync display names. 2025-02-25 11:00:54 +01:00
Kai DeLorenzo b5ac8b3ec6 Edit Authentication.md 2025-02-20 21:59:29 +00:00
Kai 78f5169880 add recommendations assignment in video details class
Changelog: added
2025-02-20 14:43:13 -06:00
Kelvin 3361b77aec Remove accidental always update 2025-02-13 21:00:02 +01:00
89 changed files with 430 additions and 1184 deletions
+1 -1
View File
@@ -197,7 +197,7 @@ dependencies {
implementation 'org.jsoup:jsoup:1.15.3'
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
implementation 'com.arthenica:ffmpeg-kit-full:6.0-2.LTS'
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
implementation 'com.github.dhaval2404:imagepicker:2.1'
implementation 'com.google.zxing:core:3.4.1'
-1
View File
@@ -156,7 +156,6 @@
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.SettingsActivity"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.FutoVideo.NoActionBar" />
<activity
android:name=".activities.DeveloperActivity"
+4
View File
@@ -263,6 +263,10 @@ class PlatformVideoDetails extends PlatformVideo {
this.rating = obj.rating ?? null; //IRating
this.subtitles = obj.subtitles ?? [];
this.isShort = !!obj.isShort ?? false;
if (obj.getContentRecommendations) {
this.getContentRecommendations = obj.getContentRecommendations
}
}
}
@@ -1,13 +1,13 @@
package com.futo.platformplayer
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StatePlatform
import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.base64UrlToByteArray
import userpackage.Protocol
import kotlin.math.abs
import kotlin.math.min
@@ -40,33 +40,25 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
}
fun String.getDataLinkFromUrl(): Protocol.URLInfoDataLink? {
val urlData = if (this.startsWith("polycentric://")) {
this.substring("polycentric://".length)
} else this;
val urlBytes = urlData.base64UrlToByteArray();
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
if (urlInfo.urlType != 4L) {
return null
}
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
return dataLink
}
fun Protocol.Claim.resolveChannelUrl(): String? {
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
}
fun Protocol.Claim.resolveChannelUrls(): List<String> {
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
}
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
Logger.w("Backfill", "Polycentric prod server not added, adding it.")
addServer(PolycentricCache.SERVER)
}
val exceptions = fullyBackfillServers()
for (pair in exceptions) {
val server = pair.key
val exception = pair.value
StateAnnouncement.instance.registerAnnouncement(
"backfill-failed",
"Backfill failed",
"Failed to backfill server $server. $exception",
AnnouncementType.SESSION_RECURRING
);
Logger.e("Backfill", "Failed to backfill server $server.", exception)
}
}
@@ -11,16 +11,16 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.LoaderView
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -87,7 +87,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
Logger.e(TAG, "Failed to save process secret to secret storage.", e)
}
processHandle.addServer(PolycentricCache.SERVER);
processHandle.addServer(ApiMethods.SERVER);
processHandle.setUsername(username);
StatePolycentric.instance.setProcessHandle(processHandle);
} catch (e: Throwable) {
@@ -12,12 +12,12 @@ import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.setNavigationBarColorAndIcons
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.KeyPair
import com.futo.polycentric.core.Process
import com.futo.polycentric.core.ProcessSecret
@@ -145,7 +145,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() {
}
StatePolycentric.instance.setProcessHandle(processHandle);
processHandle.fullyBackfillClient(PolycentricCache.SERVER);
processHandle.fullyBackfillClient(ApiMethods.SERVER);
withContext(Dispatchers.Main) {
startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java));
finish();
@@ -21,10 +21,8 @@ import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.dp
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.setNavigationBarColorAndIcons
@@ -32,8 +30,10 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.platformplayer.views.overlays.LoaderOverlay
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
import com.futo.polycentric.core.toBase64Url
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
@@ -145,7 +145,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) {
try {
processHandle.fullyBackfillClient(PolycentricCache.SERVER)
processHandle.fullyBackfillClient(ApiMethods.SERVER)
withContext(Dispatchers.Main) {
updateUI();
@@ -101,7 +101,8 @@ class SyncHomeActivity : AppCompatActivity() {
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
val connected = session?.connected ?: false
syncDeviceView.setLinkType(if (connected) LinkType.Local else LinkType.None)
.setName(publicKey)
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
//TODO: also display public key?
.setStatus(if (connected) "Connected" else "Disconnected")
return syncDeviceView
}
@@ -22,7 +22,6 @@ import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComm
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.dp
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
@@ -30,6 +29,7 @@ import com.futo.platformplayer.states.StatePolycentric
import com.futo.polycentric.core.ClaimType
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.material.button.MaterialButton
@@ -13,7 +13,6 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.dp
import com.futo.platformplayer.fixHtmlLinks
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.selectBestImage
@@ -21,6 +20,7 @@ import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.toHumanNumber
import com.futo.platformplayer.views.platform.PlatformLinkView
import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.toName
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
@@ -134,9 +134,7 @@ class ChannelAboutFragment : Fragment, IChannelTabFragment {
}
}
if(!map.containsKey("Harbor"))
this.context?.let {
map.set("Harbor", polycentricProfile.getHarborUrl(it));
}
map.set("Harbor", polycentricProfile.getHarborUrl());
if (map.isNotEmpty())
setLinks(map, if (polycentricProfile.systemState.username.isNotBlank()) polycentricProfile.systemState.username else _lastChannel?.name ?: "")
@@ -29,7 +29,6 @@ import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StatePlatform
@@ -39,6 +38,7 @@ import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.feedtypes.PreviewContentListAdapter
import com.futo.polycentric.core.PolycentricProfile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.math.max
@@ -16,12 +16,12 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.viewholders.CreatorViewHolder
import com.futo.polycentric.core.PolycentricProfile
class ChannelListFragment : Fragment, IChannelTabFragment {
private var _channels: ArrayList<IPlatformChannel> = arrayListOf();
@@ -8,8 +8,8 @@ import android.widget.TextView
import androidx.fragment.app.Fragment
import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.views.SupportView
import com.futo.polycentric.core.PolycentricProfile
class ChannelMonetizationFragment : Fragment, IChannelTabFragment {
@@ -1,7 +1,7 @@
package com.futo.platformplayer.fragment.channel.tab
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.polycentric.core.PolycentricProfile
interface IChannelTabFragment {
fun setChannel(channel: IPlatformChannel)
@@ -42,7 +42,6 @@ import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.selectHighestResolutionImage
import com.futo.platformplayer.states.StatePlatform
@@ -55,29 +54,14 @@ import com.futo.platformplayer.views.adapters.ChannelViewPagerAdapter
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.views.subscriptions.SubscribeButton
import com.futo.polycentric.core.OwnedClaim
import com.futo.polycentric.core.PublicKey
import com.futo.polycentric.core.Store
import com.futo.polycentric.core.SystemState
import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
@Serializable
data class PolycentricProfile(
val system: PublicKey, val systemState: SystemState, val ownedClaims: List<OwnedClaim>
) {
fun getHarborUrl(context: Context): String{
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system));
val url = system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable());
return "https://harbor.social/" + url.substring("polycentric://".length);
}
}
class ChannelFragment : MainFragment() {
override val isMainView: Boolean = true
@@ -144,15 +128,14 @@ class ChannelFragment : MainFragment() {
private val _onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {}
private val _taskLoadPolycentricProfile: TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>
private val _taskLoadPolycentricProfile: TaskHandler<PlatformID, PolycentricProfile?>
private val _taskGetChannel: TaskHandler<String, IPlatformChannel>
init {
inflater.inflate(R.layout.fragment_channel, this)
_taskLoadPolycentricProfile =
TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>({ fragment.lifecycleScope },
_taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>({ fragment.lifecycleScope },
{ id ->
return@TaskHandler PolycentricCache.instance.getProfileAsync(id)
return@TaskHandler ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, id.claimFieldType.toLong(), id.claimType.toLong(), id.value!!)
}).success { setPolycentricProfile(it, animate = true) }.exception<Throwable> {
Logger.w(TAG, "Failed to load polycentric profile.", it)
}
@@ -328,7 +311,7 @@ class ChannelFragment : MainFragment() {
_creatorThumbnail.setThumbnail(parameter.thumbnail, true)
Glide.with(_imageBanner).clear(_imageBanner)
loadPolycentricProfile(parameter.id, parameter.url)
loadPolycentricProfile(parameter.id)
}
_url = parameter.url
@@ -342,7 +325,7 @@ class ChannelFragment : MainFragment() {
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true)
Glide.with(_imageBanner).clear(_imageBanner)
loadPolycentricProfile(parameter.channel.id, parameter.channel.url)
loadPolycentricProfile(parameter.channel.id)
}
_url = parameter.channel.url
@@ -359,16 +342,8 @@ class ChannelFragment : MainFragment() {
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
}
private fun loadPolycentricProfile(id: PlatformID, url: String) {
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true)
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = true)
if (cachedPolycentricProfile.expired) {
_taskLoadPolycentricProfile.run(id)
}
} else {
_taskLoadPolycentricProfile.run(id)
}
private fun loadPolycentricProfile(id: PlatformID) {
_taskLoadPolycentricProfile.run(id)
}
private fun setLoading(isLoading: Boolean) {
@@ -533,20 +508,13 @@ class ChannelFragment : MainFragment() {
private fun setPolycentricProfileOr(url: String, or: () -> Unit) {
setPolycentricProfile(null, animate = false)
val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(url) }
if (cachedProfile != null) {
setPolycentricProfile(cachedProfile, animate = false)
} else {
or()
}
or()
}
private fun setPolycentricProfile(
cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean
profile: PolycentricProfile?, animate: Boolean
) {
val dp35 = 35.dp(resources)
val profile = cachedPolycentricProfile?.profile
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)?.let {
it.toURLInfoSystemLinkUrl(
profile.system.toProto(), it.process, profile.systemState.servers.toList()
@@ -23,7 +23,6 @@ import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
@@ -32,6 +31,7 @@ import com.futo.platformplayer.views.adapters.CommentWithReferenceViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.overlays.RepliesOverlay
import com.futo.polycentric.core.PublicKey
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.net.UnknownHostException
@@ -23,6 +23,7 @@ import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.NoResultsView
import com.futo.platformplayer.views.ToggleBar
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.platformplayer.views.adapters.InsertedViewHolder
@@ -94,6 +95,8 @@ class HomeFragment : MainFragment() {
class HomeView : ContentFeedView<HomeFragment> {
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
private var _toggleBar: ToggleBar? = null;
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
@@ -127,6 +130,8 @@ class HomeFragment : MainFragment() {
}, fragment);
};
initializeToolbarContent();
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
showAnnouncementView()
}
@@ -201,13 +206,43 @@ class HomeFragment : MainFragment() {
loadResults();
}
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
return results.filter { !StateMeta.instance.isVideoHidden(it.url) && !StateMeta.instance.isCreatorHidden(it.author.url) };
private val _filterLock = Object();
private var _toggleRecent = false;
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);
_taskGetPager.run(true);
_taskGetPager.run(withRefetch);
}
private fun loadedResult(pager : IPager<IPlatformContent>) {
if (pager is EmptyPager<IPlatformContent>) {
@@ -33,10 +33,8 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp
import com.futo.platformplayer.fixHtmlWhitespace
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
@@ -47,7 +45,6 @@ import com.futo.platformplayer.views.adapters.ChannelTab
import com.futo.platformplayer.views.adapters.feedtypes.PreviewPostView
import com.futo.platformplayer.views.comments.AddCommentView
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.others.Toggle
import com.futo.platformplayer.views.overlays.RepliesOverlay
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
import com.futo.platformplayer.views.platform.PlatformIndicator
@@ -57,6 +54,8 @@ import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Models
import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import com.google.android.flexbox.FlexboxLayout
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.shape.CornerFamily
@@ -112,7 +111,7 @@ class PostDetailFragment : MainFragment {
private var _isLoading = false;
private var _post: IPlatformPostDetails? = null;
private var _postOverview: IPlatformPost? = null;
private var _polycentricProfile: PolycentricCache.CachedPolycentricProfile? = null;
private var _polycentricProfile: PolycentricProfile? = null;
private var _version = 0;
private var _isRepliesVisible: Boolean = false;
private var _repliesAnimator: ViewPropertyAnimator? = null;
@@ -169,7 +168,7 @@ class PostDetailFragment : MainFragment {
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_post), it, ::fetchPost, null, _fragment);
} else TaskHandler(IPlatformPostDetails::class.java) { _fragment.lifecycleScope };
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) })
.success { it -> setPolycentricProfile(it, animate = true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load claims.", it);
@@ -274,7 +273,7 @@ class PostDetailFragment : MainFragment {
};
_buttonStore.setOnClickListener {
_polycentricProfile?.profile?.systemState?.store?.let {
_polycentricProfile?.systemState?.store?.let {
try {
val uri = Uri.parse(it);
val intent = Intent(Intent.ACTION_VIEW);
@@ -334,7 +333,7 @@ class PostDetailFragment : MainFragment {
}
try {
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null,
val queryReferencesResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, ref, null,null,
arrayListOf(
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(
ContentType.OPINION.value).setValue(
@@ -604,16 +603,8 @@ class PostDetailFragment : MainFragment {
private fun fetchPolycentricProfile() {
val author = _post?.author ?: _postOverview?.author ?: return;
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(author.url, true);
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = false);
if (cachedPolycentricProfile.expired) {
_taskLoadPolycentricProfile.run(author.id);
}
} else {
setPolycentricProfile(null, animate = false);
_taskLoadPolycentricProfile.run(author.id);
}
}
private fun setChannelMeta(value: IPlatformPost?) {
@@ -639,17 +630,18 @@ class PostDetailFragment : MainFragment {
_repliesOverlay.cleanup();
}
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
_polycentricProfile = cachedPolycentricProfile;
private fun setPolycentricProfile(polycentricProfile: PolycentricProfile?, animate: Boolean) {
_polycentricProfile = polycentricProfile;
if (cachedPolycentricProfile?.profile == null) {
val pp = _polycentricProfile;
if (pp == null) {
_layoutMonetization.visibility = View.GONE;
_creatorThumbnail.setHarborAvailable(false, animate, null);
return;
}
_layoutMonetization.visibility = View.VISIBLE;
_creatorThumbnail.setHarborAvailable(true, animate, cachedPolycentricProfile.profile.system.toProto());
_creatorThumbnail.setHarborAvailable(true, animate, pp.system.toProto());
}
private fun fetchPost() {
@@ -556,7 +556,7 @@ class SourceDetailFragment : MainFragment() {
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
val config = SourcePluginConfig.fromJson(configJson);
if (config.version <= c.version && config.name != "Youtube") {
if (config.version <= c.version) {
Logger.i(TAG, "Plugin is up to date.");
withContext(Dispatchers.Main) { UIDialogs.toast(context.getString(R.string.plugin_is_fully_up_to_date)); };
return@launch;
@@ -94,12 +94,10 @@ import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.exceptions.UnsupportedCastException
import com.futo.platformplayer.fixHtmlLinks
import com.futo.platformplayer.fixHtmlWhitespace
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.getNowDiffSeconds
import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.AnnouncementType
@@ -158,6 +156,8 @@ import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Models
import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.protobuf.ByteString
import kotlinx.coroutines.Dispatchers
@@ -294,7 +294,7 @@ class VideoDetailView : ConstraintLayout {
private set;
private var _historicalPosition: Long = 0;
private var _commentsCount = 0;
private var _polycentricProfile: PolycentricCache.CachedPolycentricProfile? = null;
private var _polycentricProfile: PolycentricProfile? = null;
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
private var _autoplayVideo: IPlatformVideo? = null
@@ -409,12 +409,12 @@ class VideoDetailView : ConstraintLayout {
};
_monetization.onSupportTap.subscribe {
_container_content_support.setPolycentricProfile(_polycentricProfile?.profile);
_container_content_support.setPolycentricProfile(_polycentricProfile);
switchContentView(_container_content_support);
};
_monetization.onStoreTap.subscribe {
_polycentricProfile?.profile?.systemState?.store?.let {
_polycentricProfile?.systemState?.store?.let {
try {
val uri = Uri.parse(it);
val intent = Intent(Intent.ACTION_VIEW);
@@ -579,6 +579,14 @@ class VideoDetailView : ConstraintLayout {
_minimize_title.setOnClickListener { onMaximize.emit(false) };
_minimize_meta.setOnClickListener { onMaximize.emit(false) };
_player.onStateChange.subscribe {
if (_player.activelyPlaying) {
Logger.i(TAG, "Play changed, resetting error counter _didTriggerDatasourceErrorCount = 0 (_player.activelyPlaying: ${_player.activelyPlaying})")
_didTriggerDatasourceErrorCount = 0;
_didTriggerDatasourceError = false;
}
}
_player.onPlayChanged.subscribe {
if (StateCasting.instance.activeDevice == null) {
handlePlayChanged(it);
@@ -922,7 +930,7 @@ class VideoDetailView : ConstraintLayout {
} else if(devices.size == 1){
val device = devices.first();
Logger.i(TAG, "Send to device? (public key: ${device.remotePublicKey}): " + videoToSend.url)
UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non ${device.remotePublicKey}" , {
UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non '${device.displayName}'" , {
Logger.i(TAG, "Send to device confirmed (public key: ${device.remotePublicKey}): " + videoToSend.url)
fragment.lifecycleScope.launch(Dispatchers.IO) {
@@ -963,6 +971,7 @@ class VideoDetailView : ConstraintLayout {
throw IllegalStateException("Expected media content, found ${video.contentType}");
withContext(Dispatchers.Main) {
_videoResumePositionMilliseconds = _player.position
setVideoDetails(video);
}
}
@@ -1227,16 +1236,8 @@ class VideoDetailView : ConstraintLayout {
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
_channelName.text = video.author.name;
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url, true);
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = false);
if (cachedPolycentricProfile.expired) {
_taskLoadPolycentricProfile.run(video.author.id);
}
} else {
setPolycentricProfile(null, animate = false);
_taskLoadPolycentricProfile.run(video.author.id);
}
setPolycentricProfile(null, animate = false);
_taskLoadPolycentricProfile.run(video.author.id);
_player.clear();
@@ -1265,8 +1266,6 @@ class VideoDetailView : ConstraintLayout {
@OptIn(ExperimentalCoroutinesApi::class)
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
_didTriggerDatasourceErrroCount = 0;
_didTriggerDatasourceError = false;
_autoplayVideo = null
Logger.i(TAG, "Autoplay video cleared (setVideoDetails)")
@@ -1277,6 +1276,10 @@ class VideoDetailView : ConstraintLayout {
_lastVideoSource = null;
_lastAudioSource = null;
_lastSubtitleSource = null;
Logger.i(TAG, "_didTriggerDatasourceErrorCount reset to 0 because new video")
_didTriggerDatasourceErrorCount = 0;
_didTriggerDatasourceError = false;
}
if (videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now())
@@ -1394,11 +1397,8 @@ class VideoDetailView : ConstraintLayout {
setTabIndex(2, true)
} else {
when (Settings.instance.comments.defaultCommentSection) {
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(
0,
true
) else setTabIndex(1, true);
1 -> setTabIndex(1, true);
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(0, true) else setTabIndex(1, true)
1 -> setTabIndex(1, true)
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
}
}
@@ -1436,16 +1436,8 @@ class VideoDetailView : ConstraintLayout {
_buttonSubscribe.setSubscribeChannel(video.author.url);
setDescription(video.description.fixHtmlLinks());
_creatorThumbnail.setThumbnail(video.author.thumbnail, false);
val cachedPolycentricProfile =
PolycentricCache.instance.getCachedProfile(video.author.url, true);
if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = false);
} else {
setPolycentricProfile(null, animate = false);
_taskLoadPolycentricProfile.run(video.author.id);
}
setPolycentricProfile(null, animate = false);
_taskLoadPolycentricProfile.run(video.author.id);
_platform.setPlatformFromClientID(video.id.pluginId);
val subTitleSegments: ArrayList<String> = ArrayList();
@@ -1474,7 +1466,7 @@ class VideoDetailView : ConstraintLayout {
fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
val queryReferencesResponse = ApiMethods.getQueryReferences(
PolycentricCache.SERVER, ref, null, null,
ApiMethods.SERVER, ref, null, null,
arrayListOf(
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
.setFromType(ContentType.OPINION.value).setValue(
@@ -1490,10 +1482,8 @@ class VideoDetailView : ConstraintLayout {
val likes = queryReferencesResponse.countsList[0];
val dislikes = queryReferencesResponse.countsList[1];
val hasLiked =
StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
val hasDisliked =
StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
withContext(Dispatchers.Main) {
_rating.visibility = View.VISIBLE;
@@ -1831,7 +1821,7 @@ class VideoDetailView : ConstraintLayout {
}
}
private var _didTriggerDatasourceErrroCount = 0;
private var _didTriggerDatasourceErrorCount = 0;
private var _didTriggerDatasourceError = false;
private fun onDataSourceError(exception: Throwable) {
Logger.e(TAG, "onDataSourceError", exception);
@@ -1841,32 +1831,53 @@ class VideoDetailView : ConstraintLayout {
return;
val config = currentVideo.sourceConfig;
if(_didTriggerDatasourceErrroCount <= 3) {
if(_didTriggerDatasourceErrorCount <= 3) {
_didTriggerDatasourceError = true;
_didTriggerDatasourceErrroCount++;
_didTriggerDatasourceErrorCount++;
UIDialogs.toast("Detected video error, attempting automatic reload (${_didTriggerDatasourceErrorCount})");
Logger.i(TAG, "Block detected, attempting bypass (_didTriggerDatasourceErrorCount = ${_didTriggerDatasourceErrorCount})");
UIDialogs.toast("Block detected, attempting bypass");
//return;
fragment.lifecycleScope.launch(Dispatchers.IO) {
val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await();
val previousVideoSource = _lastVideoSource;
val previousAudioSource = _lastAudioSource;
try {
val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await();
val previousVideoSource = _lastVideoSource;
val previousAudioSource = _lastAudioSource;
if(newDetails is IPlatformVideoDetails) {
val newVideoSource = if(previousVideoSource != null)
VideoHelper.selectBestVideoSource(newDetails.video, previousVideoSource.height * previousVideoSource.width, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS);
else null;
val newAudioSource = if(previousAudioSource != null)
VideoHelper.selectBestAudioSource(newDetails.video, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS, previousAudioSource.language, previousAudioSource.bitrate.toLong());
else null;
withContext(Dispatchers.Main) {
video = newDetails;
_player.setSource(newVideoSource, newAudioSource, true, true);
if (newDetails is IPlatformVideoDetails) {
val newVideoSource = if (previousVideoSource != null)
VideoHelper.selectBestVideoSource(
newDetails.video,
previousVideoSource.height * previousVideoSource.width,
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
);
else null;
val newAudioSource = if (previousAudioSource != null)
VideoHelper.selectBestAudioSource(
newDetails.video,
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
previousAudioSource.language,
previousAudioSource.bitrate.toLong()
);
else null;
withContext(Dispatchers.Main) {
video = newDetails;
_player.setSource(newVideoSource, newAudioSource, true, true);
}
}
} catch (e: Throwable) {
Logger.e(TAG, "Failed to get video details, attempting retrying without reloading.", e)
fragment.lifecycleScope.launch(Dispatchers.Main) {
video?.let {
_videoResumePositionMilliseconds = _player.position
setVideoDetails(it, false)
}
}
}
}
}
else if(_didTriggerDatasourceErrroCount > 3) {
else if(_didTriggerDatasourceErrorCount > 3) {
UIDialogs.showDialog(context, R.drawable.ic_error_pred,
context.getString(R.string.media_error),
context.getString(R.string.the_media_source_encountered_an_unauthorized_error_this_might_be_solved_by_a_plugin_reload_would_you_like_to_reload_experimental),
@@ -2773,13 +2784,12 @@ class VideoDetailView : ConstraintLayout {
}
}
private fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
_polycentricProfile = cachedPolycentricProfile;
private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
_polycentricProfile = profile
val dp_35 = 35.dp(context.resources)
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_35 * dp_35)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate);
@@ -2788,12 +2798,12 @@ class VideoDetailView : ConstraintLayout {
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
}
val username = cachedPolycentricProfile?.profile?.systemState?.username
val username = profile?.systemState?.username
if (username != null) {
_channelName.text = username
}
_monetization.setPolycentricProfile(cachedPolycentricProfile);
_monetization.setPolycentricProfile(profile);
}
fun setProgressBarOverlayed(isOverlayed: Boolean?) {
@@ -2981,7 +2991,7 @@ class VideoDetailView : ConstraintLayout {
Logger.w(TAG, "Failed to load recommendations.", it);
};
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(StateApp.instance.scopeGetter, { PolycentricCache.instance.getProfileAsync(it) })
private val _taskLoadPolycentricProfile = TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) })
.success { it -> setPolycentricProfile(it, animate = true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load claims.", it);
@@ -14,9 +14,9 @@ import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.views.casting.CastButton
import com.futo.polycentric.core.PolycentricProfile
class NavigationTopBarFragment : TopFragment() {
private var _buttonBack: ImageButton? = null;
@@ -1,5 +1,7 @@
package com.futo.platformplayer.images;
import static com.futo.platformplayer.Extensions_PolycentricKt.getDataLinkFromUrl;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -12,10 +14,14 @@ import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import com.bumptech.glide.signature.ObjectKey;
import com.futo.platformplayer.polycentric.PolycentricCache;
import com.futo.polycentric.core.ApiMethods;
import kotlin.Unit;
import kotlinx.coroutines.CoroutineScopeKt;
import kotlinx.coroutines.Deferred;
import kotlinx.coroutines.Dispatchers;
import userpackage.Protocol;
import java.lang.Exception;
import java.nio.ByteBuffer;
import java.util.concurrent.CancellationException;
@@ -60,7 +66,14 @@ public class PolycentricModelLoader implements ModelLoader<String, ByteBuffer> {
@Override
public void loadData(@NonNull Priority priority, @NonNull DataFetcher.DataCallback<? super ByteBuffer> callback) {
Log.i("PolycentricModelLoader", this._model);
_deferred = PolycentricCache.getInstance().getDataAsync(_model);
Protocol.URLInfoDataLink dataLink = getDataLinkFromUrl(_model);
if (dataLink == null) {
callback.onLoadFailed(new Exception("Data link cannot be null"));
return;
}
_deferred = ApiMethods.Companion.getDataFromServerAndReassemble(CoroutineScopeKt.CoroutineScope(Dispatchers.getIO()), dataLink);
_deferred.invokeOnCompletion(throwable -> {
if (throwable != null) {
Log.e("PolycentricModelLoader", "getDataAsync failed throwable: " + throwable.toString());
@@ -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
}
}
}
@@ -6,7 +6,6 @@ import com.futo.platformplayer.api.media.structures.DedupContentPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.serializers.PlatformContentSerializer
import com.futo.platformplayer.stores.db.ManagedDBStore
@@ -50,14 +49,7 @@ class StateCache {
val subs = StateSubscriptions.instance.getSubscriptions();
Logger.i(TAG, "Subscriptions CachePager polycentric urls");
val allUrls = subs
.map {
val otherUrls = PolycentricCache.instance.getCachedProfile(it.channel.url)?.profile?.ownedClaims?.mapNotNull { c -> c.claim.resolveChannelUrl() } ?: listOf();
if(!otherUrls.contains(it.channel.url))
return@map listOf(listOf(it.channel.url), otherUrls).flatten();
else
return@map otherUrls;
}
.flatten()
.map { it.channel.url }
.distinct()
.filter { StatePlatform.instance.hasEnabledChannelClient(it) };
@@ -14,7 +14,7 @@ class StateMeta {
return when(lastCommentSection.value){
"Polycentric" -> 0;
"Platform" -> 1;
else -> 1
else -> 0
}
}
fun setLastCommentSection(value: Int) {
@@ -21,9 +21,7 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
import com.futo.platformplayer.awaitFirstDeferred
import com.futo.platformplayer.dp
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.polycentric.PolycentricStorage
import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.selectBestImage
@@ -33,6 +31,7 @@ import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.ClaimType
import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.ProcessHandle
import com.futo.polycentric.core.PublicKey
import com.futo.polycentric.core.SignedEvent
@@ -234,34 +233,7 @@ class StatePolycentric {
if (!enabled) {
return Pair(false, listOf(url));
}
var polycentricProfile: PolycentricProfile? = null;
try {
val polycentricCached = PolycentricCache.instance.getCachedProfile(url, cacheOnly)
polycentricProfile = polycentricCached?.profile;
if (polycentricCached == null && channelId != null) {
Logger.i("StateSubscriptions", "Get polycentric profile not cached");
if(!cacheOnly) {
polycentricProfile = runBlocking { PolycentricCache.instance.getProfileAsync(channelId, if(doCacheNull) url else null) }?.profile;
didUpdate = true;
}
} else {
Logger.i("StateSubscriptions", "Get polycentric profile cached");
}
}
catch(ex: Throwable) {
Logger.w(StateSubscriptions.TAG, "Polycentric getCachedProfile failed for subscriptions", ex);
//TODO: Some way to communicate polycentric failing without blocking here
}
if(polycentricProfile != null) {
val urls = polycentricProfile.ownedClaims.groupBy { it.claim.claimType }
.mapNotNull { it.value.firstOrNull()?.claim?.resolveChannelUrl() }.toMutableList();
if(urls.any { it.equals(url, true) })
return Pair(didUpdate, urls);
else
return Pair(didUpdate, listOf(url) + urls);
}
else
return Pair(didUpdate, listOf(url));
return Pair(didUpdate, listOf(url));
}
fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager<IPlatformContent>? {
@@ -325,7 +297,7 @@ class StatePolycentric {
id = PlatformID("polycentric", author, null, ClaimType.POLYCENTRIC.value.toInt()),
name = systemState.username,
url = author,
thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
subscribers = null
),
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
@@ -349,7 +321,7 @@ class StatePolycentric {
suspend fun getLikesDislikesReplies(reference: Protocol.Reference): LikesDislikesReplies {
ensureEnabled()
val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null,
val response = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, null,
null,
listOf(
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
@@ -382,7 +354,7 @@ class StatePolycentric {
}
val pointer = Protocol.Pointer.parseFrom(reference.reference)
val events = ApiMethods.getEvents(PolycentricCache.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder()
val events = ApiMethods.getEvents(ApiMethods.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder()
.addRangesForProcesses(Protocol.RangesForProcess.newBuilder()
.setProcess(pointer.process)
.addRanges(Protocol.Range.newBuilder()
@@ -400,11 +372,11 @@ class StatePolycentric {
}
val post = Protocol.Post.parseFrom(ev.content);
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(ApiMethods.SERVER));
val dp_25 = 25.dp(StateApp.instance.context.resources)
val profileEvents = ApiMethods.getQueryLatest(
PolycentricCache.SERVER,
ApiMethods.SERVER,
ev.system.toProto(),
listOf(
ContentType.AVATAR.value,
@@ -433,7 +405,7 @@ class StatePolycentric {
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
url = systemLinkUrl,
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
subscribers = null
),
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
@@ -445,12 +417,12 @@ class StatePolycentric {
)
}
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
suspend fun getCommentPager(contextUrl: String, reference: Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
if (!enabled) {
return EmptyPager()
}
val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null,
val response = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, null,
Protocol.QueryReferencesRequestEvents.newBuilder()
.setFromType(ContentType.POST.value)
.addAllCountLwwElementReferences(arrayListOf(
@@ -486,7 +458,7 @@ class StatePolycentric {
}
override suspend fun nextPageAsync() {
val nextPageResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, _cursor,
val nextPageResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, _cursor,
Protocol.QueryReferencesRequestEvents.newBuilder()
.setFromType(ContentType.POST.value)
.addAllCountLwwElementReferences(arrayListOf(
@@ -534,7 +506,7 @@ class StatePolycentric {
return@mapNotNull LazyComment(scope.async(_commentPoolDispatcher){
Logger.i(TAG, "Fetching comment data for [" + ev.system.key.toBase64() + "]");
val profileEvents = ApiMethods.getQueryLatest(
PolycentricCache.SERVER,
ApiMethods.SERVER,
ev.system.toProto(),
listOf(
ContentType.AVATAR.value,
@@ -558,7 +530,7 @@ class StatePolycentric {
val unixMilliseconds = ev.unixMilliseconds
//TODO: Don't use single hardcoded sderver here
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(ApiMethods.SERVER));
val dp_25 = 25.dp(StateApp.instance.context.resources)
return@async PolycentricPlatformComment(
contextUrl = contextUrl,
@@ -566,7 +538,7 @@ class StatePolycentric {
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
url = systemLinkUrl,
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
subscribers = null
),
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
@@ -1,54 +1,17 @@
package com.futo.platformplayer.states
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.ResultCapabilities
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.structures.*
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.findNonRuntimeException
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.getNowDiffDays
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.states.StateHistory.Companion
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringDateMapStorage
import com.futo.platformplayer.stores.SubscriptionStorage
import com.futo.platformplayer.stores.v2.ReconstructStore
import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.OffsetDateTime
import java.util.concurrent.ExecutionException
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinTask
import kotlin.collections.ArrayList
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlin.streams.asSequence
import kotlin.streams.toList
import kotlin.system.measureTimeMillis
/***
* Used to maintain subscription groups
@@ -15,7 +15,6 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.resolveChannelUrl
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringDateMapStorage
@@ -335,12 +334,6 @@ class StateSubscriptions {
return true;
}
//TODO: This causes issues, because what if the profile is not cached yet when the susbcribe button is loaded for example?
val cachedProfile = PolycentricCache.instance.getCachedProfile(urls.first(), true)?.profile;
if (cachedProfile != null) {
return cachedProfile.ownedClaims.any { c -> _subscriptions.hasItem { s -> c.claim.resolveChannelUrl() == s.channel.url } };
}
return false;
}
}
@@ -44,6 +44,7 @@ import kotlin.system.measureTimeMillis
class StateSync {
private val _authorizedDevices = FragmentedStorage.get<StringArrayStorage>("authorized_devices")
private val _nameStorage = FragmentedStorage.get<StringStringMapStorage>("sync_remembered_name_storage")
private val _syncKeyPair = FragmentedStorage.get<StringStorage>("sync_key_pair")
private val _lastAddressStorage = FragmentedStorage.get<StringStringMapStorage>("sync_last_address_storage")
private val _syncSessionData = FragmentedStorage.get<StringTMapStorage<SyncSessionData>>("syncSessionData")
@@ -305,12 +306,22 @@ class StateSync {
synchronized(_sessions) {
session = _sessions[s.remotePublicKey]
if (session == null) {
val remoteDeviceName = synchronized(_nameStorage) {
_nameStorage.get(remotePublicKey)
}
session = SyncSession(remotePublicKey, onAuthorized = { it, isNewlyAuthorized, isNewSession ->
if (!isNewSession) {
return@SyncSession
}
Logger.i(TAG, "${s.remotePublicKey} authorized")
it.remoteDeviceName?.let { remoteDeviceName ->
synchronized(_nameStorage) {
_nameStorage.setAndSave(remotePublicKey, remoteDeviceName)
}
}
Logger.i(TAG, "${s.remotePublicKey} authorized (name: ${it.displayName})")
synchronized(_lastAddressStorage) {
_lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress)
}
@@ -341,7 +352,7 @@ class StateSync {
deviceRemoved.emit(it.remotePublicKey)
})
}, remoteDeviceName)
_sessions[remotePublicKey] = session!!
}
session!!.addSocketSession(s)
@@ -469,6 +480,12 @@ class StateSync {
}
}
fun getCachedName(publicKey: String): String? {
return synchronized(_nameStorage) {
_nameStorage.get(publicKey)
}
}
suspend fun delete(publicKey: String) {
withContext(Dispatchers.IO) {
try {
@@ -1,31 +0,0 @@
package com.futo.platformplayer.stores
import com.futo.platformplayer.polycentric.PolycentricCache
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@kotlinx.serialization.Serializable
class CachedPolycentricProfileStorage : FragmentedStorageFileJson() {
var map: HashMap<String, PolycentricCache.CachedPolycentricProfile> = hashMapOf();
override fun encode(): String {
val encoded = Json.encodeToString(this);
return encoded;
}
fun get(key: String) : PolycentricCache.CachedPolycentricProfile? {
return map[key];
}
fun setAndSave(key: String, value: PolycentricCache.CachedPolycentricProfile) : PolycentricCache.CachedPolycentricProfile {
map[key] = value;
save();
return value;
}
fun setAndSaveBlocking(key: String, value: PolycentricCache.CachedPolycentricProfile) : PolycentricCache.CachedPolycentricProfile {
map[key] = value;
saveBlocking();
return value;
}
}
@@ -6,12 +6,10 @@ import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.smartMerge
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.states.StateSubscriptionGroups
import com.futo.platformplayer.states.StateSubscriptions
@@ -30,6 +28,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.ByteArrayInputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
@@ -53,6 +52,9 @@ class SyncSession : IAuthorizable {
private val _id = UUID.randomUUID()
private var _remoteId: UUID? = null
private var _lastAuthorizedRemoteId: UUID? = null
var remoteDeviceName: String? = null
private set
val displayName: String get() = remoteDeviceName ?: remotePublicKey
var connected: Boolean = false
private set(v) {
@@ -62,7 +64,7 @@ class SyncSession : IAuthorizable {
}
}
constructor(remotePublicKey: String, onAuthorized: (session: SyncSession, isNewlyAuthorized: Boolean, isNewSession: Boolean) -> Unit, onUnauthorized: (session: SyncSession) -> Unit, onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit, onClose: (session: SyncSession) -> Unit) {
constructor(remotePublicKey: String, onAuthorized: (session: SyncSession, isNewlyAuthorized: Boolean, isNewSession: Boolean) -> Unit, onUnauthorized: (session: SyncSession) -> Unit, onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit, onClose: (session: SyncSession) -> Unit, remoteDeviceName: String?) {
this.remotePublicKey = remotePublicKey
_onAuthorized = onAuthorized
_onUnauthorized = onUnauthorized
@@ -85,7 +87,20 @@ class SyncSession : IAuthorizable {
fun authorize(socketSession: SyncSocketSession) {
Logger.i(TAG, "Sent AUTHORIZED with session id $_id")
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(_id.toString().toByteArray()))
if (socketSession.remoteVersion >= 3) {
val idStringBytes = _id.toString().toByteArray()
val nameBytes = "${android.os.Build.MANUFACTURER}-${android.os.Build.MODEL}".toByteArray()
val buffer = ByteArray(1 + idStringBytes.size + 1 + nameBytes.size)
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).apply {
put(idStringBytes.size.toByte())
put(idStringBytes)
put(nameBytes.size.toByte())
put(nameBytes)
}.apply { flip() })
} else {
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(_id.toString().toByteArray()))
}
_authorized = true
checkAuthorized()
}
@@ -138,15 +153,37 @@ class SyncSession : IAuthorizable {
when (opcode) {
Opcode.NOTIFY_AUTHORIZED.value -> {
val str = data.toUtf8String()
_remoteId = if (data.remaining() >= 0) UUID.fromString(str) else UUID.fromString("00000000-0000-0000-0000-000000000000")
if (socketSession.remoteVersion >= 3) {
val idByteCount = data.get().toInt()
if (idByteCount > 64)
throw Exception("Id should always be smaller than 64 bytes")
val idBytes = ByteArray(idByteCount)
data.get(idBytes)
val nameByteCount = data.get().toInt()
if (nameByteCount > 64)
throw Exception("Name should always be smaller than 64 bytes")
val nameBytes = ByteArray(nameByteCount)
data.get(nameBytes)
_remoteId = UUID.fromString(idBytes.toString(Charsets.UTF_8))
remoteDeviceName = nameBytes.toString(Charsets.UTF_8)
} else {
val str = data.toUtf8String()
_remoteId = if (data.remaining() >= 0) UUID.fromString(str) else UUID.fromString("00000000-0000-0000-0000-000000000000")
remoteDeviceName = null
}
_remoteAuthorized = true
Logger.i(TAG, "Received AUTHORIZED with session id $_remoteId")
Logger.i(TAG, "Received AUTHORIZED with session id $_remoteId (device name: '${remoteDeviceName ?: "not set"}')")
checkAuthorized()
return
}
Opcode.NOTIFY_UNAUTHORIZED.value -> {
_remoteId = null
remoteDeviceName = null
_lastAuthorizedRemoteId = null
_remoteAuthorized = false
_onUnauthorized(this)
@@ -46,6 +46,8 @@ class SyncSocketSession {
val localPublicKey: String get() = _localPublicKey
private val _onData: (session: SyncSocketSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) -> Unit
var authorizable: IAuthorizable? = null
var remoteVersion: Int = -1
private set
val remoteAddress: String
@@ -162,11 +164,12 @@ class SyncSocketSession {
}
private fun performVersionCheck() {
val CURRENT_VERSION = 2
val CURRENT_VERSION = 3
val MINIMUM_VERSION = 2
_outputStream.writeInt(CURRENT_VERSION)
val version = _inputStream.readInt()
Logger.i(TAG, "performVersionCheck (version = $version)")
if (version != CURRENT_VERSION)
remoteVersion = _inputStream.readInt()
Logger.i(TAG, "performVersionCheck (version = $remoteVersion)")
if (remoteVersion < MINIMUM_VERSION)
throw Exception("Invalid version")
}
@@ -16,11 +16,11 @@ import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
import com.futo.platformplayer.views.adapters.viewholders.StoreItemViewHolder
import com.futo.platformplayer.views.platform.PlatformIndicator
import com.futo.polycentric.core.PolycentricProfile
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@@ -125,8 +125,7 @@ class MonetizationView : LinearLayout {
}
}
fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?) {
val profile = cachedPolycentricProfile?.profile;
fun setPolycentricProfile(profile: PolycentricProfile?) {
if (profile != null) {
if (profile.systemState.store.isNotEmpty()) {
_buttonStore.visibility = View.VISIBLE;
@@ -14,10 +14,10 @@ import androidx.core.view.isVisible
import androidx.core.view.size
import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.views.buttons.BigButton
import com.futo.polycentric.core.PolycentricProfile
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.material.imageview.ShapeableImageView
@@ -0,0 +1,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;
}
}
}
@@ -17,7 +17,7 @@ import com.futo.platformplayer.fragment.channel.tab.ChannelListFragment
import com.futo.platformplayer.fragment.channel.tab.ChannelMonetizationFragment
import com.futo.platformplayer.fragment.channel.tab.ChannelPlaylistsFragment
import com.futo.platformplayer.fragment.channel.tab.IChannelTabFragment
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.polycentric.core.PolycentricProfile
import com.google.android.material.tabs.TabLayout
@@ -18,8 +18,6 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.fixHtmlLinks
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
@@ -29,6 +27,7 @@ import com.futo.platformplayer.views.LoaderView
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.pills.PillButton
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
import com.futo.polycentric.core.ApiMethods
import com.futo.polycentric.core.Opinion
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -81,24 +80,18 @@ class CommentViewHolder : ViewHolder {
throw Exception("Not implemented for non polycentric comments")
}
if (args.hasLiked) {
args.processHandle.opinion(c.reference, Opinion.like);
val newOpinion: Opinion = if (args.hasLiked) {
Opinion.like
} else if (args.hasDisliked) {
args.processHandle.opinion(c.reference, Opinion.dislike);
Opinion.dislike
} else {
args.processHandle.opinion(c.reference, Opinion.neutral);
Opinion.neutral
}
_layoutComment.alpha = if (args.dislikes > 2 && args.dislikes.toFloat() / (args.likes + args.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f;
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try {
Logger.i(TAG, "Started backfill");
args.processHandle.fullyBackfillServersAnnounceExceptions();
Logger.i(TAG, "Finished backfill");
} catch (e: Throwable) {
Logger.e(TAG, "Failed to backfill servers.", e)
}
ApiMethods.setOpinion(args.processHandle, c.reference, newOpinion)
}
StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
@@ -16,7 +16,6 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.fixHtmlLinks
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod
import com.futo.platformplayer.states.StateApp
@@ -26,6 +25,7 @@ import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.pills.PillButton
import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.IdentityHashMap
@@ -16,7 +16,6 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.others.CreatorThumbnail
@@ -29,21 +28,12 @@ open class PlaylistView : LinearLayout {
protected val _imageThumbnail: ImageView
protected val _imageChannel: ImageView?
protected val _creatorThumbnail: CreatorThumbnail?
protected val _imageNeopassChannel: ImageView?;
protected val _platformIndicator: PlatformIndicator;
protected val _textPlaylistName: TextView
protected val _textVideoCount: TextView
protected val _textVideoCountLabel: TextView;
protected val _textPlaylistItems: TextView
protected val _textChannelName: TextView
protected var _neopassAnimator: ObjectAnimator? = null;
private val _taskLoadValidClaims = TaskHandler<PlatformID, PolycentricCache.CachedOwnedClaims>(StateApp.instance.scopeGetter,
{ PolycentricCache.instance.getValidClaimsAsync(it).await() })
.success { it -> updateClaimsLayout(it, animate = true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load claims.", it);
};
val onPlaylistClicked = Event1<IPlatformPlaylist>();
val onChannelClicked = Event1<PlatformAuthorLink>();
@@ -66,7 +56,6 @@ open class PlaylistView : LinearLayout {
_textVideoCountLabel = findViewById(R.id.text_video_count_label);
_textChannelName = findViewById(R.id.text_channel_name);
_textPlaylistItems = findViewById(R.id.text_playlist_items);
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
setOnClickListener { onOpenClicked() };
_imageChannel?.setOnClickListener { currentPlaylist?.let { onChannelClicked.emit(it.author) } };
@@ -88,20 +77,6 @@ open class PlaylistView : LinearLayout {
open fun bind(content: IPlatformContent) {
_taskLoadValidClaims.cancel();
if (content.author.id.claimType > 0) {
val cachedClaims = PolycentricCache.instance.getCachedValidClaims(content.author.id);
if (cachedClaims != null) {
updateClaimsLayout(cachedClaims, animate = false);
} else {
updateClaimsLayout(null, animate = false);
_taskLoadValidClaims.run(content.author.id);
}
} else {
updateClaimsLayout(null, animate = false);
}
isClickable = true;
_imageChannel?.let {
@@ -155,25 +130,6 @@ open class PlaylistView : LinearLayout {
}
}
private fun updateClaimsLayout(claims: PolycentricCache.CachedOwnedClaims?, animate: Boolean) {
_neopassAnimator?.cancel();
_neopassAnimator = null;
val firstClaim = claims?.ownedClaims?.firstOrNull();
val harborAvailable = firstClaim != null
if (harborAvailable) {
_imageNeopassChannel?.visibility = View.VISIBLE
if (animate) {
_neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500)
_neopassAnimator?.start()
}
} else {
_imageNeopassChannel?.visibility = View.GONE
}
_creatorThumbnail?.setHarborAvailable(harborAvailable, animate, firstClaim?.system?.toProto())
}
companion object {
private val TAG = "VideoPreviewViewHolder"
}
@@ -15,7 +15,6 @@ import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.toHumanTimeIndicator
@@ -32,14 +31,6 @@ class SubscriptionViewHolder : ViewHolder {
private val _platformIndicator : PlatformIndicator;
private val _textMeta: TextView;
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
StateApp.instance.scopeGetter,
{ PolycentricCache.instance.getProfileAsync(it) })
.success { it -> onProfileLoaded(null, it, true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load profile.", it);
};
var subscription: Subscription? = null
private set;
@@ -74,45 +65,12 @@ class SubscriptionViewHolder : ViewHolder {
}
fun bind(sub: Subscription) {
_taskLoadProfile.cancel();
this.subscription = sub;
_creatorThumbnail.setThumbnail(sub.channel.thumbnail, false);
_textName.text = sub.channel.name;
bindViewMetrics(sub);
_platformIndicator.setPlatformFromClientID(sub.channel.id.pluginId);
val cachedProfile = PolycentricCache.instance.getCachedProfile(sub.channel.url, true);
if (cachedProfile != null) {
onProfileLoaded(sub, cachedProfile, false);
if (cachedProfile.expired) {
_taskLoadProfile.run(sub.channel.id);
}
} else {
_taskLoadProfile.run(sub.channel.id);
}
}
private fun onProfileLoaded(sub: Subscription?, cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
val dp_46 = 46.dp(itemView.context.resources);
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_46 * dp_46)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(this.subscription?.channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
}
if (profile != null) {
_textName.text = profile.systemState.username;
}
if(sub != null)
bindViewMetrics(sub)
}
fun bindViewMetrics(sub: Subscription?) {
@@ -30,7 +30,6 @@ import com.futo.platformplayer.dp
import com.futo.platformplayer.fixHtmlWhitespace
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.toHumanNowDiffString
import com.futo.platformplayer.views.FeedStyle
@@ -44,7 +43,6 @@ class PreviewPostView : LinearLayout {
private val _imageAuthorThumbnail: ImageView;
private val _textAuthorName: TextView;
private val _imageNeopassChannel: ImageView;
private val _textMetadata: TextView;
private val _textTitle: TextView;
private val _textDescription: TextView;
@@ -64,15 +62,6 @@ class PreviewPostView : LinearLayout {
private val _layoutComments: LinearLayout?;
private val _textComments: TextView?;
private var _neopassAnimator: ObjectAnimator? = null;
private val _taskLoadValidClaims = TaskHandler<PlatformID, PolycentricCache.CachedOwnedClaims>(StateApp.instance.scopeGetter,
{ PolycentricCache.instance.getValidClaimsAsync(it).await() })
.success { it -> updateClaimsLayout(it, animate = true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load claims.", it);
};
val content: IPlatformContent? get() = _content;
val onContentClicked = Event1<IPlatformContent>();
@@ -83,7 +72,6 @@ class PreviewPostView : LinearLayout {
_imageAuthorThumbnail = findViewById(R.id.image_author_thumbnail);
_textAuthorName = findViewById(R.id.text_author_name);
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
_textMetadata = findViewById(R.id.text_metadata);
_textTitle = findViewById(R.id.text_title);
_textDescription = findViewById(R.id.text_description);
@@ -130,21 +118,8 @@ class PreviewPostView : LinearLayout {
}
fun bind(content: IPlatformContent) {
_taskLoadValidClaims.cancel();
_content = content;
if (content.author.id.claimType > 0) {
val cachedClaims = PolycentricCache.instance.getCachedValidClaims(content.author.id);
if (cachedClaims != null) {
updateClaimsLayout(cachedClaims, animate = false);
} else {
updateClaimsLayout(null, animate = false);
_taskLoadValidClaims.run(content.author.id);
}
} else {
updateClaimsLayout(null, animate = false);
}
_textAuthorName.text = content.author.name;
_textMetadata.text = content.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: "";
@@ -292,25 +267,6 @@ class PreviewPostView : LinearLayout {
};
}
private fun updateClaimsLayout(claims: PolycentricCache.CachedOwnedClaims?, animate: Boolean) {
_neopassAnimator?.cancel();
_neopassAnimator = null;
val harborAvailable = claims != null && !claims.ownedClaims.isNullOrEmpty();
if (harborAvailable) {
_imageNeopassChannel.visibility = View.VISIBLE
if (animate) {
_neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500)
_neopassAnimator?.start()
}
} else {
_imageNeopassChannel.visibility = View.GONE
}
//TODO: Necessary if we decide to use creator thumbnail with neopass indicator instead
//_creatorThumbnail?.setHarborAvailable(harborAvailable, animate)
}
companion object {
val TAG = "PreviewPostView";
}
@@ -24,7 +24,6 @@ import com.futo.platformplayer.getNowDiffSeconds
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.images.GlideHelper.Companion.loadThumbnails
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads
@@ -47,7 +46,6 @@ open class PreviewVideoView : LinearLayout {
protected val _imageVideo: ImageView
protected val _imageChannel: ImageView?
protected val _creatorThumbnail: CreatorThumbnail?
protected val _imageNeopassChannel: ImageView?;
protected val _platformIndicator: PlatformIndicator;
protected val _textVideoName: TextView
protected val _textChannelName: TextView
@@ -57,7 +55,6 @@ open class PreviewVideoView : LinearLayout {
protected var _playerVideoThumbnail: FutoThumbnailPlayer? = null;
protected val _containerLive: LinearLayout;
protected val _playerContainer: FrameLayout;
protected var _neopassAnimator: ObjectAnimator? = null;
protected val _layoutDownloaded: FrameLayout;
protected val _button_add_to_queue : View;
@@ -65,15 +62,6 @@ open class PreviewVideoView : LinearLayout {
protected val _button_add_to : View;
protected val _exoPlayer: PlayerManager?;
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
StateApp.instance.scopeGetter,
{ PolycentricCache.instance.getProfileAsync(it) })
.success { it -> onProfileLoaded(it, true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load profile.", it);
};
private val _timeBar: ProgressBar?;
val onVideoClicked = Event2<IPlatformVideo, Long>();
@@ -108,7 +96,6 @@ open class PreviewVideoView : LinearLayout {
_button_add_to_queue = findViewById(R.id.button_add_to_queue);
_button_add_to_watch_later = findViewById(R.id.button_add_to_watch_later);
_button_add_to = findViewById(R.id.button_add_to);
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
_layoutDownloaded = findViewById(R.id.layout_downloaded);
_timeBar = findViewById(R.id.time_bar)
@@ -160,15 +147,12 @@ open class PreviewVideoView : LinearLayout {
open fun bind(content: IPlatformContent) {
_taskLoadProfile.cancel();
isClickable = true;
val isPlanned = (content.datetime?.getNowDiffSeconds() ?: 0) < 0;
stopPreview();
_imageNeopassChannel?.visibility = View.GONE;
_creatorThumbnail?.setThumbnail(content.author.thumbnail, false);
val thumbnail = content.author.thumbnail
@@ -186,16 +170,6 @@ open class PreviewVideoView : LinearLayout {
_textChannelName.text = content.author.name
val cachedProfile = PolycentricCache.instance.getCachedProfile(content.author.url, true);
if (cachedProfile != null) {
onProfileLoaded(cachedProfile, false);
if (cachedProfile.expired) {
_taskLoadProfile.run(content.author.id);
}
} else {
_taskLoadProfile.run(content.author.id);
}
_imageChannel?.clipToOutline = true;
_textVideoName.text = content.name;
@@ -335,52 +309,6 @@ open class PreviewVideoView : LinearLayout {
_playerVideoThumbnail?.setMuteChangedListener(callback);
}
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
_neopassAnimator?.cancel();
_neopassAnimator = null;
val profile = cachedPolycentricProfile?.profile;
if (_creatorThumbnail != null) {
val dp_32 = 32.dp(context.resources);
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_32 * dp_32)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(content?.author?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
}
} else if (_imageChannel != null) {
val dp_28 = 28.dp(context.resources);
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_28 * dp_28)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_imageChannel.let {
Glide.with(_imageChannel)
.load(avatar)
.placeholder(R.drawable.placeholder_channel_thumbnail)
.into(_imageChannel);
}
_imageNeopassChannel?.visibility = View.VISIBLE
if (animate) {
_neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500)
_neopassAnimator?.start()
} else {
_imageNeopassChannel?.alpha = 1.0f;
}
} else {
_imageNeopassChannel?.visibility = View.GONE
}
}
if (profile != null) {
_textChannelName.text = profile.systemState.username
}
}
companion object {
private val TAG = "VideoPreviewViewHolder"
}
@@ -11,7 +11,6 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.adapters.AnyAdapter
@@ -27,14 +26,6 @@ class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyVi
val onClick = Event1<IPlatformChannel>();
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
StateApp.instance.scopeGetter,
{ PolycentricCache.instance.getProfileAsync(it) })
.success { onProfileLoaded(it, true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load profile.", it);
};
init {
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
_name = _view.findViewById(R.id.text_channel_name);
@@ -45,40 +36,10 @@ class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyVi
}
override fun bind(value: IPlatformChannel) {
_taskLoadProfile.cancel();
_channel = value;
_creatorThumbnail.setThumbnail(value.thumbnail, false);
_name.text = value.name;
val cachedProfile = PolycentricCache.instance.getCachedProfile(value.url, true);
if (cachedProfile != null) {
onProfileLoaded(cachedProfile, false);
if (cachedProfile.expired) {
_taskLoadProfile.run(value.id);
}
} else {
_taskLoadProfile.run(value.id);
}
}
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
val dp_55 = 55.dp(itemView.context.resources)
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
}
if (profile != null) {
_name.text = profile.systemState.username;
}
}
companion object {
@@ -94,14 +55,6 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
val onClick = Event1<Selectable>();
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
StateApp.instance.scopeGetter,
{ PolycentricCache.instance.getProfileAsync(it) })
.success { onProfileLoaded(it, true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load profile.", it);
};
init {
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
_name = _view.findViewById(R.id.text_channel_name);
@@ -112,8 +65,6 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
}
override fun bind(value: Selectable) {
_taskLoadProfile.cancel();
_channel = value;
if(value.active)
@@ -123,34 +74,6 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda
_creatorThumbnail.setThumbnail(value.channel.thumbnail, false);
_name.text = value.channel.name;
val cachedProfile = PolycentricCache.instance.getCachedProfile(value.channel.url, true);
if (cachedProfile != null) {
onProfileLoaded(cachedProfile, false);
if (cachedProfile.expired) {
_taskLoadProfile.run(value.channel.id);
}
} else {
_taskLoadProfile.run(value.channel.id);
}
}
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
val dp_55 = 55.dp(itemView.context.resources)
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(_channel?.channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
}
if (profile != null) {
_name.text = profile.systemState.username;
}
}
companion object {
@@ -12,7 +12,6 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.toHumanNumber
@@ -34,14 +33,6 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
val onClick = Event1<PlatformAuthorLink>();
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
StateApp.instance.scopeGetter,
{ PolycentricCache.instance.getProfileAsync(it) })
.success { it -> onProfileLoaded(it, true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load profile.", it);
};
init {
_textName = _view.findViewById(R.id.text_channel_name);
_creatorThumbnail = _view.findViewById(R.id.creator_thumbnail);
@@ -61,21 +52,9 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
}
override fun bind(value: PlatformAuthorLink) {
_taskLoadProfile.cancel();
_creatorThumbnail.setThumbnail(value.thumbnail, false);
_textName.text = value.name;
val cachedProfile = PolycentricCache.instance.getCachedProfile(value.url, true);
if (cachedProfile != null) {
onProfileLoaded(cachedProfile, false);
if (cachedProfile.expired) {
_taskLoadProfile.run(value.id);
}
} else {
_taskLoadProfile.run(value.id);
}
if(value.subscribers == null || (value.subscribers ?: 0) <= 0L)
_textMetadata.visibility = View.GONE;
else {
@@ -87,25 +66,6 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
_authorLink = value;
}
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
val dp_61 = 61.dp(itemView.context.resources);
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_61 * dp_61)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(_authorLink?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
}
if (profile != null) {
_textName.text = profile.systemState.username;
}
}
companion object {
private const val TAG = "CreatorViewHolder";
}
@@ -12,7 +12,6 @@ import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.adapters.AnyAdapter
@@ -27,14 +26,6 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
private var _subscription: Subscription? = null;
private var _channel: SerializedChannel? = null;
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
StateApp.instance.scopeGetter,
{ PolycentricCache.instance.getProfileAsync(it) })
.success { onProfileLoaded(it, true) }
.exception<Throwable> {
Logger.w(TAG, "Failed to load profile.", it);
};
val onClick = Event1<Subscription>();
init {
@@ -47,44 +38,14 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
}
override fun bind(value: Subscription) {
_taskLoadProfile.cancel();
_channel = value.channel;
_creatorThumbnail.setThumbnail(value.channel.thumbnail, false);
_name.text = value.channel.name;
val cachedProfile = PolycentricCache.instance.getCachedProfile(value.channel.url, true);
if (cachedProfile != null) {
onProfileLoaded(cachedProfile, false);
if (cachedProfile.expired) {
_taskLoadProfile.run(value.channel.id);
}
} else {
_taskLoadProfile.run(value.channel.id);
}
_subscription = value;
}
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
val dp_55 = 55.dp(itemView.context.resources)
val profile = cachedPolycentricProfile?.profile;
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_55 * dp_55)
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
if (avatar != null) {
_creatorThumbnail.setThumbnail(avatar, animate);
} else {
_creatorThumbnail.setThumbnail(_channel?.thumbnail, animate);
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
}
if (profile != null) {
_name.text = profile.systemState.username;
}
}
companion object {
private const val TAG = "SubscriptionBarViewHolder";
}
@@ -19,7 +19,6 @@ import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.models.SubscriptionGroup
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.selectBestImage
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.adapters.AnyAdapter
@@ -11,8 +11,8 @@ import androidx.constraintlayout.widget.ConstraintLayout
import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.getDataLinkFromUrl
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.views.IdenticonView
import userpackage.Protocol
@@ -68,7 +68,7 @@ class CreatorThumbnail : ConstraintLayout {
if (url.startsWith("polycentric://")) {
try {
val dataLink = PolycentricCache.getDataLinkFromUrl(url)
val dataLink = url.getDataLinkFromUrl()
setHarborAvailable(true, animate, dataLink?.system);
} catch (e: Throwable) {
setHarborAvailable(false, animate, null);
@@ -5,8 +5,8 @@ import android.util.AttributeSet
import android.widget.LinearLayout
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.views.SupportView
import com.futo.polycentric.core.PolycentricProfile
class SupportOverlay : LinearLayout {
val onClose = Event0();
@@ -6,9 +6,7 @@ import android.webkit.WebView
import android.widget.LinearLayout
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.views.SupportView
class WebviewOverlay : LinearLayout {
val onClose = Event0();
@@ -22,12 +22,12 @@ import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.adapters.CommentViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.net.UnknownHostException
@@ -96,6 +96,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
val exoPlayerStateName: String;
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 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");
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}");
if(error.cause is HttpDataSource.InvalidResponseCodeException) {
val cause = error.cause as HttpDataSource.InvalidResponseCodeException
@@ -254,7 +254,7 @@
<TextView
android:id="@+id/text_channel_name"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
@@ -268,23 +268,10 @@
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
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"
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
android:id="@+id/text_video_metadata"
android:layout_width="0dp"
+2 -15
View File
@@ -104,7 +104,7 @@
<TextView
android:id="@+id/text_channel_name"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:maxLines="1"
@@ -117,21 +117,8 @@
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
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_constraintTop_toTopOf="@id/text_channel_name"
app:layout_constraintBottom_toBottomOf="@id/text_channel_name"
android:layout_marginStart="4dp"
android:visibility="gone"/>
app:layout_constraintTop_toBottomOf="@id/text_playlist_name" />
<TextView
android:id="@+id/text_playlist_items"
+2 -16
View File
@@ -37,7 +37,7 @@
<TextView
android:id="@+id/text_author_name"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="1"
@@ -51,24 +51,10 @@
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
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"
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
android:id="@+id/text_metadata"
android:layout_width="0dp"
@@ -36,9 +36,10 @@
<TextView
android:id="@+id/text_author_name"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:ellipsize="end"
android:fontFamily="@font/inter_extra_light"
@@ -50,24 +51,10 @@
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
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"
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
android:id="@+id/text_metadata"
android:layout_width="0dp"
@@ -232,7 +232,7 @@
<TextView
android:id="@+id/text_channel_name"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
@@ -246,23 +246,10 @@
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
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"
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
android:id="@+id/text_video_metadata"
android:layout_width="0dp"
@@ -270,7 +270,7 @@
<TextView
android:id="@+id/text_channel_name"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
@@ -284,23 +284,10 @@
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
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"
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
android:id="@+id/text_video_metadata"
android:layout_width="0dp"
@@ -317,8 +304,6 @@
app:layout_constraintRight_toRightOf="parent"
android:layout_marginStart="4dp"/>
<com.futo.platformplayer.views.platform.PlatformIndicator
android:id="@+id/thumbnail_platform_nested"
android:layout_width="20dp"
@@ -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>
+1 -1
View File
@@ -418,7 +418,7 @@
<string name="log_level">Log Level</string>
<string name="logging">Logging</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_your_polycentric_identity">Manage your Polycentric identity</string>
<string name="manual_check">Manual check</string>
+2 -2
View File
@@ -8,7 +8,7 @@ The goal of the authentication system is to provide plugins the ability to make
>
>You should always only login (and install for that matter) plugins you trust.
How to actually use the authenticated client is described in the Http package documentation (See [Package: Http](_blank)).
How to actually use the authenticated client is described in the Http package documentation (See [Package: Http](docs/packages/packageHttp.md)).
This documentation will exclusively focus on configuring authentication and how it behaves.
## How it works
@@ -58,5 +58,5 @@ Headers are exclusively applied to the domains they are retrieved from. A plugin
By default, when authentication requests are made, the authenticated client will behave similar to that of a normal browser. Meaning that if the server you are communicating with sets new cookies, the client will use those cookies instead. These new cookies are NOT saved to disk, meaning that whenever that plugin reloads the cookies will revert to those assigned at login.
This behavior can be modified by using custom http clients as described in the http package documentation.
(See [Package: Http](_blank))
(See [Package: Http](docs/packages/packageHttp.md))