mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-09-25 18:59:04 +00:00
Merge branch 'master' into 'small-hls-fixes'
# Conflicts: # app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
This commit is contained in:
commit
bdae35b1a8
128 changed files with 826 additions and 1266 deletions
2
.gitmodules
vendored
2
.gitmodules
vendored
|
@ -83,7 +83,7 @@
|
|||
path = app/src/stable/assets/sources/dailymotion
|
||||
url = ../plugins/dailymotion.git
|
||||
[submodule "app/src/stable/assets/sources/apple-podcast"]
|
||||
path = app/src/stable/assets/sources/apple-podcast
|
||||
path = app/src/stable/assets/sources/apple-podcasts
|
||||
url = ../plugins/apple-podcasts.git
|
||||
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
|
||||
path = app/src/unstable/assets/sources/apple-podcasts
|
||||
|
|
|
@ -197,7 +197,7 @@ dependencies {
|
|||
implementation 'org.jsoup:jsoup:1.15.3'
|
||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
|
||||
implementation 'com.arthenica:ffmpeg-kit-full:6.0-2.LTS'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||
implementation 'com.google.zxing:core:3.4.1'
|
||||
|
|
|
@ -156,7 +156,6 @@
|
|||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.DeveloperActivity"
|
||||
|
|
|
@ -263,6 +263,10 @@ class PlatformVideoDetails extends PlatformVideo {
|
|||
this.rating = obj.rating ?? null; //IRating
|
||||
this.subtitles = obj.subtitles ?? [];
|
||||
this.isShort = !!obj.isShort ?? false;
|
||||
|
||||
if (obj.getContentRecommendations) {
|
||||
this.getContentRecommendations = obj.getContentRecommendations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package com.futo.platformplayer
|
||||
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.AnnouncementType
|
||||
import com.futo.platformplayer.states.StateAnnouncement
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.polycentric.core.ProcessHandle
|
||||
import com.futo.polycentric.core.Store
|
||||
import com.futo.polycentric.core.SystemState
|
||||
import com.futo.polycentric.core.base64UrlToByteArray
|
||||
import userpackage.Protocol
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
@ -40,33 +40,25 @@ fun Protocol.ImageBundle?.selectHighestResolutionImage(): Protocol.ImageManifest
|
|||
return imageManifestsList.filter { it.byteCount < maximumFileSize }.maxByOrNull { abs(it.width * it.height) }
|
||||
}
|
||||
|
||||
fun String.getDataLinkFromUrl(): Protocol.URLInfoDataLink? {
|
||||
val urlData = if (this.startsWith("polycentric://")) {
|
||||
this.substring("polycentric://".length)
|
||||
} else this;
|
||||
|
||||
val urlBytes = urlData.base64UrlToByteArray();
|
||||
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
|
||||
if (urlInfo.urlType != 4L) {
|
||||
return null
|
||||
}
|
||||
|
||||
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
||||
return dataLink
|
||||
}
|
||||
|
||||
fun Protocol.Claim.resolveChannelUrl(): String? {
|
||||
return StatePlatform.instance.resolveChannelUrlByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||
}
|
||||
|
||||
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
||||
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
||||
}
|
||||
|
||||
suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() {
|
||||
val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
|
||||
if (!systemState.servers.contains(PolycentricCache.SERVER)) {
|
||||
Logger.w("Backfill", "Polycentric prod server not added, adding it.")
|
||||
addServer(PolycentricCache.SERVER)
|
||||
}
|
||||
|
||||
val exceptions = fullyBackfillServers()
|
||||
for (pair in exceptions) {
|
||||
val server = pair.key
|
||||
val exception = pair.value
|
||||
|
||||
StateAnnouncement.instance.registerAnnouncement(
|
||||
"backfill-failed",
|
||||
"Backfill failed",
|
||||
"Failed to backfill server $server. $exception",
|
||||
AnnouncementType.SESSION_RECURRING
|
||||
);
|
||||
|
||||
Logger.e("Backfill", "Failed to backfill server $server.", exception)
|
||||
}
|
||||
}
|
|
@ -356,7 +356,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||
var playback = PlaybackSettings();
|
||||
@Serializable
|
||||
class PlaybackSettings {
|
||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1)
|
||||
@FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -2)
|
||||
@DropdownFieldOptionsId(R.array.audio_languages)
|
||||
var primaryLanguage: Int = 0;
|
||||
|
||||
|
@ -380,6 +380,8 @@ class Settings : FragmentedStorageFileJson() {
|
|||
else -> null
|
||||
}
|
||||
}
|
||||
@FormField(R.string.prefer_original_audio, FieldForm.TOGGLE, R.string.prefer_original_audio_description, -1)
|
||||
var preferOriginalAudio: Boolean = true;
|
||||
|
||||
//= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
|
||||
|
||||
|
@ -644,6 +646,9 @@ class Settings : FragmentedStorageFileJson() {
|
|||
@Serializable
|
||||
class Plugins {
|
||||
|
||||
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
|
||||
var checkDisabledPluginsForUpdates: Boolean = false;
|
||||
|
||||
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
|
||||
var clearCookiesOnLogout: Boolean = true;
|
||||
|
||||
|
|
|
@ -79,6 +79,36 @@ class UISlideOverlays {
|
|||
return menu;
|
||||
}
|
||||
|
||||
fun showQueueOptionsOverlay(context: Context, container: ViewGroup) {
|
||||
UISlideOverlays.showOverlay(container, "Queue options", null, {
|
||||
|
||||
}, SlideUpMenuItem(context, R.drawable.ic_playlist, "Save as playlist", "", "Creates a new playlist with queue as videos", null, {
|
||||
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
|
||||
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
|
||||
|
||||
addPlaylistOverlay.onOK.subscribe {
|
||||
val text = nameInput.text.trim()
|
||||
if (text.isBlank()) {
|
||||
return@subscribe;
|
||||
}
|
||||
|
||||
addPlaylistOverlay.hide();
|
||||
nameInput.deactivate();
|
||||
nameInput.clear();
|
||||
StatePlayer.instance.saveQueueAsPlaylist(text);
|
||||
UIDialogs.appToast("Playlist [${text}] created");
|
||||
};
|
||||
|
||||
addPlaylistOverlay.onCancel.subscribe {
|
||||
nameInput.deactivate();
|
||||
nameInput.clear();
|
||||
};
|
||||
|
||||
addPlaylistOverlay.show();
|
||||
nameInput.activate();
|
||||
}, false));
|
||||
}
|
||||
|
||||
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
||||
val items = arrayListOf<View>();
|
||||
|
||||
|
@ -337,7 +367,9 @@ class UISlideOverlays {
|
|||
call = {
|
||||
selectedVideoVariant = it
|
||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
if (audioButtons.isEmpty()){
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
}
|
||||
},
|
||||
invokeParent = false
|
||||
))
|
||||
|
@ -372,7 +404,7 @@ class UISlideOverlays {
|
|||
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
||||
slideUpMenuOverlay.hide()
|
||||
} else if (source is IHLSManifestAudioSource) {
|
||||
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, resolvedPlaylistUrl), null)
|
||||
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, resolvedPlaylistUrl), null)
|
||||
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
||||
slideUpMenuOverlay.hide()
|
||||
} else {
|
||||
|
@ -419,7 +451,7 @@ class UISlideOverlays {
|
|||
}
|
||||
|
||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
||||
listOf(listOf(SlideUpMenuItem(
|
||||
listOf((if (audioSources != null) listOf(SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
container.context.getString(R.string.none),
|
||||
|
@ -432,7 +464,7 @@ class UISlideOverlays {
|
|||
menu?.setOk(container.context.getString(R.string.download));
|
||||
},
|
||||
invokeParent = false
|
||||
)) +
|
||||
)) else listOf()) +
|
||||
videoSources
|
||||
.filter { it.isDownloadable() }
|
||||
.map {
|
||||
|
@ -909,7 +941,7 @@ class UISlideOverlays {
|
|||
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||
(listOf(
|
||||
if(!isLimited)
|
||||
if(!isLimited && !video.isLive)
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_download,
|
||||
|
@ -1045,8 +1077,9 @@ class UISlideOverlays {
|
|||
StatePlayer.TYPE_WATCHLATER,
|
||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||
tag = "watch later",
|
||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true);
|
||||
UIDialogs.appToast("Added to watch later", false);
|
||||
call = {
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
|
||||
UIDialogs.appToast("Added to watch later", false);
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
|
|
@ -113,7 +113,7 @@ class LoginActivity : AppCompatActivity() {
|
|||
|
||||
companion object {
|
||||
private val TAG = "LoginActivity";
|
||||
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#_ ]*");
|
||||
private val REGEX_LOGIN_BUTTON = Regex("[a-zA-Z\\-\\.#:_ ]*");
|
||||
|
||||
private var _callback: ((SourceAuth?) -> Unit)? = null;
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
|
|||
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
|
||||
|
||||
current += bytesToSend.toLong()
|
||||
if (current >= end) {
|
||||
if (current > end) {
|
||||
Logger.i(TAG, "Expected amount of bytes sent")
|
||||
break
|
||||
}
|
||||
|
|
|
@ -13,7 +13,8 @@ class AudioUrlSource(
|
|||
override val codec: String = "",
|
||||
override val language: String = Language.UNKNOWN,
|
||||
override val duration: Long? = null,
|
||||
override var priority: Boolean = false
|
||||
override var priority: Boolean = false,
|
||||
override var original: Boolean = false
|
||||
) : IAudioUrlSource, IStreamMetaDataSource{
|
||||
override var streamMetaData: StreamMetaData? = null;
|
||||
|
||||
|
@ -36,7 +37,9 @@ class AudioUrlSource(
|
|||
source.container,
|
||||
source.codec,
|
||||
source.language,
|
||||
source.duration
|
||||
source.duration,
|
||||
source.priority,
|
||||
source.original
|
||||
);
|
||||
ret.streamMetaData = streamData;
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ class HLSVariantAudioUrlSource(
|
|||
override val language: String,
|
||||
override val duration: Long?,
|
||||
override val priority: Boolean,
|
||||
override val original: Boolean,
|
||||
val url: String
|
||||
) : IAudioUrlSource {
|
||||
override fun getAudioUrl(): String {
|
||||
|
|
|
@ -8,4 +8,5 @@ interface IAudioSource {
|
|||
val language : String;
|
||||
val duration : Long?;
|
||||
val priority: Boolean;
|
||||
val original: Boolean;
|
||||
}
|
|
@ -15,6 +15,7 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
|||
override val duration: Long? = null;
|
||||
|
||||
override var priority: Boolean = false;
|
||||
override val original: Boolean = false;
|
||||
|
||||
val filePath : String;
|
||||
val fileSize: Long;
|
||||
|
@ -33,13 +34,13 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
|||
}
|
||||
|
||||
companion object {
|
||||
fun fromSource(source: IAudioSource, path: String, fileSize: Long): LocalAudioSource {
|
||||
fun fromSource(source: IAudioSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalAudioSource {
|
||||
return LocalAudioSource(
|
||||
source.name,
|
||||
path,
|
||||
fileSize,
|
||||
source.bitrate,
|
||||
source.container,
|
||||
overrideContainer ?: source.container,
|
||||
source.codec,
|
||||
source.language
|
||||
);
|
||||
|
|
|
@ -35,7 +35,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
|||
}
|
||||
|
||||
companion object {
|
||||
fun fromSource(source: IVideoSource, path: String, fileSize: Long): LocalVideoSource {
|
||||
fun fromSource(source: IVideoSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalVideoSource {
|
||||
return LocalVideoSource(
|
||||
source.name,
|
||||
path,
|
||||
|
@ -43,7 +43,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
|||
source.width,
|
||||
source.height,
|
||||
source.duration,
|
||||
source.container,
|
||||
overrideContainer ?: source.container,
|
||||
source.codec,
|
||||
source.bitrate?:0
|
||||
);
|
||||
|
|
|
@ -21,6 +21,8 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
|
|||
|
||||
override var priority: Boolean = false;
|
||||
|
||||
override var original: Boolean = false;
|
||||
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_AUDIOURL, plugin, obj) {
|
||||
val contextName = "AudioUrlSource";
|
||||
val config = plugin.config;
|
||||
|
@ -35,6 +37,7 @@ open class JSAudioUrlSource : IAudioUrlSource, JSSource {
|
|||
name = _obj.getOrDefault(config, "name", contextName, "${container} ${bitrate}") ?: "${container} ${bitrate}";
|
||||
|
||||
priority = if(_obj.has("priority")) obj.getOrThrow(config, "priority", contextName) else false;
|
||||
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
||||
}
|
||||
|
||||
override fun getAudioUrl() : String {
|
||||
|
|
|
@ -4,6 +4,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
|||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
|
@ -14,13 +16,14 @@ import com.futo.platformplayer.getOrThrow
|
|||
import com.futo.platformplayer.others.Language
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource {
|
||||
override val container : String = "application/dash+xml";
|
||||
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||
override val container : String;
|
||||
override val name : String;
|
||||
override val codec: String;
|
||||
override val bitrate: Int;
|
||||
override val duration: Long;
|
||||
override val priority: Boolean;
|
||||
override var original: Boolean = false;
|
||||
|
||||
override val language: String;
|
||||
|
||||
|
@ -29,17 +32,21 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
|||
|
||||
override val hasGenerate: Boolean;
|
||||
|
||||
override var streamMetaData: StreamMetaData? = null;
|
||||
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||
val contextName = "DashRawSource";
|
||||
val config = plugin.config;
|
||||
name = _obj.getOrThrow(config, "name", contextName);
|
||||
url = _obj.getOrThrow(config, "url", contextName);
|
||||
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
|
||||
manifest = _obj.getOrThrow(config, "manifest", contextName);
|
||||
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
||||
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
||||
duration = _obj.getOrDefault(config, "duration", contextName, 0) ?: 0;
|
||||
priority = _obj.getOrDefault(config, "priority", contextName, false) ?: false;
|
||||
language = _obj.getOrDefault(config, "language", contextName, Language.UNKNOWN) ?: Language.UNKNOWN;
|
||||
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
||||
hasGenerate = _obj.has("generate");
|
||||
}
|
||||
|
||||
|
@ -50,15 +57,28 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
|||
throw IllegalStateException("Source object already closed");
|
||||
|
||||
val plugin = _plugin.getUnderlyingPlugin();
|
||||
|
||||
var result: String? = null;
|
||||
if(_plugin is DevJSClient)
|
||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
||||
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||
_obj.invokeString("generate");
|
||||
}
|
||||
}
|
||||
else
|
||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||
_obj.invokeString("generate");
|
||||
}
|
||||
|
||||
if(result != null){
|
||||
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
|
||||
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
|
||||
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
|
||||
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -6,6 +6,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
|||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
|
@ -20,8 +22,8 @@ interface IJSDashManifestRawSource {
|
|||
var manifest: String?;
|
||||
fun generate(): String?;
|
||||
}
|
||||
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource {
|
||||
override val container : String = "application/dash+xml";
|
||||
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||
override val container : String;
|
||||
override val name : String;
|
||||
override val width: Int;
|
||||
override val height: Int;
|
||||
|
@ -36,11 +38,14 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
|||
override val hasGenerate: Boolean;
|
||||
val canMerge: Boolean;
|
||||
|
||||
override var streamMetaData: StreamMetaData? = null;
|
||||
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||
val contextName = "DashRawSource";
|
||||
val config = plugin.config;
|
||||
name = _obj.getOrThrow(config, "name", contextName);
|
||||
url = _obj.getOrThrow(config, "url", contextName);
|
||||
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
|
||||
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
|
||||
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
|
||||
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
|
||||
|
@ -57,17 +62,30 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
|||
return manifest;
|
||||
if(_obj.isClosed)
|
||||
throw IllegalStateException("Source object already closed");
|
||||
|
||||
var result: String? = null;
|
||||
if(_plugin is DevJSClient) {
|
||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
||||
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||
_obj.invokeString("generate");
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||
_obj.invokeString("generate");
|
||||
});
|
||||
|
||||
if(result != null){
|
||||
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
|
||||
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
|
||||
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
|
||||
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,12 +118,16 @@ class JSDashManifestMergingRawSource(
|
|||
if(videoDash == null) return null;
|
||||
|
||||
//TODO: Temporary simple solution..make more reliable version
|
||||
|
||||
var result: String? = null;
|
||||
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
|
||||
if(audioAdaptationSet != null) {
|
||||
return videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
||||
result = videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
||||
}
|
||||
else
|
||||
return videoDash;
|
||||
result = videoDash;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -21,6 +21,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
|||
override val language: String;
|
||||
|
||||
override var priority: Boolean = false;
|
||||
override var original: Boolean = false;
|
||||
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_HLS, plugin, obj) {
|
||||
val contextName = "HLSAudioSource";
|
||||
|
@ -32,6 +33,7 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
|||
language = _obj.getOrThrow(config, "language", contextName);
|
||||
|
||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||
original = if(_obj.has("original")) obj.getOrThrow(config, "original", contextName) else false;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ class StateCasting {
|
|||
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
||||
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
||||
|
||||
private val _castServer = ManagedHttpServer(9999);
|
||||
private val _castServer = ManagedHttpServer();
|
||||
private var _started = false;
|
||||
|
||||
var devices: HashMap<String, CastingDevice> = hashMapOf();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -141,11 +141,17 @@ class VideoDownload {
|
|||
var error: String? = null;
|
||||
|
||||
var videoFilePath: String? = null;
|
||||
var videoFileName: String? = null;
|
||||
var videoFileNameBase: String? = null;
|
||||
var videoFileNameExt: String? = null;
|
||||
val videoFileName: String? get() = if(videoFileNameBase.isNullOrEmpty()) null else videoFileNameBase + (if(!videoFileNameExt.isNullOrEmpty()) "." + videoFileNameExt else "");
|
||||
var videoOverrideContainer: String? = null;
|
||||
var videoFileSize: Long? = null;
|
||||
|
||||
var audioFilePath: String? = null;
|
||||
var audioFileName: String? = null;
|
||||
var audioFileNameBase: String? = null;
|
||||
var audioFileNameExt: String? = null;
|
||||
val audioFileName: String? get() = if(audioFileNameBase.isNullOrEmpty()) null else audioFileNameBase + (if(!audioFileNameExt.isNullOrEmpty()) "." + audioFileNameExt else "");
|
||||
var audioOverrideContainer: String? = null;
|
||||
var audioFileSize: Long? = null;
|
||||
|
||||
var subtitleFilePath: String? = null;
|
||||
|
@ -235,11 +241,13 @@ class VideoDownload {
|
|||
videoDetails = null;
|
||||
videoSource = null;
|
||||
videoSourceLive = null;
|
||||
videoOverrideContainer = null;
|
||||
}
|
||||
if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
|
||||
videoDetails = null;
|
||||
audioSource = null;
|
||||
videoSourceLive = null;
|
||||
audioOverrideContainer = null;
|
||||
}
|
||||
if(video == null && videoDetails == null)
|
||||
throw IllegalStateException("Missing information for download to complete");
|
||||
|
@ -367,8 +375,8 @@ class VideoDownload {
|
|||
else throw DownloadException("Could not find a valid video or audio source for download")
|
||||
|
||||
if(asource is JSSource) {
|
||||
this.hasVideoRequestExecutor = this.hasVideoRequestExecutor || asource.hasRequestExecutor;
|
||||
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
|
||||
this.hasAudioRequestExecutor = this.hasAudioRequestExecutor || asource.hasRequestExecutor;
|
||||
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
|
||||
}
|
||||
|
||||
if(asource == null) {
|
||||
|
@ -410,11 +418,13 @@ class VideoDownload {
|
|||
else audioSource;
|
||||
|
||||
if(actualVideoSource != null) {
|
||||
videoFileName = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}].${videoContainerToExtension(actualVideoSource!!.container)}".sanitizeFileName();
|
||||
videoFileNameBase = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}]".sanitizeFileName();
|
||||
videoFileNameExt = videoContainerToExtension(actualVideoSource!!.container);
|
||||
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
||||
}
|
||||
if(actualAudioSource != null) {
|
||||
audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(actualAudioSource!!.container)}".sanitizeFileName();
|
||||
audioFileNameBase = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}]".sanitizeFileName();
|
||||
audioFileNameExt = audioContainerToExtension(actualAudioSource!!.container);
|
||||
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
||||
}
|
||||
if(subtitleSource != null) {
|
||||
|
@ -1058,8 +1068,8 @@ class VideoDownload {
|
|||
fun complete() {
|
||||
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
||||
val existing = StateDownloads.instance.getCachedVideo(id);
|
||||
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0) };
|
||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0) };
|
||||
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0, videoOverrideContainer) };
|
||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0, audioOverrideContainer) };
|
||||
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
||||
|
||||
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
||||
|
@ -1140,7 +1150,7 @@ class VideoDownload {
|
|||
else if (container.contains("video/x-matroska"))
|
||||
return "mkv";
|
||||
else
|
||||
return "video";
|
||||
return "video";//throw IllegalStateException("Unknown container: " + container)
|
||||
}
|
||||
|
||||
fun audioContainerToExtension(container: String): String {
|
||||
|
@ -1151,11 +1161,11 @@ class VideoDownload {
|
|||
else if (container.contains("audio/mp3"))
|
||||
return "mp3";
|
||||
else if (container.contains("audio/webm"))
|
||||
return "webma";
|
||||
return "webm";
|
||||
else if (container == "application/vnd.apple.mpegurl")
|
||||
return "mp4";
|
||||
return "mp4a";
|
||||
else
|
||||
return "audio";
|
||||
return "audio";// throw IllegalStateException("Unknown container: " + container)
|
||||
}
|
||||
|
||||
fun subtitleContainerToExtension(container: String?): String {
|
||||
|
|
|
@ -39,7 +39,7 @@ class VideoExport {
|
|||
this.subtitleSource = subtitleSource;
|
||||
}
|
||||
|
||||
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope {
|
||||
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null, documentRoot: DocumentFile? = null): DocumentFile = coroutineScope {
|
||||
val v = videoSource;
|
||||
val a = audioSource;
|
||||
val s = subtitleSource;
|
||||
|
@ -50,7 +50,7 @@ class VideoExport {
|
|||
if (s != null) sourceCount++;
|
||||
|
||||
val outputFile: DocumentFile?;
|
||||
val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
||||
val downloadRoot = documentRoot ?: StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
||||
if (sourceCount > 1) {
|
||||
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
||||
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
@ -238,8 +221,8 @@ class ChannelFragment : MainFragment() {
|
|||
}
|
||||
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
||||
if (content is IPlatformVideo) {
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)
|
||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
|
||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||
}
|
||||
}
|
||||
adapter.onUrlClicked.subscribe { url ->
|
||||
|
@ -328,7 +311,7 @@ class ChannelFragment : MainFragment() {
|
|||
_creatorThumbnail.setThumbnail(parameter.thumbnail, true)
|
||||
Glide.with(_imageBanner).clear(_imageBanner)
|
||||
|
||||
loadPolycentricProfile(parameter.id, parameter.url)
|
||||
loadPolycentricProfile(parameter.id)
|
||||
}
|
||||
|
||||
_url = parameter.url
|
||||
|
@ -342,7 +325,7 @@ class ChannelFragment : MainFragment() {
|
|||
_creatorThumbnail.setThumbnail(parameter.channel.thumbnail, true)
|
||||
Glide.with(_imageBanner).clear(_imageBanner)
|
||||
|
||||
loadPolycentricProfile(parameter.channel.id, parameter.channel.url)
|
||||
loadPolycentricProfile(parameter.channel.id)
|
||||
}
|
||||
|
||||
_url = parameter.channel.url
|
||||
|
@ -359,16 +342,8 @@ class ChannelFragment : MainFragment() {
|
|||
_tabs.selectTab(_tabs.getTabAt(selectedTabIndex))
|
||||
}
|
||||
|
||||
private fun loadPolycentricProfile(id: PlatformID, url: String) {
|
||||
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(url, true)
|
||||
if (cachedPolycentricProfile != null) {
|
||||
setPolycentricProfile(cachedPolycentricProfile, animate = true)
|
||||
if (cachedPolycentricProfile.expired) {
|
||||
_taskLoadPolycentricProfile.run(id)
|
||||
}
|
||||
} else {
|
||||
_taskLoadPolycentricProfile.run(id)
|
||||
}
|
||||
private fun loadPolycentricProfile(id: PlatformID) {
|
||||
_taskLoadPolycentricProfile.run(id)
|
||||
}
|
||||
|
||||
private fun setLoading(isLoading: Boolean) {
|
||||
|
@ -533,20 +508,13 @@ class ChannelFragment : MainFragment() {
|
|||
|
||||
private fun setPolycentricProfileOr(url: String, or: () -> Unit) {
|
||||
setPolycentricProfile(null, animate = false)
|
||||
|
||||
val cachedProfile = channel?.let { PolycentricCache.instance.getCachedProfile(url) }
|
||||
if (cachedProfile != null) {
|
||||
setPolycentricProfile(cachedProfile, animate = false)
|
||||
} else {
|
||||
or()
|
||||
}
|
||||
or()
|
||||
}
|
||||
|
||||
private fun setPolycentricProfile(
|
||||
cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean
|
||||
profile: PolycentricProfile?, animate: Boolean
|
||||
) {
|
||||
val dp35 = 35.dp(resources)
|
||||
val profile = cachedPolycentricProfile?.profile
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)?.let {
|
||||
it.toURLInfoSystemLinkUrl(
|
||||
profile.system.toProto(), it.process, profile.systemState.servers.toList()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -82,8 +82,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||
};
|
||||
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
}
|
||||
};
|
||||
adapter.onLongPress.subscribe(this) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import android.widget.EditText
|
|||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -26,6 +27,7 @@ class CreatorsFragment : MainFragment() {
|
|||
private var _overlayContainer: FrameLayout? = null;
|
||||
private var _containerSearch: FrameLayout? = null;
|
||||
private var _editSearch: EditText? = null;
|
||||
private var _textMeta: TextView? = null;
|
||||
private var _buttonClearSearch: ImageButton? = null
|
||||
|
||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
|
@ -34,6 +36,7 @@ class CreatorsFragment : MainFragment() {
|
|||
val editSearch: EditText = view.findViewById(R.id.edit_search);
|
||||
val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search)
|
||||
_editSearch = editSearch
|
||||
_textMeta = view.findViewById(R.id.text_meta);
|
||||
_buttonClearSearch = buttonClearSearch
|
||||
buttonClearSearch.setOnClickListener {
|
||||
editSearch.text.clear()
|
||||
|
@ -41,7 +44,11 @@ class CreatorsFragment : MainFragment() {
|
|||
_buttonClearSearch?.visibility = View.INVISIBLE;
|
||||
}
|
||||
|
||||
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
|
||||
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription)) { subs ->
|
||||
_textMeta?.let {
|
||||
it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}";
|
||||
}
|
||||
};
|
||||
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
|
||||
adapter.onSettings.subscribe { sub -> _overlayContainer?.let { UISlideOverlays.showSubscriptionOptionsOverlay(sub, it) } }
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import com.futo.platformplayer.states.StateDownloads
|
|||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.toHumanBytesSize
|
||||
import com.futo.platformplayer.toHumanDuration
|
||||
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
||||
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
||||
import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder
|
||||
|
@ -215,7 +216,7 @@ class DownloadsFragment : MainFragment() {
|
|||
_listDownloadedHeader.visibility = GONE;
|
||||
} else {
|
||||
_listDownloadedHeader.visibility = VISIBLE;
|
||||
_listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()})";
|
||||
_listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()}${if(downloaded.size > 0) ", ${downloaded.sumOf { it.duration }.toHumanDuration(false)}" else ""})";
|
||||
}
|
||||
|
||||
lastDownloads = downloaded;
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.view.ViewGroup
|
|||
import androidx.core.app.ShareCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
|
@ -78,6 +79,14 @@ class PlaylistFragment : MainFragment() {
|
|||
val nameInput = SlideUpMenuTextInput(context, context.getString(R.string.name));
|
||||
val editPlaylistOverlay = SlideUpMenuOverlay(context, overlayContainer, context.getString(R.string.edit_playlist), context.getString(R.string.ok), false, nameInput);
|
||||
|
||||
_buttonExport.setOnClickListener {
|
||||
_playlist?.let {
|
||||
val context = StateApp.instance.contextOrNull ?: return@let;
|
||||
if(context is IWithResultLauncher)
|
||||
StateDownloads.instance.exportPlaylist(context, it.id);
|
||||
}
|
||||
};
|
||||
|
||||
_buttonDownload.visibility = View.VISIBLE;
|
||||
editPlaylistOverlay.onOK.subscribe {
|
||||
val text = nameInput.text;
|
||||
|
@ -176,6 +185,7 @@ class PlaylistFragment : MainFragment() {
|
|||
setVideos(parameter.videos, true)
|
||||
setMetadata(parameter.videos.size, parameter.videos.sumOf { it.duration })
|
||||
setButtonDownloadVisible(true)
|
||||
setButtonExportVisible(false)
|
||||
setButtonEditVisible(true)
|
||||
|
||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -256,6 +256,8 @@ class SubscriptionGroupFragment : MainFragment() {
|
|||
val sub = StateSubscriptions.instance.getSubscription(sub) ?: StateSubscriptions.instance.getSubscriptionOther(sub);
|
||||
if(sub != null && sub.channel.thumbnail != null) {
|
||||
g.image = ImageVariable.fromUrl(sub.channel.thumbnail!!);
|
||||
if(g.image != null)
|
||||
g.image!!.subscriptionUrl = sub.channel.url;
|
||||
g.image?.setImageView(_imageGroup);
|
||||
g.image?.setImageView(_imageGroupBackground);
|
||||
break;
|
||||
|
|
|
@ -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);
|
||||
|
@ -882,7 +890,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
_slideUpOverlay?.hide();
|
||||
}
|
||||
else null,
|
||||
if(!isLimitedVersion)
|
||||
if(!isLimitedVersion && !(video?.isLive ?: false))
|
||||
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
||||
video?.let {
|
||||
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
||||
|
@ -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),
|
||||
|
@ -2595,8 +2606,13 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
onAddToWatchLaterClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
}
|
||||
}
|
||||
onAddToQueueClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
StatePlayer.instance.addToQueue(it);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -2768,13 +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);
|
||||
|
@ -2783,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?) {
|
||||
|
@ -2976,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);
|
||||
|
|
|
@ -34,6 +34,7 @@ abstract class VideoListEditorView : LinearLayout {
|
|||
protected var overlayContainer: FrameLayout
|
||||
private set;
|
||||
protected var _buttonDownload: ImageButton;
|
||||
protected var _buttonExport: ImageButton;
|
||||
private var _buttonShare: ImageButton;
|
||||
private var _buttonEdit: ImageButton;
|
||||
|
||||
|
@ -54,6 +55,8 @@ abstract class VideoListEditorView : LinearLayout {
|
|||
_buttonEdit = findViewById(R.id.button_edit);
|
||||
_buttonDownload = findViewById(R.id.button_download);
|
||||
_buttonDownload.visibility = View.GONE;
|
||||
_buttonExport = findViewById(R.id.button_export);
|
||||
_buttonExport.visibility = View.GONE;
|
||||
|
||||
_buttonShare = findViewById(R.id.button_share);
|
||||
val onShare = _onShare;
|
||||
|
@ -68,6 +71,7 @@ abstract class VideoListEditorView : LinearLayout {
|
|||
buttonShuffle.setOnClickListener { onShuffleClick(); };
|
||||
|
||||
_buttonEdit.setOnClickListener { onEditClick(); };
|
||||
setButtonExportVisible(false);
|
||||
setButtonDownloadVisible(canEdit());
|
||||
|
||||
videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged);
|
||||
|
@ -108,6 +112,7 @@ abstract class VideoListEditorView : LinearLayout {
|
|||
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
|
||||
|
||||
if(isDownloading) {
|
||||
setButtonExportVisible(false);
|
||||
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
||||
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
||||
_buttonDownload.setOnClickListener {
|
||||
|
@ -117,6 +122,7 @@ abstract class VideoListEditorView : LinearLayout {
|
|||
}
|
||||
}
|
||||
else if(isDownloaded) {
|
||||
setButtonExportVisible(true)
|
||||
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
||||
_buttonDownload.setOnClickListener {
|
||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||
|
@ -125,6 +131,7 @@ abstract class VideoListEditorView : LinearLayout {
|
|||
}
|
||||
}
|
||||
else {
|
||||
setButtonExportVisible(false);
|
||||
_buttonDownload.setImageResource(R.drawable.ic_download);
|
||||
_buttonDownload.setOnClickListener {
|
||||
onDownload();
|
||||
|
@ -171,6 +178,9 @@ abstract class VideoListEditorView : LinearLayout {
|
|||
protected fun setButtonDownloadVisible(isVisible: Boolean) {
|
||||
_buttonDownload.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
||||
}
|
||||
protected fun setButtonExportVisible(isVisible: Boolean) {
|
||||
_buttonExport.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
||||
}
|
||||
|
||||
protected fun setButtonEditVisible(isVisible: Boolean) {
|
||||
_buttonEdit.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
||||
|
|
|
@ -14,9 +14,9 @@ import com.futo.platformplayer.R
|
|||
import com.futo.platformplayer.api.media.IPlatformClient
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.models.Playlist
|
||||
import com.futo.platformplayer.views.casting.CastButton
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
|
||||
class NavigationTopBarFragment : TopFragment() {
|
||||
private var _buttonBack: ImageButton? = null;
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.media3.datasource.ResolvingDataSource
|
|||
import androidx.media3.exoplayer.dash.DashMediaSource
|
||||
import androidx.media3.exoplayer.dash.manifest.DashManifestParser
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
|
@ -85,12 +86,17 @@ class VideoHelper {
|
|||
|
||||
return selectBestAudioSource((desc as VideoUnMuxedSourceDescriptor).audioSources.toList(), prefContainers, prefLanguage, targetBitrate);
|
||||
}
|
||||
fun selectBestAudioSource(altSources : Iterable<IAudioSource>, prefContainers : Array<String>, preferredLanguage: String? = null, targetBitrate: Long? = null) : IAudioSource? {
|
||||
fun selectBestAudioSource(sources : Iterable<IAudioSource>, prefContainers : Array<String>, preferredLanguage: String? = null, targetBitrate: Long? = null) : IAudioSource? {
|
||||
val hasPriority = sources.any { it.priority };
|
||||
var altSources = if(hasPriority) sources.filter { it.priority } else sources;
|
||||
val hasOriginal = altSources.any { it.original };
|
||||
if(hasOriginal && Settings.instance.playback.preferOriginalAudio)
|
||||
altSources = altSources.filter { it.original };
|
||||
val languageToFilter = if(preferredLanguage != null && altSources.any { it.language == preferredLanguage }) {
|
||||
preferredLanguage
|
||||
} else {
|
||||
if(altSources.any { it.language == Language.ENGLISH })
|
||||
Language.ENGLISH
|
||||
Language.ENGLISH;
|
||||
else
|
||||
Language.UNKNOWN;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.widget.ImageView
|
|||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.PresetImages
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Transient
|
||||
import java.io.File
|
||||
|
@ -18,7 +19,8 @@ data class ImageVariable(
|
|||
@Transient
|
||||
@Contextual
|
||||
private val bitmap: Bitmap? = null,
|
||||
val presetName: String? = null) {
|
||||
val presetName: String? = null,
|
||||
var subscriptionUrl: String? = null) {
|
||||
|
||||
@SuppressLint("DiscouragedApi")
|
||||
fun setImageView(imageView: ImageView, fallbackResId: Int = -1) {
|
||||
|
@ -63,7 +65,13 @@ data class ImageVariable(
|
|||
return ImageVariable(null, null, null, str);
|
||||
}
|
||||
fun fromFile(file: File): ImageVariable {
|
||||
return ImageVariable.fromBitmap(BitmapFactory.decodeFile(file.absolutePath));
|
||||
try {
|
||||
return ImageVariable.fromBitmap(BitmapFactory.decodeFile(file.absolutePath));
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.e("ImageVariable", "Unsupported image format? " + ex.message, ex);
|
||||
return fromResource(R.drawable.ic_error_pred);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -125,7 +125,7 @@ class HLS {
|
|||
return if (source is IHLSManifestSource) {
|
||||
listOf()
|
||||
} else if (source is IHLSManifestAudioSource) {
|
||||
listOf(HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, url))
|
||||
listOf(HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, false, url))
|
||||
} else {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
@ -346,7 +346,7 @@ class HLS {
|
|||
|
||||
val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
||||
return@mapNotNull when (it.type) {
|
||||
"AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, it.uri)
|
||||
"AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, false, it.uri)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,353 +0,0 @@
|
|||
package com.futo.platformplayer.polycentric
|
||||
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.constructs.BatchedTaskHandler
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.getNowDiffSeconds
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.resolveChannelUrls
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||
import com.futo.platformplayer.states.StatePolycentric
|
||||
import com.futo.platformplayer.stores.CachedPolycentricProfileStorage
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.polycentric.core.ApiMethods
|
||||
import com.futo.polycentric.core.ContentType
|
||||
import com.futo.polycentric.core.OwnedClaim
|
||||
import com.futo.polycentric.core.PublicKey
|
||||
import com.futo.polycentric.core.SignedEvent
|
||||
import com.futo.polycentric.core.StorageTypeSystemState
|
||||
import com.futo.polycentric.core.SystemState
|
||||
import com.futo.polycentric.core.base64ToByteArray
|
||||
import com.futo.polycentric.core.base64UrlToByteArray
|
||||
import com.futo.polycentric.core.getClaimIfValid
|
||||
import com.futo.polycentric.core.getValidClaims
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.serialization.Serializable
|
||||
import userpackage.Protocol
|
||||
import java.nio.ByteBuffer
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
class PolycentricCache {
|
||||
data class CachedOwnedClaims(val ownedClaims: List<OwnedClaim>?, val creationTime: OffsetDateTime = OffsetDateTime.now()) {
|
||||
val expired get() = creationTime.getNowDiffSeconds() > CACHE_EXPIRATION_SECONDS
|
||||
}
|
||||
@Serializable
|
||||
data class CachedPolycentricProfile(val profile: PolycentricProfile?, @Serializable(with = OffsetDateTimeSerializer::class) val creationTime: OffsetDateTime = OffsetDateTime.now()) {
|
||||
val expired get() = creationTime.getNowDiffSeconds() > CACHE_EXPIRATION_SECONDS
|
||||
}
|
||||
|
||||
private val _cache = hashMapOf<PlatformID, CachedOwnedClaims>()
|
||||
private val _profileCache = hashMapOf<PublicKey, CachedPolycentricProfile>()
|
||||
private val _profileUrlCache: CachedPolycentricProfileStorage;
|
||||
private val _scope = CoroutineScope(Dispatchers.IO);
|
||||
init {
|
||||
Logger.i(TAG, "Initializing Polycentric cache");
|
||||
val time = measureTimeMillis {
|
||||
_profileUrlCache = FragmentedStorage.get<CachedPolycentricProfileStorage>("profileUrlCache")
|
||||
}
|
||||
Logger.i(TAG, "Initialized Polycentric cache (${_profileUrlCache.map.size}, ${time}ms)");
|
||||
}
|
||||
|
||||
private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_scope,
|
||||
{ system ->
|
||||
val signedEventsList = ApiMethods.getQueryLatest(
|
||||
SERVER,
|
||||
system.toProto(),
|
||||
listOf(
|
||||
ContentType.BANNER.value,
|
||||
ContentType.AVATAR.value,
|
||||
ContentType.USERNAME.value,
|
||||
ContentType.DESCRIPTION.value,
|
||||
ContentType.STORE.value,
|
||||
ContentType.SERVER.value,
|
||||
ContentType.STORE_DATA.value,
|
||||
ContentType.PROMOTION_BANNER.value,
|
||||
ContentType.PROMOTION.value,
|
||||
ContentType.MEMBERSHIP_URLS.value,
|
||||
ContentType.DONATION_DESTINATIONS.value
|
||||
)
|
||||
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
||||
|
||||
val signedProfileEvents = signedEventsList.groupBy { e -> e.event.contentType }
|
||||
.map { (_, events) -> events.maxBy { it.event.unixMilliseconds ?: 0 } };
|
||||
|
||||
val storageSystemState = StorageTypeSystemState.create()
|
||||
for (signedEvent in signedProfileEvents) {
|
||||
storageSystemState.update(signedEvent.event)
|
||||
}
|
||||
|
||||
val signedClaimEvents = ApiMethods.getQueryIndex(
|
||||
SERVER,
|
||||
system.toProto(),
|
||||
ContentType.CLAIM.value,
|
||||
limit = 200
|
||||
).eventsList.map { e -> SignedEvent.fromProto(e) };
|
||||
|
||||
val ownedClaims: ArrayList<OwnedClaim> = arrayListOf()
|
||||
for (signedEvent in signedClaimEvents) {
|
||||
if (signedEvent.event.contentType != ContentType.CLAIM.value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
val response = ApiMethods.getQueryReferences(
|
||||
SERVER,
|
||||
Protocol.Reference.newBuilder()
|
||||
.setReference(signedEvent.toPointer().toProto().toByteString())
|
||||
.setReferenceType(2)
|
||||
.build(),
|
||||
null,
|
||||
Protocol.QueryReferencesRequestEvents.newBuilder()
|
||||
.setFromType(ContentType.VOUCH.value)
|
||||
.build()
|
||||
);
|
||||
|
||||
val ownedClaim = response.itemsList.map { SignedEvent.fromProto(it.event) }.getClaimIfValid(signedEvent);
|
||||
if (ownedClaim != null) {
|
||||
ownedClaims.add(ownedClaim);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Retrieved profile (ownedClaims = $ownedClaims)");
|
||||
val systemState = SystemState.fromStorageTypeSystemState(storageSystemState);
|
||||
return@BatchedTaskHandler CachedPolycentricProfile(PolycentricProfile(system, systemState, ownedClaims));
|
||||
},
|
||||
{ system -> return@BatchedTaskHandler getCachedProfile(system); },
|
||||
{ system, result ->
|
||||
synchronized(_cache) {
|
||||
_profileCache[system] = result;
|
||||
|
||||
if (result.profile != null) {
|
||||
for (claim in result.profile.ownedClaims) {
|
||||
val urls = claim.claim.resolveChannelUrls();
|
||||
for (url in urls)
|
||||
_profileUrlCache.map[url] = result;
|
||||
}
|
||||
}
|
||||
|
||||
_profileUrlCache.save();
|
||||
}
|
||||
});
|
||||
|
||||
private val _batchTaskGetClaims = BatchedTaskHandler<PlatformID, CachedOwnedClaims>(_scope,
|
||||
{ id ->
|
||||
val resolved = if (id.claimFieldType == -1) ApiMethods.getResolveClaim(SERVER, system, id.claimType.toLong(), id.value!!)
|
||||
else ApiMethods.getResolveClaim(SERVER, system, id.claimType.toLong(), id.claimFieldType.toLong(), id.value!!);
|
||||
Logger.v(TAG, "getResolveClaim(url = $SERVER, system = $system, id = $id, claimType = ${id.claimType}, matchAnyField = ${id.value})");
|
||||
val protoEvents = resolved.matchesList.flatMap { arrayListOf(it.claim).apply { addAll(it.proofChainList) } }
|
||||
val resolvedEvents = protoEvents.map { i -> SignedEvent.fromProto(i) };
|
||||
return@BatchedTaskHandler CachedOwnedClaims(resolvedEvents.getValidClaims());
|
||||
},
|
||||
{ id -> return@BatchedTaskHandler getCachedValidClaims(id); },
|
||||
{ id, result ->
|
||||
synchronized(_cache) {
|
||||
_cache[id] = result;
|
||||
}
|
||||
});
|
||||
|
||||
private val _batchTaskGetData = BatchedTaskHandler<String, ByteBuffer>(_scope,
|
||||
{
|
||||
val dataLink = getDataLinkFromUrl(it) ?: throw Exception("Only URLInfoDataLink is supported");
|
||||
return@BatchedTaskHandler ApiMethods.getDataFromServerAndReassemble(dataLink);
|
||||
},
|
||||
{ return@BatchedTaskHandler null },
|
||||
{ _, _ -> });
|
||||
|
||||
fun getCachedValidClaims(id: PlatformID, ignoreExpired: Boolean = false): CachedOwnedClaims? {
|
||||
if (!StatePolycentric.instance.enabled || id.claimType <= 0) {
|
||||
return CachedOwnedClaims(null);
|
||||
}
|
||||
|
||||
synchronized(_cache) {
|
||||
val cached = _cache[id]
|
||||
if (cached == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!ignoreExpired && cached.expired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Review all return null in this file, perhaps it should be CachedX(null) instead
|
||||
fun getValidClaimsAsync(id: PlatformID): Deferred<CachedOwnedClaims> {
|
||||
if (!StatePolycentric.instance.enabled || id.value == null || id.claimType <= 0) {
|
||||
return _scope.async { CachedOwnedClaims(null) };
|
||||
}
|
||||
|
||||
Logger.v(TAG, "getValidClaims (id: $id)")
|
||||
val def = _batchTaskGetClaims.execute(id);
|
||||
def.invokeOnCompletion {
|
||||
if (it == null) {
|
||||
return@invokeOnCompletion
|
||||
}
|
||||
|
||||
handleException(it, handleNetworkException = { /* Do nothing (do not cache) */ }, handleOtherException = {
|
||||
//Cache failed result
|
||||
synchronized(_cache) {
|
||||
_cache[id] = CachedOwnedClaims(null);
|
||||
}
|
||||
})
|
||||
};
|
||||
return def;
|
||||
}
|
||||
|
||||
fun getDataAsync(url: String): Deferred<ByteBuffer> {
|
||||
StatePolycentric.instance.ensureEnabled()
|
||||
return _batchTaskGetData.execute(url);
|
||||
}
|
||||
|
||||
fun getCachedProfile(url: String, ignoreExpired: Boolean = false): CachedPolycentricProfile? {
|
||||
if (!StatePolycentric.instance.enabled) {
|
||||
return CachedPolycentricProfile(null)
|
||||
}
|
||||
|
||||
synchronized (_profileCache) {
|
||||
val cached = _profileUrlCache.get(url) ?: return null;
|
||||
if (!ignoreExpired && cached.expired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
fun getCachedProfile(system: PublicKey, ignoreExpired: Boolean = false): CachedPolycentricProfile? {
|
||||
if (!StatePolycentric.instance.enabled) {
|
||||
return CachedPolycentricProfile(null)
|
||||
}
|
||||
|
||||
synchronized(_profileCache) {
|
||||
val cached = _profileCache[system] ?: return null;
|
||||
if (!ignoreExpired && cached.expired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getProfileAsync(id: PlatformID, urlNullCache: String? = null): CachedPolycentricProfile? {
|
||||
if (!StatePolycentric.instance.enabled || id.claimType <= 0) {
|
||||
return CachedPolycentricProfile(null);
|
||||
}
|
||||
|
||||
val cachedClaims = getCachedValidClaims(id);
|
||||
if (cachedClaims != null) {
|
||||
if (!cachedClaims.ownedClaims.isNullOrEmpty()) {
|
||||
Logger.v(TAG, "getProfileAsync (id: $id) != null (with cached valid claims)")
|
||||
return getProfileAsync(cachedClaims.ownedClaims.first().system).await();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
Logger.v(TAG, "getProfileAsync (id: $id) no cached valid claims, will be retrieved")
|
||||
|
||||
val claims = getValidClaimsAsync(id).await()
|
||||
if (!claims.ownedClaims.isNullOrEmpty()) {
|
||||
Logger.v(TAG, "getProfileAsync (id: $id) != null (with retrieved valid claims)")
|
||||
return getProfileAsync(claims.ownedClaims.first().system).await()
|
||||
} else {
|
||||
synchronized (_cache) {
|
||||
if (urlNullCache != null) {
|
||||
_profileUrlCache.setAndSave(urlNullCache, CachedPolycentricProfile(null))
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getProfileAsync(system: PublicKey): Deferred<CachedPolycentricProfile?> {
|
||||
if (!StatePolycentric.instance.enabled) {
|
||||
return _scope.async { CachedPolycentricProfile(null) };
|
||||
}
|
||||
|
||||
Logger.i(TAG, "getProfileAsync (system: ${system})")
|
||||
val def = _taskGetProfile.execute(system);
|
||||
def.invokeOnCompletion {
|
||||
if (it == null) {
|
||||
return@invokeOnCompletion
|
||||
}
|
||||
|
||||
handleException(it, handleNetworkException = { /* Do nothing (do not cache) */ }, handleOtherException = {
|
||||
//Cache failed result
|
||||
synchronized(_cache) {
|
||||
val cachedProfile = CachedPolycentricProfile(null);
|
||||
_profileCache[system] = cachedProfile;
|
||||
}
|
||||
})
|
||||
};
|
||||
return def;
|
||||
}
|
||||
|
||||
private fun handleException(e: Throwable, handleNetworkException: () -> Unit, handleOtherException: () -> Unit) {
|
||||
val isNetworkException = when(e) {
|
||||
is java.net.UnknownHostException,
|
||||
is java.net.SocketTimeoutException,
|
||||
is java.net.ConnectException -> true
|
||||
else -> when(e.cause) {
|
||||
is java.net.UnknownHostException,
|
||||
is java.net.SocketTimeoutException,
|
||||
is java.net.ConnectException -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
if (isNetworkException) {
|
||||
handleNetworkException()
|
||||
} else {
|
||||
handleOtherException()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val system = Protocol.PublicKey.newBuilder()
|
||||
.setKeyType(1)
|
||||
.setKey(ByteString.copyFrom("gX0eCWctTm6WHVGot4sMAh7NDAIwWsIM5tRsOz9dX04=".base64ToByteArray())) //Production key
|
||||
//.setKey(ByteString.copyFrom("LeQkzn1j625YZcZHayfCmTX+6ptrzsA+CdAyq+BcEdQ".base64ToByteArray())) //Test key koen-futo
|
||||
.build();
|
||||
|
||||
private const val TAG = "PolycentricCache"
|
||||
const val SERVER = "https://srv1-prod.polycentric.io"
|
||||
private var _instance: PolycentricCache? = null;
|
||||
private val CACHE_EXPIRATION_SECONDS = 60 * 5;
|
||||
|
||||
@JvmStatic
|
||||
val instance: PolycentricCache
|
||||
get(){
|
||||
if(_instance == null)
|
||||
_instance = PolycentricCache();
|
||||
return _instance!!;
|
||||
};
|
||||
|
||||
fun finish() {
|
||||
_instance?.let {
|
||||
_instance = null;
|
||||
it._scope.cancel("PolycentricCache finished");
|
||||
}
|
||||
}
|
||||
|
||||
fun getDataLinkFromUrl(it: String): Protocol.URLInfoDataLink? {
|
||||
val urlData = if (it.startsWith("polycentric://")) {
|
||||
it.substring("polycentric://".length)
|
||||
} else it;
|
||||
|
||||
val urlBytes = urlData.base64UrlToByteArray();
|
||||
val urlInfo = Protocol.URLInfo.parseFrom(urlBytes);
|
||||
if (urlInfo.urlType != 4L) {
|
||||
return null
|
||||
}
|
||||
|
||||
val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body);
|
||||
return dataLink
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) };
|
||||
|
||||
|
|
|
@ -3,9 +3,11 @@ package com.futo.platformplayer.states
|
|||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.os.StatFs
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
|
@ -46,6 +48,17 @@ class StateDownloads {
|
|||
private val _downloadsStat = StatFs(_downloadsDirectory.absolutePath);
|
||||
|
||||
private val _downloaded = FragmentedStorage.storeJson<VideoLocal>("downloaded")
|
||||
.withOnModified({
|
||||
synchronized(_downloadedSet) {
|
||||
if(!_downloadedSet.contains(it.id))
|
||||
_downloadedSet.add(it.id);
|
||||
}
|
||||
}, {
|
||||
synchronized(_downloadedSet) {
|
||||
if(_downloadedSet.contains(it.id))
|
||||
_downloadedSet.remove(it.id);
|
||||
}
|
||||
})
|
||||
.load()
|
||||
.apply { afterLoadingDownloaded(this) };
|
||||
private val _downloading = FragmentedStorage.storeJson<VideoDownload>("downloading")
|
||||
|
@ -85,9 +98,6 @@ class StateDownloads {
|
|||
Logger.i("StateDownloads", "Deleting local video ${id.value}");
|
||||
val downloaded = getCachedVideo(id);
|
||||
if(downloaded != null) {
|
||||
synchronized(_downloadedSet) {
|
||||
_downloadedSet.remove(id);
|
||||
}
|
||||
_downloaded.delete(downloaded);
|
||||
}
|
||||
onDownloadedChanged.emit();
|
||||
|
@ -261,9 +271,6 @@ class StateDownloads {
|
|||
if(existing.groupID == null) {
|
||||
existing.groupID = VideoDownload.GROUP_WATCHLATER;
|
||||
existing.groupType = VideoDownload.GROUP_WATCHLATER;
|
||||
synchronized(_downloadedSet) {
|
||||
_downloadedSet.add(existing.id);
|
||||
}
|
||||
_downloaded.save(existing);
|
||||
}
|
||||
}
|
||||
|
@ -306,9 +313,6 @@ class StateDownloads {
|
|||
if(existing.groupID == null) {
|
||||
existing.groupID = playlist.id;
|
||||
existing.groupType = VideoDownload.GROUP_PLAYLIST;
|
||||
synchronized(_downloadedSet) {
|
||||
_downloadedSet.add(existing.id);
|
||||
}
|
||||
_downloaded.save(existing);
|
||||
}
|
||||
}
|
||||
|
@ -466,6 +470,65 @@ class StateDownloads {
|
|||
return _downloadsDirectory;
|
||||
}
|
||||
|
||||
fun exportPlaylist(context: Context, playlistId: String) {
|
||||
if(context is IWithResultLauncher)
|
||||
StateApp.instance.requestDirectoryAccess(context, "Export Playlist", "To export playlist to directory", null) {
|
||||
if (it == null)
|
||||
return@requestDirectoryAccess;
|
||||
|
||||
val root = DocumentFile.fromTreeUri(context, it!!);
|
||||
|
||||
val playlist = StatePlaylists.instance.getPlaylist(playlistId);
|
||||
var localVideos = StateDownloads.instance.getDownloadedVideosPlaylist(playlistId);
|
||||
if(playlist != null) {
|
||||
val missing = playlist.videos
|
||||
.filter { vid -> !localVideos.any { it.id.value == null || it.id.value == vid.id.value } }
|
||||
.map { getCachedVideo(it.id) }
|
||||
.filterNotNull();
|
||||
if(missing.size > 0)
|
||||
localVideos = localVideos + missing;
|
||||
};
|
||||
|
||||
var lastNotifyTime = -1L;
|
||||
|
||||
UIDialogs.showDialogProgress(context) {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
it.setText("Exporting videos..");
|
||||
var i = 0;
|
||||
var success = 0;
|
||||
for (video in localVideos) {
|
||||
withContext(Dispatchers.Main) {
|
||||
it.setText("Exporting videos...(${i}/${localVideos.size})");
|
||||
//it.setProgress(i.toDouble() / localVideos.size);
|
||||
}
|
||||
|
||||
try {
|
||||
val export = VideoExport(video, video.videoSource.firstOrNull(), video.audioSource.firstOrNull(), video.subtitlesSources.firstOrNull());
|
||||
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
|
||||
|
||||
val file = export.export(context, { progress ->
|
||||
val now = System.currentTimeMillis();
|
||||
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
|
||||
it.setProgress(progress);
|
||||
lastNotifyTime = now;
|
||||
}
|
||||
}, root);
|
||||
success++;
|
||||
} catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed export [${video.name}]: ${ex.message}", ex);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
it.setProgress(1f);
|
||||
it.dismiss();
|
||||
UIDialogs.appToast("Finished exporting playlist (${success} videos${if(i < success) ", ${i} errors" else ""})");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
|
||||
var lastNotifyTime = -1L;
|
||||
|
||||
|
@ -477,13 +540,13 @@ class StateDownloads {
|
|||
try {
|
||||
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
|
||||
|
||||
val file = export.export(context) { progress ->
|
||||
val file = export.export(context, { progress ->
|
||||
val now = System.currentTimeMillis();
|
||||
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
|
||||
it.setProgress(progress);
|
||||
lastNotifyTime = now;
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
it.setProgress(100.0f)
|
||||
|
|
|
@ -14,7 +14,7 @@ class StateMeta {
|
|||
return when(lastCommentSection.value){
|
||||
"Polycentric" -> 0;
|
||||
"Platform" -> 1;
|
||||
else -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
fun setLastCommentSection(value: Int) {
|
||||
|
|
|
@ -13,6 +13,7 @@ import com.futo.platformplayer.UIDialogs
|
|||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
|
@ -130,6 +131,12 @@ class StatePlayer {
|
|||
closeMediaSession();
|
||||
}
|
||||
|
||||
fun saveQueueAsPlaylist(name: String){
|
||||
val videos = _queue.toList();
|
||||
val playlist = Playlist(name, videos.map { SerializedPlatformVideo.fromVideo(it) });
|
||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||
}
|
||||
|
||||
//Notifications
|
||||
fun hasMediaSession() : Boolean {
|
||||
return MediaPlaybackService.getService() != null;
|
||||
|
|
|
@ -177,11 +177,14 @@ class StatePlaylists {
|
|||
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
||||
}
|
||||
}
|
||||
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1) {
|
||||
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1): Boolean {
|
||||
var wasNew = false;
|
||||
synchronized(_watchlistStore) {
|
||||
if(!_watchlistStore.hasItem { it.url == video.url })
|
||||
wasNew = true;
|
||||
_watchlistStore.saveAsync(video);
|
||||
if(orderPosition == -1)
|
||||
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values) .toTypedArray());
|
||||
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values).toTypedArray());
|
||||
else {
|
||||
val existing = _watchlistOrderStore.getAllValues().toMutableList();
|
||||
existing.add(orderPosition, video.url);
|
||||
|
@ -198,6 +201,7 @@ class StatePlaylists {
|
|||
}
|
||||
|
||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||
return wasNew;
|
||||
}
|
||||
|
||||
fun getLastPlayedPlaylist() : Playlist? {
|
||||
|
@ -226,17 +230,20 @@ class StatePlaylists {
|
|||
}
|
||||
}
|
||||
|
||||
public fun getWatchLaterSyncPacket(orderOnly: Boolean = false): SyncWatchLaterPackage{
|
||||
return SyncWatchLaterPackage(
|
||||
if (orderOnly) listOf() else getWatchLater(),
|
||||
if (orderOnly) mapOf() else _watchLaterAdds.all(),
|
||||
if (orderOnly) mapOf() else _watchLaterRemovals.all(),
|
||||
getWatchLaterLastReorderTime().toEpochSecond(),
|
||||
_watchlistOrderStore.values.toList()
|
||||
)
|
||||
}
|
||||
private fun broadcastWatchLater(orderOnly: Boolean = false) {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
StateSync.instance.broadcastJsonData(
|
||||
GJSyncOpcodes.syncWatchLater, SyncWatchLaterPackage(
|
||||
if (orderOnly) listOf() else getWatchLater(),
|
||||
if (orderOnly) mapOf() else _watchLaterAdds.all(),
|
||||
if (orderOnly) mapOf() else _watchLaterRemovals.all(),
|
||||
getWatchLaterLastReorderTime().toEpochSecond(),
|
||||
_watchlistOrderStore.values.toList()
|
||||
)
|
||||
GJSyncOpcodes.syncWatchLater, getWatchLaterSyncPacket(orderOnly)
|
||||
);
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to broadcast watch later", e)
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.futo.platformplayer.states
|
|||
|
||||
import android.content.Context
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.LoginActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
|
@ -101,6 +102,8 @@ class StatePlugins {
|
|||
if (availableClient !is JSClient) {
|
||||
continue
|
||||
}
|
||||
if(!Settings.instance.plugins.checkDisabledPluginsForUpdates && !StatePlatform.instance.isClientEnabled(availableClient.id))
|
||||
continue;
|
||||
|
||||
val newConfig = checkForUpdates(availableClient.config);
|
||||
if (newConfig != null) {
|
||||
|
|
|
@ -21,9 +21,7 @@ import com.futo.platformplayer.api.media.structures.IPager
|
|||
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
|
||||
import com.futo.platformplayer.awaitFirstDeferred
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.polycentric.PolycentricStorage
|
||||
import com.futo.platformplayer.resolveChannelUrl
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
|
@ -33,6 +31,7 @@ import com.futo.polycentric.core.ApiMethods
|
|||
import com.futo.polycentric.core.ClaimType
|
||||
import com.futo.polycentric.core.ContentType
|
||||
import com.futo.polycentric.core.Opinion
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
import com.futo.polycentric.core.ProcessHandle
|
||||
import com.futo.polycentric.core.PublicKey
|
||||
import com.futo.polycentric.core.SignedEvent
|
||||
|
@ -234,34 +233,7 @@ class StatePolycentric {
|
|||
if (!enabled) {
|
||||
return Pair(false, listOf(url));
|
||||
}
|
||||
var polycentricProfile: PolycentricProfile? = null;
|
||||
try {
|
||||
val polycentricCached = PolycentricCache.instance.getCachedProfile(url, cacheOnly)
|
||||
polycentricProfile = polycentricCached?.profile;
|
||||
if (polycentricCached == null && channelId != null) {
|
||||
Logger.i("StateSubscriptions", "Get polycentric profile not cached");
|
||||
if(!cacheOnly) {
|
||||
polycentricProfile = runBlocking { PolycentricCache.instance.getProfileAsync(channelId, if(doCacheNull) url else null) }?.profile;
|
||||
didUpdate = true;
|
||||
}
|
||||
} else {
|
||||
Logger.i("StateSubscriptions", "Get polycentric profile cached");
|
||||
}
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
Logger.w(StateSubscriptions.TAG, "Polycentric getCachedProfile failed for subscriptions", ex);
|
||||
//TODO: Some way to communicate polycentric failing without blocking here
|
||||
}
|
||||
if(polycentricProfile != null) {
|
||||
val urls = polycentricProfile.ownedClaims.groupBy { it.claim.claimType }
|
||||
.mapNotNull { it.value.firstOrNull()?.claim?.resolveChannelUrl() }.toMutableList();
|
||||
if(urls.any { it.equals(url, true) })
|
||||
return Pair(didUpdate, urls);
|
||||
else
|
||||
return Pair(didUpdate, listOf(url) + urls);
|
||||
}
|
||||
else
|
||||
return Pair(didUpdate, listOf(url));
|
||||
return Pair(didUpdate, listOf(url));
|
||||
}
|
||||
|
||||
fun getChannelContent(scope: CoroutineScope, profile: PolycentricProfile, isSubscriptionOptimized: Boolean = false, channelConcurrency: Int = -1): IPager<IPlatformContent>? {
|
||||
|
@ -325,7 +297,7 @@ class StatePolycentric {
|
|||
id = PlatformID("polycentric", author, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||
name = systemState.username,
|
||||
url = author,
|
||||
thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
||||
thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
|
||||
subscribers = null
|
||||
),
|
||||
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||
|
@ -349,7 +321,7 @@ class StatePolycentric {
|
|||
suspend fun getLikesDislikesReplies(reference: Protocol.Reference): LikesDislikesReplies {
|
||||
ensureEnabled()
|
||||
|
||||
val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null,
|
||||
val response = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, null,
|
||||
null,
|
||||
listOf(
|
||||
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
|
||||
|
@ -382,7 +354,7 @@ class StatePolycentric {
|
|||
}
|
||||
|
||||
val pointer = Protocol.Pointer.parseFrom(reference.reference)
|
||||
val events = ApiMethods.getEvents(PolycentricCache.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder()
|
||||
val events = ApiMethods.getEvents(ApiMethods.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder()
|
||||
.addRangesForProcesses(Protocol.RangesForProcess.newBuilder()
|
||||
.setProcess(pointer.process)
|
||||
.addRanges(Protocol.Range.newBuilder()
|
||||
|
@ -400,11 +372,11 @@ class StatePolycentric {
|
|||
}
|
||||
|
||||
val post = Protocol.Post.parseFrom(ev.content);
|
||||
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
|
||||
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(ApiMethods.SERVER));
|
||||
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
||||
|
||||
val profileEvents = ApiMethods.getQueryLatest(
|
||||
PolycentricCache.SERVER,
|
||||
ApiMethods.SERVER,
|
||||
ev.system.toProto(),
|
||||
listOf(
|
||||
ContentType.AVATAR.value,
|
||||
|
@ -433,7 +405,7 @@ class StatePolycentric {
|
|||
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
||||
url = systemLinkUrl,
|
||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
|
||||
subscribers = null
|
||||
),
|
||||
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||
|
@ -445,12 +417,12 @@ class StatePolycentric {
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
|
||||
suspend fun getCommentPager(contextUrl: String, reference: Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
|
||||
if (!enabled) {
|
||||
return EmptyPager()
|
||||
}
|
||||
|
||||
val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null,
|
||||
val response = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, null,
|
||||
Protocol.QueryReferencesRequestEvents.newBuilder()
|
||||
.setFromType(ContentType.POST.value)
|
||||
.addAllCountLwwElementReferences(arrayListOf(
|
||||
|
@ -486,7 +458,7 @@ class StatePolycentric {
|
|||
}
|
||||
|
||||
override suspend fun nextPageAsync() {
|
||||
val nextPageResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, _cursor,
|
||||
val nextPageResponse = ApiMethods.getQueryReferences(ApiMethods.SERVER, reference, _cursor,
|
||||
Protocol.QueryReferencesRequestEvents.newBuilder()
|
||||
.setFromType(ContentType.POST.value)
|
||||
.addAllCountLwwElementReferences(arrayListOf(
|
||||
|
@ -534,7 +506,7 @@ class StatePolycentric {
|
|||
return@mapNotNull LazyComment(scope.async(_commentPoolDispatcher){
|
||||
Logger.i(TAG, "Fetching comment data for [" + ev.system.key.toBase64() + "]");
|
||||
val profileEvents = ApiMethods.getQueryLatest(
|
||||
PolycentricCache.SERVER,
|
||||
ApiMethods.SERVER,
|
||||
ev.system.toProto(),
|
||||
listOf(
|
||||
ContentType.AVATAR.value,
|
||||
|
@ -558,7 +530,7 @@ class StatePolycentric {
|
|||
|
||||
val unixMilliseconds = ev.unixMilliseconds
|
||||
//TODO: Don't use single hardcoded sderver here
|
||||
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
|
||||
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(ApiMethods.SERVER));
|
||||
val dp_25 = 25.dp(StateApp.instance.context.resources)
|
||||
return@async PolycentricPlatformComment(
|
||||
contextUrl = contextUrl,
|
||||
|
@ -566,7 +538,7 @@ class StatePolycentric {
|
|||
id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()),
|
||||
name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown",
|
||||
url = systemLinkUrl,
|
||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
|
||||
thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(ApiMethods.SERVER)) },
|
||||
subscribers = null
|
||||
),
|
||||
msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
|
||||
|
|
|
@ -1,54 +1,17 @@
|
|||
package com.futo.platformplayer.states
|
||||
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.structures.*
|
||||
import com.futo.platformplayer.api.media.structures.ReusablePager.Companion.asReusable
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
|
||||
import com.futo.platformplayer.exceptions.ChannelException
|
||||
import com.futo.platformplayer.findNonRuntimeException
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.getNowDiffDays
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.resolveChannelUrl
|
||||
import com.futo.platformplayer.states.StateHistory.Companion
|
||||
import com.futo.platformplayer.stores.FragmentedStorage
|
||||
import com.futo.platformplayer.stores.StringDateMapStorage
|
||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||
import com.futo.platformplayer.stores.v2.ReconstructStore
|
||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
|
||||
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
|
||||
import com.futo.platformplayer.sync.internal.GJSyncOpcodes
|
||||
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
|
||||
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
import java.util.concurrent.ForkJoinTask
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.streams.asSequence
|
||||
import kotlin.streams.toList
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
/***
|
||||
* Used to maintain subscription groups
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -33,6 +33,9 @@ class ManagedStore<T>{
|
|||
|
||||
val className: String? get() = _class.classifier?.assume<KClass<*>>()?.simpleName;
|
||||
|
||||
private var _onModificationCreate: ((T) -> Unit)? = null;
|
||||
private var _onModificationDelete: ((T) -> Unit)? = null;
|
||||
|
||||
val name: String;
|
||||
|
||||
constructor(name: String, dir: File, clazz: KType, serializer: StoreSerializer<T>, niceName: String? = null) {
|
||||
|
@ -62,6 +65,12 @@ class ManagedStore<T>{
|
|||
return this;
|
||||
}
|
||||
|
||||
fun withOnModified(created: (T)->Unit, deleted: (T)->Unit): ManagedStore<T> {
|
||||
_onModificationCreate = created;
|
||||
_onModificationDelete = deleted;
|
||||
return this;
|
||||
}
|
||||
|
||||
fun load(): ManagedStore<T> {
|
||||
synchronized(_files) {
|
||||
_files.clear();
|
||||
|
@ -265,6 +274,7 @@ class ManagedStore<T>{
|
|||
file = saveNew(obj);
|
||||
if(_reconstructStore != null && (_reconstructStore!!.backupOnCreate || withReconstruction))
|
||||
saveReconstruction(file, obj);
|
||||
_onModificationCreate?.invoke(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -300,6 +310,7 @@ class ManagedStore<T>{
|
|||
_files.remove(item);
|
||||
Logger.v(TAG, "Deleting file ${logName(file.id)}");
|
||||
file.delete();
|
||||
_onModificationDelete?.invoke(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,12 +6,10 @@ import com.futo.platformplayer.api.media.Serializer
|
|||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.HistoryVideo
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.smartMerge
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
|
@ -30,6 +28,7 @@ import kotlinx.serialization.encodeToString
|
|||
import kotlinx.serialization.json.Json
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
@ -53,6 +52,9 @@ class SyncSession : IAuthorizable {
|
|||
private val _id = UUID.randomUUID()
|
||||
private var _remoteId: UUID? = null
|
||||
private var _lastAuthorizedRemoteId: UUID? = null
|
||||
var remoteDeviceName: String? = null
|
||||
private set
|
||||
val displayName: String get() = remoteDeviceName ?: remotePublicKey
|
||||
|
||||
var connected: Boolean = false
|
||||
private set(v) {
|
||||
|
@ -62,7 +64,7 @@ class SyncSession : IAuthorizable {
|
|||
}
|
||||
}
|
||||
|
||||
constructor(remotePublicKey: String, onAuthorized: (session: SyncSession, isNewlyAuthorized: Boolean, isNewSession: Boolean) -> Unit, onUnauthorized: (session: SyncSession) -> Unit, onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit, onClose: (session: SyncSession) -> Unit) {
|
||||
constructor(remotePublicKey: String, onAuthorized: (session: SyncSession, isNewlyAuthorized: Boolean, isNewSession: Boolean) -> Unit, onUnauthorized: (session: SyncSession) -> Unit, onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit, onClose: (session: SyncSession) -> Unit, remoteDeviceName: String?) {
|
||||
this.remotePublicKey = remotePublicKey
|
||||
_onAuthorized = onAuthorized
|
||||
_onUnauthorized = onUnauthorized
|
||||
|
@ -85,7 +87,20 @@ class SyncSession : IAuthorizable {
|
|||
|
||||
fun authorize(socketSession: SyncSocketSession) {
|
||||
Logger.i(TAG, "Sent AUTHORIZED with session id $_id")
|
||||
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(_id.toString().toByteArray()))
|
||||
|
||||
if (socketSession.remoteVersion >= 3) {
|
||||
val idStringBytes = _id.toString().toByteArray()
|
||||
val nameBytes = "${android.os.Build.MANUFACTURER}-${android.os.Build.MODEL}".toByteArray()
|
||||
val buffer = ByteArray(1 + idStringBytes.size + 1 + nameBytes.size)
|
||||
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).apply {
|
||||
put(idStringBytes.size.toByte())
|
||||
put(idStringBytes)
|
||||
put(nameBytes.size.toByte())
|
||||
put(nameBytes)
|
||||
}.apply { flip() })
|
||||
} else {
|
||||
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(_id.toString().toByteArray()))
|
||||
}
|
||||
_authorized = true
|
||||
checkAuthorized()
|
||||
}
|
||||
|
@ -138,15 +153,37 @@ class SyncSession : IAuthorizable {
|
|||
|
||||
when (opcode) {
|
||||
Opcode.NOTIFY_AUTHORIZED.value -> {
|
||||
val str = data.toUtf8String()
|
||||
_remoteId = if (data.remaining() >= 0) UUID.fromString(str) else UUID.fromString("00000000-0000-0000-0000-000000000000")
|
||||
if (socketSession.remoteVersion >= 3) {
|
||||
val idByteCount = data.get().toInt()
|
||||
if (idByteCount > 64)
|
||||
throw Exception("Id should always be smaller than 64 bytes")
|
||||
|
||||
val idBytes = ByteArray(idByteCount)
|
||||
data.get(idBytes)
|
||||
|
||||
val nameByteCount = data.get().toInt()
|
||||
if (nameByteCount > 64)
|
||||
throw Exception("Name should always be smaller than 64 bytes")
|
||||
|
||||
val nameBytes = ByteArray(nameByteCount)
|
||||
data.get(nameBytes)
|
||||
|
||||
_remoteId = UUID.fromString(idBytes.toString(Charsets.UTF_8))
|
||||
remoteDeviceName = nameBytes.toString(Charsets.UTF_8)
|
||||
} else {
|
||||
val str = data.toUtf8String()
|
||||
_remoteId = if (data.remaining() >= 0) UUID.fromString(str) else UUID.fromString("00000000-0000-0000-0000-000000000000")
|
||||
remoteDeviceName = null
|
||||
}
|
||||
|
||||
_remoteAuthorized = true
|
||||
Logger.i(TAG, "Received AUTHORIZED with session id $_remoteId")
|
||||
Logger.i(TAG, "Received AUTHORIZED with session id $_remoteId (device name: '${remoteDeviceName ?: "not set"}')")
|
||||
checkAuthorized()
|
||||
return
|
||||
}
|
||||
Opcode.NOTIFY_UNAUTHORIZED.value -> {
|
||||
_remoteId = null
|
||||
remoteDeviceName = null
|
||||
_lastAuthorizedRemoteId = null
|
||||
_remoteAuthorized = false
|
||||
_onUnauthorized(this)
|
||||
|
@ -195,6 +232,8 @@ class SyncSession : IAuthorizable {
|
|||
sendData(GJSyncOpcodes.syncSubscriptionGroups, StateSubscriptionGroups.instance.getSyncSubscriptionGroupsPackageString());
|
||||
sendData(GJSyncOpcodes.syncPlaylists, StatePlaylists.instance.getSyncPlaylistsPackageString())
|
||||
|
||||
sendData(GJSyncOpcodes.syncWatchLater, Json.encodeToString(StatePlaylists.instance.getWatchLaterSyncPacket(false)));
|
||||
|
||||
val recentHistory = StateHistory.instance.getRecentHistory(syncSessionData.lastHistory);
|
||||
if(recentHistory.size > 0)
|
||||
sendJsonData(GJSyncOpcodes.syncHistory, recentHistory);
|
||||
|
|
|
@ -46,6 +46,8 @@ class SyncSocketSession {
|
|||
val localPublicKey: String get() = _localPublicKey
|
||||
private val _onData: (session: SyncSocketSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) -> Unit
|
||||
var authorizable: IAuthorizable? = null
|
||||
var remoteVersion: Int = -1
|
||||
private set
|
||||
|
||||
val remoteAddress: String
|
||||
|
||||
|
@ -162,11 +164,12 @@ class SyncSocketSession {
|
|||
}
|
||||
|
||||
private fun performVersionCheck() {
|
||||
val CURRENT_VERSION = 2
|
||||
val CURRENT_VERSION = 3
|
||||
val MINIMUM_VERSION = 2
|
||||
_outputStream.writeInt(CURRENT_VERSION)
|
||||
val version = _inputStream.readInt()
|
||||
Logger.i(TAG, "performVersionCheck (version = $version)")
|
||||
if (version != CURRENT_VERSION)
|
||||
remoteVersion = _inputStream.readInt()
|
||||
Logger.i(TAG, "performVersionCheck (version = $remoteVersion)")
|
||||
if (remoteVersion < MINIMUM_VERSION)
|
||||
throw Exception("Invalid version")
|
||||
}
|
||||
|
||||
|
|
|
@ -16,11 +16,11 @@ import com.futo.platformplayer.constructs.Event0
|
|||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||
import com.futo.platformplayer.views.adapters.viewholders.StoreItemViewHolder
|
||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
|
@ -125,8 +125,7 @@ class MonetizationView : LinearLayout {
|
|||
}
|
||||
}
|
||||
|
||||
fun setPolycentricProfile(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?) {
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
fun setPolycentricProfile(profile: PolycentricProfile?) {
|
||||
if (profile != null) {
|
||||
if (profile.systemState.store.isNotEmpty()) {
|
||||
_buttonStore.visibility = View.VISIBLE;
|
||||
|
|
|
@ -14,10 +14,10 @@ import androidx.core.view.isVisible
|
|||
import androidx.core.view.size
|
||||
import com.bumptech.glide.Glide
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.views.buttons.BigButton
|
||||
import com.futo.polycentric.core.PolycentricProfile
|
||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
|
||||
|
|
74
app/src/main/java/com/futo/platformplayer/views/ToggleBar.kt
Normal file
74
app/src/main/java/com/futo/platformplayer/views/ToggleBar.kt
Normal file
|
@ -0,0 +1,74 @@
|
|||
package com.futo.platformplayer.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||
import com.futo.platformplayer.views.others.ToggleTagView
|
||||
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionBarViewHolder
|
||||
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
|
||||
import com.futo.platformplayer.views.subscriptions.SubscriptionExploreButton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ToggleBar : LinearLayout {
|
||||
private val _tagsContainer: LinearLayout;
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
StateSubscriptionGroups.instance.onGroupsChanged.remove(this);
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
inflate(context, R.layout.view_toggle_bar, this);
|
||||
|
||||
_tagsContainer = findViewById(R.id.container_tags);
|
||||
}
|
||||
|
||||
fun setToggles(vararg buttons: Toggle) {
|
||||
_tagsContainer.removeAllViews();
|
||||
for(button in buttons) {
|
||||
_tagsContainer.addView(ToggleTagView(context).apply {
|
||||
this.setInfo(button.name, button.isActive);
|
||||
this.onClick.subscribe { button.action(it); };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Toggle {
|
||||
val name: String;
|
||||
val icon: Int;
|
||||
val action: (Boolean)->Unit;
|
||||
val isActive: Boolean;
|
||||
|
||||
constructor(name: String, icon: Int, isActive: Boolean = false, action: (Boolean)->Unit) {
|
||||
this.name = name;
|
||||
this.icon = icon;
|
||||
this.action = action;
|
||||
this.isActive = isActive;
|
||||
}
|
||||
constructor(name: String, isActive: Boolean = false, action: (Boolean)->Unit) {
|
||||
this.name = name;
|
||||
this.icon = 0;
|
||||
this.action = action;
|
||||
this.isActive = isActive;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
|||
private lateinit var _sortedDataset: List<Subscription>;
|
||||
private val _inflater: LayoutInflater;
|
||||
private val _confirmationMessage: String;
|
||||
private val _onDatasetChanged: ((List<Subscription>)->Unit)?;
|
||||
|
||||
var onClick = Event1<Subscription>();
|
||||
var onSettings = Event1<Subscription>();
|
||||
|
@ -30,9 +31,10 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
|||
updateDataset();
|
||||
}
|
||||
|
||||
constructor(inflater: LayoutInflater, confirmationMessage: String) : super() {
|
||||
constructor(inflater: LayoutInflater, confirmationMessage: String, onDatasetChanged: ((List<Subscription>)->Unit)? = null) : super() {
|
||||
_inflater = inflater;
|
||||
_confirmationMessage = confirmationMessage;
|
||||
_onDatasetChanged = onDatasetChanged;
|
||||
|
||||
StateSubscriptions.instance.onSubscriptionsChanged.subscribe { _, _ -> if(Looper.myLooper() != Looper.getMainLooper())
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { updateDataset() }
|
||||
|
@ -78,6 +80,8 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
|||
.filter { (queryLower.isNullOrBlank() || it.channel.name.lowercase().contains(queryLower)) }
|
||||
.toList();
|
||||
|
||||
_onDatasetChanged?.invoke(_sortedDataset);
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ import com.futo.platformplayer.constructs.TaskHandler
|
|||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.toHumanTimeIndicator
|
||||
|
@ -32,14 +31,6 @@ class SubscriptionViewHolder : ViewHolder {
|
|||
private val _platformIndicator : PlatformIndicator;
|
||||
private val _textMeta: TextView;
|
||||
|
||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
||||
StateApp.instance.scopeGetter,
|
||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
||||
.success { it -> onProfileLoaded(null, it, true) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load profile.", it);
|
||||
};
|
||||
|
||||
var subscription: Subscription? = null
|
||||
private set;
|
||||
|
||||
|
@ -74,45 +65,12 @@ class SubscriptionViewHolder : ViewHolder {
|
|||
}
|
||||
|
||||
fun bind(sub: Subscription) {
|
||||
_taskLoadProfile.cancel();
|
||||
|
||||
this.subscription = sub;
|
||||
|
||||
_creatorThumbnail.setThumbnail(sub.channel.thumbnail, false);
|
||||
_textName.text = sub.channel.name;
|
||||
bindViewMetrics(sub);
|
||||
_platformIndicator.setPlatformFromClientID(sub.channel.id.pluginId);
|
||||
|
||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(sub.channel.url, true);
|
||||
if (cachedProfile != null) {
|
||||
onProfileLoaded(sub, cachedProfile, false);
|
||||
if (cachedProfile.expired) {
|
||||
_taskLoadProfile.run(sub.channel.id);
|
||||
}
|
||||
} else {
|
||||
_taskLoadProfile.run(sub.channel.id);
|
||||
}
|
||||
}
|
||||
|
||||
private fun onProfileLoaded(sub: Subscription?, cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
val dp_46 = 46.dp(itemView.context.resources);
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_46 * dp_46)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(this.subscription?.channel?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
_textName.text = profile.systemState.username;
|
||||
}
|
||||
|
||||
if(sub != null)
|
||||
bindViewMetrics(sub)
|
||||
}
|
||||
|
||||
fun bindViewMetrics(sub: Subscription?) {
|
||||
|
|
|
@ -30,7 +30,6 @@ import com.futo.platformplayer.dp
|
|||
import com.futo.platformplayer.fixHtmlWhitespace
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.toHumanNowDiffString
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
|
@ -44,7 +43,6 @@ class PreviewPostView : LinearLayout {
|
|||
|
||||
private val _imageAuthorThumbnail: ImageView;
|
||||
private val _textAuthorName: TextView;
|
||||
private val _imageNeopassChannel: ImageView;
|
||||
private val _textMetadata: TextView;
|
||||
private val _textTitle: TextView;
|
||||
private val _textDescription: TextView;
|
||||
|
@ -64,15 +62,6 @@ class PreviewPostView : LinearLayout {
|
|||
private val _layoutComments: LinearLayout?;
|
||||
private val _textComments: TextView?;
|
||||
|
||||
private var _neopassAnimator: ObjectAnimator? = null;
|
||||
|
||||
private val _taskLoadValidClaims = TaskHandler<PlatformID, PolycentricCache.CachedOwnedClaims>(StateApp.instance.scopeGetter,
|
||||
{ PolycentricCache.instance.getValidClaimsAsync(it).await() })
|
||||
.success { it -> updateClaimsLayout(it, animate = true) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load claims.", it);
|
||||
};
|
||||
|
||||
val content: IPlatformContent? get() = _content;
|
||||
|
||||
val onContentClicked = Event1<IPlatformContent>();
|
||||
|
@ -83,7 +72,6 @@ class PreviewPostView : LinearLayout {
|
|||
|
||||
_imageAuthorThumbnail = findViewById(R.id.image_author_thumbnail);
|
||||
_textAuthorName = findViewById(R.id.text_author_name);
|
||||
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
|
||||
_textMetadata = findViewById(R.id.text_metadata);
|
||||
_textTitle = findViewById(R.id.text_title);
|
||||
_textDescription = findViewById(R.id.text_description);
|
||||
|
@ -130,21 +118,8 @@ class PreviewPostView : LinearLayout {
|
|||
}
|
||||
|
||||
fun bind(content: IPlatformContent) {
|
||||
_taskLoadValidClaims.cancel();
|
||||
_content = content;
|
||||
|
||||
if (content.author.id.claimType > 0) {
|
||||
val cachedClaims = PolycentricCache.instance.getCachedValidClaims(content.author.id);
|
||||
if (cachedClaims != null) {
|
||||
updateClaimsLayout(cachedClaims, animate = false);
|
||||
} else {
|
||||
updateClaimsLayout(null, animate = false);
|
||||
_taskLoadValidClaims.run(content.author.id);
|
||||
}
|
||||
} else {
|
||||
updateClaimsLayout(null, animate = false);
|
||||
}
|
||||
|
||||
_textAuthorName.text = content.author.name;
|
||||
_textMetadata.text = content.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: "";
|
||||
|
||||
|
@ -292,25 +267,6 @@ class PreviewPostView : LinearLayout {
|
|||
};
|
||||
}
|
||||
|
||||
private fun updateClaimsLayout(claims: PolycentricCache.CachedOwnedClaims?, animate: Boolean) {
|
||||
_neopassAnimator?.cancel();
|
||||
_neopassAnimator = null;
|
||||
|
||||
val harborAvailable = claims != null && !claims.ownedClaims.isNullOrEmpty();
|
||||
if (harborAvailable) {
|
||||
_imageNeopassChannel.visibility = View.VISIBLE
|
||||
if (animate) {
|
||||
_neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500)
|
||||
_neopassAnimator?.start()
|
||||
}
|
||||
} else {
|
||||
_imageNeopassChannel.visibility = View.GONE
|
||||
}
|
||||
|
||||
//TODO: Necessary if we decide to use creator thumbnail with neopass indicator instead
|
||||
//_creatorThumbnail?.setHarborAvailable(harborAvailable, animate)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = "PreviewPostView";
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import com.futo.platformplayer.getNowDiffSeconds
|
|||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.loadThumbnails
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||
import com.futo.platformplayer.selectBestImage
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
|
@ -47,7 +46,6 @@ open class PreviewVideoView : LinearLayout {
|
|||
protected val _imageVideo: ImageView
|
||||
protected val _imageChannel: ImageView?
|
||||
protected val _creatorThumbnail: CreatorThumbnail?
|
||||
protected val _imageNeopassChannel: ImageView?;
|
||||
protected val _platformIndicator: PlatformIndicator;
|
||||
protected val _textVideoName: TextView
|
||||
protected val _textChannelName: TextView
|
||||
|
@ -57,7 +55,6 @@ open class PreviewVideoView : LinearLayout {
|
|||
protected var _playerVideoThumbnail: FutoThumbnailPlayer? = null;
|
||||
protected val _containerLive: LinearLayout;
|
||||
protected val _playerContainer: FrameLayout;
|
||||
protected var _neopassAnimator: ObjectAnimator? = null;
|
||||
protected val _layoutDownloaded: FrameLayout;
|
||||
|
||||
protected val _button_add_to_queue : View;
|
||||
|
@ -65,15 +62,6 @@ open class PreviewVideoView : LinearLayout {
|
|||
protected val _button_add_to : View;
|
||||
|
||||
protected val _exoPlayer: PlayerManager?;
|
||||
|
||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
||||
StateApp.instance.scopeGetter,
|
||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
||||
.success { it -> onProfileLoaded(it, true) }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load profile.", it);
|
||||
};
|
||||
|
||||
private val _timeBar: ProgressBar?;
|
||||
|
||||
val onVideoClicked = Event2<IPlatformVideo, Long>();
|
||||
|
@ -108,7 +96,6 @@ open class PreviewVideoView : LinearLayout {
|
|||
_button_add_to_queue = findViewById(R.id.button_add_to_queue);
|
||||
_button_add_to_watch_later = findViewById(R.id.button_add_to_watch_later);
|
||||
_button_add_to = findViewById(R.id.button_add_to);
|
||||
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
|
||||
_layoutDownloaded = findViewById(R.id.layout_downloaded);
|
||||
_timeBar = findViewById(R.id.time_bar)
|
||||
|
||||
|
@ -132,7 +119,7 @@ open class PreviewVideoView : LinearLayout {
|
|||
|
||||
fun hideAddTo() {
|
||||
_button_add_to.visibility = View.GONE
|
||||
_button_add_to_queue.visibility = View.GONE
|
||||
//_button_add_to_queue.visibility = View.GONE
|
||||
}
|
||||
|
||||
protected open fun inflate(feedStyle: FeedStyle) {
|
||||
|
@ -160,15 +147,12 @@ open class PreviewVideoView : LinearLayout {
|
|||
|
||||
|
||||
open fun bind(content: IPlatformContent) {
|
||||
_taskLoadProfile.cancel();
|
||||
|
||||
isClickable = true;
|
||||
|
||||
val isPlanned = (content.datetime?.getNowDiffSeconds() ?: 0) < 0;
|
||||
|
||||
stopPreview();
|
||||
|
||||
_imageNeopassChannel?.visibility = View.GONE;
|
||||
_creatorThumbnail?.setThumbnail(content.author.thumbnail, false);
|
||||
|
||||
val thumbnail = content.author.thumbnail
|
||||
|
@ -186,16 +170,6 @@ open class PreviewVideoView : LinearLayout {
|
|||
|
||||
_textChannelName.text = content.author.name
|
||||
|
||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(content.author.url, true);
|
||||
if (cachedProfile != null) {
|
||||
onProfileLoaded(cachedProfile, false);
|
||||
if (cachedProfile.expired) {
|
||||
_taskLoadProfile.run(content.author.id);
|
||||
}
|
||||
} else {
|
||||
_taskLoadProfile.run(content.author.id);
|
||||
}
|
||||
|
||||
_imageChannel?.clipToOutline = true;
|
||||
|
||||
_textVideoName.text = content.name;
|
||||
|
@ -335,52 +309,6 @@ open class PreviewVideoView : LinearLayout {
|
|||
_playerVideoThumbnail?.setMuteChangedListener(callback);
|
||||
}
|
||||
|
||||
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||
_neopassAnimator?.cancel();
|
||||
_neopassAnimator = null;
|
||||
|
||||
val profile = cachedPolycentricProfile?.profile;
|
||||
if (_creatorThumbnail != null) {
|
||||
val dp_32 = 32.dp(context.resources);
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_32 * dp_32)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_creatorThumbnail.setThumbnail(avatar, animate);
|
||||
} else {
|
||||
_creatorThumbnail.setThumbnail(content?.author?.thumbnail, animate);
|
||||
_creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto());
|
||||
}
|
||||
} else if (_imageChannel != null) {
|
||||
val dp_28 = 28.dp(context.resources);
|
||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_28 * dp_28)
|
||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) };
|
||||
|
||||
if (avatar != null) {
|
||||
_imageChannel.let {
|
||||
Glide.with(_imageChannel)
|
||||
.load(avatar)
|
||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||
.into(_imageChannel);
|
||||
}
|
||||
|
||||
_imageNeopassChannel?.visibility = View.VISIBLE
|
||||
if (animate) {
|
||||
_neopassAnimator = ObjectAnimator.ofFloat(_imageNeopassChannel, "alpha", 0.0f, 1.0f).setDuration(500)
|
||||
_neopassAnimator?.start()
|
||||
} else {
|
||||
_imageNeopassChannel?.alpha = 1.0f;
|
||||
}
|
||||
} else {
|
||||
_imageNeopassChannel?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
if (profile != null) {
|
||||
_textChannelName.text = profile.systemState.username
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "VideoPreviewViewHolder"
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -2,16 +2,26 @@ package com.futo.platformplayer.views.overlays
|
|||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UISlideOverlays
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.views.lists.VideoListEditorView
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
||||
|
||||
class QueueEditorOverlay : LinearLayout {
|
||||
|
||||
private val _topbar : OverlayTopbar;
|
||||
private val _editor : VideoListEditorView;
|
||||
private val _btnSettings: ImageView;
|
||||
|
||||
private val _overlayContainer: FrameLayout;
|
||||
|
||||
|
||||
val onClose = Event0();
|
||||
|
||||
|
@ -19,6 +29,9 @@ class QueueEditorOverlay : LinearLayout {
|
|||
inflate(context, R.layout.overlay_queue, this)
|
||||
_topbar = findViewById(R.id.topbar);
|
||||
_editor = findViewById(R.id.editor);
|
||||
_btnSettings = findViewById(R.id.button_settings);
|
||||
_overlayContainer = findViewById(R.id.overlay_container_queue);
|
||||
|
||||
|
||||
_topbar.onClose.subscribe(this, onClose::emit);
|
||||
_editor.onVideoOrderChanged.subscribe { StatePlayer.instance.setQueueWithExisting(it) }
|
||||
|
@ -28,6 +41,10 @@ class QueueEditorOverlay : LinearLayout {
|
|||
}
|
||||
_editor.onVideoClicked.subscribe { v -> StatePlayer.instance.setQueuePosition(v) }
|
||||
|
||||
_btnSettings.setOnClickListener {
|
||||
handleSettings();
|
||||
}
|
||||
|
||||
_topbar.setInfo(context.getString(R.string.queue), "");
|
||||
}
|
||||
|
||||
|
@ -40,4 +57,8 @@ class QueueEditorOverlay : LinearLayout {
|
|||
fun cleanup() {
|
||||
_topbar.onClose.remove(this);
|
||||
}
|
||||
|
||||
fun handleSettings() {
|
||||
UISlideOverlays.showQueueOptionsOverlay(context, _overlayContainer);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:layout_height="110dp"
|
||||
android:minHeight="0dp"
|
||||
app:layout_scrollFlags="scroll"
|
||||
app:contentInsetStart="0dp"
|
||||
|
@ -77,7 +77,16 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp" />
|
||||
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/text_meta"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="9dp"
|
||||
android:textAlignment="center"
|
||||
android:textColor="#333333"
|
||||
android:text="0 creators" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
|
|
@ -54,6 +54,22 @@
|
|||
|
||||
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_export"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:contentDescription="@string/cd_button_download"
|
||||
android:background="@drawable/background_button_round"
|
||||
android:gravity="center"
|
||||
android:layout_marginStart="10dp"
|
||||
android:orientation="horizontal"
|
||||
app:srcCompat="@drawable/ic_export"
|
||||
app:layout_constraintRight_toLeftOf="@id/button_share"
|
||||
app:layout_constraintTop_toTopOf="@id/button_share"
|
||||
app:tint="@color/white"
|
||||
android:padding="8dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:scaleType="fitCenter" />
|
||||
<ImageButton
|
||||
android:id="@+id/button_share"
|
||||
android:layout_width="40dp"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -21,5 +21,20 @@
|
|||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/topbar"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
<ImageView
|
||||
android:id="@+id/button_settings"
|
||||
android:background="@drawable/background_pill"
|
||||
android:padding="5dp"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_margin="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:srcCompat="@drawable/ic_settings" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/overlay_container_queue"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
16
app/src/main/res/layout/view_toggle_bar.xml
Normal file
16
app/src/main/res/layout/view_toggle_bar.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollbars="horizontal">
|
||||
<LinearLayout
|
||||
android:id="@+id/container_tags"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" />
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
|
@ -286,6 +286,8 @@
|
|||
<string name="also_removes_any_data_related_plugin_like_login_or_settings">Also removes any data related plugin like login or settings</string>
|
||||
<string name="announcement">Announcement</string>
|
||||
<string name="notifications">Notifications</string>
|
||||
<string name="check_disabled_plugin_updates">Check disabled plugins for updates</string>
|
||||
<string name="check_disabled_plugin_updates_description">Check disabled plugins for updates</string>
|
||||
<string name="planned_content_notifications">Planned Content Notifications</string>
|
||||
<string name="planned_content_notifications_description">Schedules discovered planned content as notifications, resulting in more accurate notifications for this content.</string>
|
||||
<string name="attempt_to_utilize_byte_ranges">Attempt to utilize byte ranges</string>
|
||||
|
@ -416,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>
|
||||
|
@ -445,6 +447,8 @@
|
|||
<string name="preferred_preview_quality">Preferred Preview Quality</string>
|
||||
<string name="preferred_preview_quality_description">Default quality while previewing a video in a feed</string>
|
||||
<string name="primary_language">Primary Language</string>
|
||||
<string name="prefer_original_audio">Prefer Original Audio</string>
|
||||
<string name="prefer_original_audio_description">Use original audio instead of preferred language when it is known</string>
|
||||
<string name="default_comment_section">Default Comment Section</string>
|
||||
<string name="hide_recommendations">Hide Recommendations</string>
|
||||
<string name="hide_recommendations_description">Fully hide the recommendations tab.</string>
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit f79c7141bcb11464103abc56fd7be492fe8568ab
|
1
app/src/stable/assets/sources/apple-podcasts
Submodule
1
app/src/stable/assets/sources/apple-podcasts
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 07e39f9df71b1937adf5bfb718a115fc232aa6f8
|
|
@ -1 +1 @@
|
|||
Subproject commit 13b30fd76e30a60c114c97b876542f7f106b5881
|
||||
Subproject commit ce0571bdeaed4e341351ef477ef4b6599aa4d0fb
|
|
@ -1 +1 @@
|
|||
Subproject commit 8d7c0e252738450f2a8bb2a48e9f8bdc24cfea54
|
||||
Subproject commit 3fbd872ad8bd7df62c5fbec7437e1200d82b74e1
|
|
@ -1 +1 @@
|
|||
Subproject commit d00c7ff8e557d8b5624c162e4e554f65625c5e29
|
||||
Subproject commit b34134ca2dbb1662b060b4a67f14e7c5d077889d
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue